com.pekinsoft.desktop.MainWindow Maven / Gradle / Ivy
Show all versions of application-framework-api Show documentation
/*
* Copyright (C) 2024 PekinSOFT Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
* *****************************************************************************
* Project : application-framework-api
* Class : MainWindow.java
* Author : Sean Carrick
* Created : Oct 28, 2024
* Modified : Oct 28, 2024
*
* Purpose: See class JavaDoc for explanation
*
* Revision History:
*
* WHEN BY REASON
* ------------ ------------------- -----------------------------------------
* Oct 28, 2024 Sean Carrick Initial creation.
* *****************************************************************************
*/
package com.pekinsoft.desktop;
import com.pekinsoft.api.PreferenceKeys;
import com.pekinsoft.api.WindowManager;
import com.pekinsoft.framework.*;
import java.awt.*;
import java.awt.event.*;
import java.beans.PropertyChangeListener;
import java.io.IOException;
import java.util.*;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.*;
import javax.swing.Timer;
/**
*
* @author Sean Carrick <sean at pekinsoft dot com>
*
* @version 1.0
* @since 1.0
*/
public abstract class MainWindow extends JFrame
implements WindowManager, PropertyChangeListener {
/**
* Constructs a new {@code MainWindow} for an {@link Application}, with the
* given {@link ApplicationContext}.
*
* This constructor is only callable by subclasses.
*
* @param context the application context
*/
protected MainWindow(ApplicationContext context) {
this.context = context;
resourceMap = context.getResourceMap(getClass());
logger = context.getLogger(getClass());
openWindows = new ArrayList<>();
// Retrieve the login timeout from the SYSTEM settings, with a default
//+ value of 5 minutes
int timeout = context.getPreferences(ApplicationContext.SYSTEM)
.getInt(PreferenceKeys.PREF_LOGIN_TIMEOUT, 300000);
if (timeout > 0) {
loginTimer = new Timer(timeout, (ActionEvent evt) -> {
performLogout();
});
}
initComponents();
}
@Override
public void showMainFrame() {
getResourceMap().injectComponents(this);
restore(this);
setVisible(true);
}
@Override
public void show(JComponent component) {
if (component == null) {
return;
}
getContext().getResourceMap(component.getClass())
.injectComponents(component);
restore(component);
setComponent(component);
}
@Override
public void show(JFrame frame) {
if (frame == null) {
return;
}
getContext().getResourceMap(frame.getClass())
.injectComponents(frame);
restore(frame);
openWindows.add(frame);
frame.setVisible(true);
}
@Override
public void show(JDialog dialog) {
getContext().getResourceMap(dialog.getClass())
.injectComponents(dialog);
restore(dialog);
// We do not save dialogs to the open windows listing.
dialog.setVisible(true);
}
@Override
public void closeMainFrame() {
save(this);
setVisible(false);
dispose();
}
@Override
public void close(JComponent component) {
save(component);
setComponent(this.component);
}
@Override
public void close(JFrame frame) {
save(frame);
openWindows.remove(frame);
frame.setVisible(false);
frame.dispose();
}
@Override
public void close(JDialog dialog) {
save(dialog);
dialog.setVisible(false);
// We do not dispose of a dialog because we do not know if the method
//+ that showed it is done with it.
}
@Override
public Container find(String name) {
for (Window window : openWindows) {
if (name.equals(window.getName())) {
return window;
} else if (window instanceof Frame frame) {
if (name.equals(frame.getTitle())) {
return frame;
}
}
}
return null;
}
@Override
public JFrame getMainFrame() {
return this;
}
@Override
public void addToolBar(JToolBar toolBar) {
if (toolBar == null) {
return;
}
List oldValue = getToolBars();
if (toolBars == null) {
toolBars = new ArrayList<>();
}
toolBars.add(toolBar);
updateToolBarsPanel(oldValue);
}
@Override
public void showToolBar(String name) {
if (name == null || name.isBlank() || name.isEmpty()) {
return;
}
for (JToolBar toolBar : getToolBars()) {
if (name.equals(toolBar.getName())) {
toolBar.setVisible(true);
getContext().getPreferences(ApplicationContext.USER)
.putBoolean(name + ".visible", true);
}
}
}
@Override
public void hideToolBar(String name) {
if (name == null || name.isBlank() || name.isEmpty()) {
return;
}
for (JToolBar toolBar : getToolBars()) {
if (name.equals(toolBar.getName())) {
toolBar.setVisible(false);
getContext().getPreferences(ApplicationContext.USER)
.putBoolean(name + ".visible", false);
}
}
}
@Override
public void showButtonText(boolean showText) {
getContext().getPreferences(ApplicationContext.USER)
.putBoolean(PreferenceKeys.PREF_HIDE_ACTION_TEXT, !showText);
for (JToolBar toolBar : getToolBars()) {
toolBar.putClientProperty("hideActionText", !showText);
for (Component c : toolBar.getComponents()) {
if (c instanceof AbstractButton button) {
button.putClientProperty("hideActionText", !showText);
}
}
}
}
/**
* Retrieves a read-only {@link List} of the
* {@link JToolBar toolbars} installed on this window.
*
* @return a list of installed toolbars
*/
public List getToolBars() {
return Collections.unmodifiableList(toolBars);
}
/**
* Sets the {@code List} of {@link JToolBar toolbars} to be installed on
* this window.
*
* If the specified {@code toolBars} list is {@code null}, a
* {@link NullPointerException} is thrown.
*
* This is a bound property.
*
* @param toolBars the toolbars to install
*
* @throws NullPointerException if {@code toolBars} is {@code null}
*/
public void setToolBars(List toolBars) {
if (toolBars == null) {
throw new NullPointerException("null toolbars");
}
List oldValue = getToolBars();
this.toolBars = new ArrayList<>(toolBars);
updateToolBarsPanel(oldValue);
}
/**
* Removes the supplied {@link JToolBar} from the toolbars installed in this
* window.
*
* If the specified {@code toolBar} is {@code null}, no exception is thrown
* and no action is taken.
*
* This is a bound property.
*
* @param toolBar the toolbar to remove
*/
@Override
public void removeToolBar(JToolBar toolBar) {
if (toolBar == null) {
return;
}
List oldValue = getToolBars();
if (toolBars != null && !toolBars.isEmpty() && toolBars
.contains(toolBar)) {
toolBars.remove(toolBar);
updateToolBarsPanel(oldValue);
}
}
/**
* Removes the {@link JToolBar} that has its
* {@link java.awt.Component#getName() name property} set to the given
* value.
*
* If the specified {@code name} is blank, empty, or {@code null}, no
* exception is thrown and no action is taken.
*
* This is a bound property.
*
* @param name the name of the toolbar to remove
*/
public void removeToolBar(String name) {
if (name == null || name.isBlank() || name.isEmpty()) {
return;
}
List oldValue = getToolBars();
for (JToolBar tb : toolBars) {
if (name.equals(tb.getName())) {
toolBars.remove(tb);
updateToolBarsPanel(oldValue);
}
}
}
/**
* Sets the status bar for the {@code Application}. Typically, the specified
* status bar will implement the
* {@link com.pekinsoft.api.ProgressHandler ProgressHandler},
* {@link com.pekinsoft.api.StatusDisplayer StatusDisplayer}, and
* {@link com.pekinsoft.api.Notifier Notifier} interfaces.
*
* This is a bound property.
*
* @param statusBar
*/
public void setStatusBar(JComponent statusBar) {
if (statusBar == null) {
throw new NullPointerException("statusBar is null");
}
JComponent oldValue = getStatusBar();
this.statusBar = statusBar;
replaceContentPaneChild(oldValue, this.statusBar, BorderLayout.PAGE_END);
firePropertyChange("statusBar", oldValue, getStatusBar());
}
/**
* Retrieves the status bar for this window. The status bar is located at
* {@link BorderLayout#PAGE_END BorderLayout.PAGE_END}.
*
* @return the status bar
*/
public JComponent getStatusBar() {
return statusBar;
}
/**
* Sets the main component for this window. This component is located at
* {@link BorderLayout#CENTER BorderLayout.CENTER}.
*
* If the specified {@code component} is {@code null}, a
* {@link NullPointerException} is thrown.
*
* This is a bound property.
*
* @param component the new main component
*
* @throws NullPointerException
*/
public void setComponent(JComponent component) {
if (component == null) {
throw new NullPointerException("component is null");
}
JComponent oldValue = getComponent();
this.component = component;
replaceContentPaneChild(oldValue, this.component, BorderLayout.CENTER);
firePropertyChange("component", oldValue, this.component);
}
/**
* Retrieves the main component of this window. This component is located at
* {@link BorderLayout#CENTER BorderLayout.CENTER}.
*
* @return the main component
*/
public JComponent getComponent() {
return component;
}
/**
* Method called from the {@link MouseAdapter} and/or {@link KeyAdapter} to
* allow the user to log back in after a period of inactivity has locked the
* window.
*/
protected abstract void performLogin();
/**
* Retrieves the {@link ApplicationContext} with which this
* {@code MainWindow} was constructed.
*
* @return the context
*/
protected ApplicationContext getContext() {
return context;
}
/**
* Convenience method for retrieving the {@link Application} for which this
* is the main window.
*
* @return the application
*/
protected Application getApplication() {
return context.getApplication();
}
/**
* Retrieves the {@code ResourceMap} containing the resources defined for
* this {@code MainWindow}.
*
* @return the resource map
*/
protected ResourceMap getResourceMap() {
return resourceMap;
}
/**
* Updates the toolbar panel whenever a {@link JToolBar} is added to or
* removed from this window.
*
* This is a bound property.
*
* @param oldToolBars the old toolbars list
*/
protected void updateToolBarsPanel(List oldToolBars) {
List oldValue = oldToolBars;
JComponent oldToolBarsPanel = this.toolBarsPanel;
JComponent newToolBarsPanel = new JPanel(new FlowLayout(
FlowLayout.LEADING));
boolean hideActionText = getContext()
.getPreferences(ApplicationContext.USER)
.getBoolean(PreferenceKeys.PREF_HIDE_ACTION_TEXT, true);
for (JToolBar tb : toolBars) {
tb.putClientProperty("hideActionText", hideActionText);
for (Component c : tb.getComponents()) {
if (c instanceof AbstractButton button) {
button.putClientProperty("hideActionText", hideActionText);
}
}
newToolBarsPanel.add(tb);
}
replaceContentPaneChild(oldToolBarsPanel, newToolBarsPanel,
BorderLayout.NORTH);
firePropertyChange("toolBars", oldValue, this.toolBars);
}
/**
* Replaces the specified {@code oldChild} {@link Component} with the
* specified {@code newChild} component, using the given constraints.
*
* If either of the specified components is {@code null}, no exception is
* thrown and no action is taken.
*
* @param oldChild the component being replaced
* @param newChild the component to place
* @param constraint constraints for the placement of the new component
*/
protected void replaceContentPaneChild(JComponent oldChild,
JComponent newChild, String constraint) {
Container contentPane = getRootPane().getContentPane();
if (oldChild != null) {
contentPane.remove(oldChild);
}
if (newChild != null) {
contentPane.add(newChild, constraint);
SwingUtilities.updateComponentTreeUI(contentPane);
SwingUtilities.updateComponentTreeUI(newChild);
for (Component c : newChild.getComponents()) {
SwingUtilities.updateComponentTreeUI(c);
}
contentPane.invalidate();
newChild.invalidate();
}
}
/**
* Restores the last saved session state for the specified
* {@code component}.
*
* @param component the component for which session state is to be restored
*/
protected void restore(Component component) {
getContext().getResourceMap(component.getClass())
.injectComponents(component);
String fileName = sessionFileName(component);
try {
getContext().getSessionStorage().restore(component, fileName);
} catch (IOException e) {
log(Level.WARNING, "Unable to restore session state for \""
+ "{0}\": {1}", new Object[]{fileName, e.getMessage()});
}
}
/**
* Saves the session state for the specified {@code component}.
*
* @param component the component for which session state is to be saved
*/
protected void save(Component component) {
String fileName = sessionFileName(component);
try {
getContext().getSessionStorage().save(component, fileName);
} catch (IOException e) {
log(Level.WARNING, "Unable to save session state for \""
+ "{0}\": {1}", new Object[]{fileName, e.getMessage()});
}
}
/**
* Retrieves a file name for the session state file for the specified
* {@link Component}. If the component is {@code null}, {@code null} is
* returned.
*
* @param c the component for which a session state file name is needed
*
* @return the session state file name
*/
protected String sessionFileName(Component c) {
if (c == null) {
return null;
}
return c.getName() + ".session.xml";
}
/**
* Convenience method for subclasses to perform logging using the
* {@link java.util.logging.Logger Logger} obtained from the
* {@link ApplicationContext} for this class.
*
* @param level the {@link java.util.logging.Level Level} at which the
* message should be logged
* @param msg the message to be logged
* @param args any arguments to the message or a {@link Throwable}
*/
protected void log(Level level, String msg, Object... args) {
logger.log(level, msg, args);
}
private void performLogout() {
setGlassPane(opaqueGlassPane);
getGlassPane().setVisible(true);
}
private void resetLogoutTimer() {
loginTimer.restart();
}
private void initComponents() {
setStatusBar(StatusBar.getInstance());
setLayout(new BorderLayout());
component = new JPanel();
component.setLayout(new BorderLayout());
add(statusBar, BorderLayout.SOUTH);
add(toolBarsPanel, BorderLayout.NORTH);
add(component, BorderLayout.CENTER);
addMouseListener(new MouseTimerListener());
addKeyListener(new KeyTimerListener());
}
protected final List openWindows;
private final Logger logger;
private final ApplicationContext context;
private final ResourceMap resourceMap;
private final JComponent opaqueGlassPane = new SemiOpaqueGlassPane();
private List toolBars = new ArrayList<>();
private Timer loginTimer;
private JComponent toolBarsPanel = new JPanel(new FlowLayout(
FlowLayout.LEADING));
private JComponent component = null;
private JComponent statusBar = null;
private final class MouseTimerListener extends MouseAdapter {
@Override
public void mouseMoved(MouseEvent e) {
resetLogoutTimer();
}
@Override
public void mouseExited(MouseEvent e) {
resetLogoutTimer();
}
@Override
public void mouseEntered(MouseEvent e) {
resetLogoutTimer();
}
@Override
public void mouseReleased(MouseEvent e) {
resetLogoutTimer();
}
@Override
public void mousePressed(MouseEvent e) {
resetLogoutTimer();
}
}
private final class KeyTimerListener extends KeyAdapter {
@Override
public void keyReleased(KeyEvent e) {
resetLogoutTimer();
}
@Override
public void keyPressed(KeyEvent e) {
resetLogoutTimer();
}
@Override
public void keyTyped(KeyEvent e) {
resetLogoutTimer();
}
}
private final class SemiOpaqueGlassPane extends JComponent {
public SemiOpaqueGlassPane() {
setSize(MainWindow.this.getSize());
}
@Override
public void paint(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g.create();
g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
0.8f));
g2.setColor(getBackground());
g2.fillRect(0, 0, getWidth(), getHeight());
g2.dispose();
}
}
}