All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.github.jonestimd.swing.window.FrameManager Maven / Gradle / Ivy

There is a newer version: 1.4.5
Show newest version
// The MIT License (MIT)
//
// Copyright (c) 2019 Timothy D. Jones
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
package io.github.jonestimd.swing.window;

import java.awt.Component;
import java.awt.Container;
import java.awt.Dialog;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Predicate;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;

import io.github.jonestimd.swing.ClientProperty;
import io.github.jonestimd.swing.ComponentFactory;
import io.github.jonestimd.swing.ComponentTreeUtils;
import io.github.jonestimd.swing.action.LocalizedAction;

/**
 * Manages an application's windows. This class creates window and initializes their menu bar with the {@code Windows}
 * and {@code Help} menus.  This class updates the {@code Windows} menu whenever another window is opened or closed.
 * Typically, menu items for opening new windows are created using {@link FrameAction}.  Non-singleton
 * windows can be reused by overriding {@link ApplicationWindowAction#matches(StatusFrame)}.
 * @param  The class defining the window types (typically an {@code enum})
 */
public class FrameManager {
    protected static final String WINDOWS_MENU_KEY = "menu.windows.mnemonicAndName";
    private static final Predicate IS_WINDOW_MENU = menu -> WINDOWS_MENU_KEY.equals(menu.getClientProperty(ClientProperty.MNEMONIC_AND_NAME_KEY));
    private final ResourceBundle bundle;
    private final Map singletonPanels = new HashMap<>();
    private final Function singletonPanelSupplier;
    private final Map>> panelFactories;
    private final Map singletonFrames = new HashMap<>();
    private final List frames = new ArrayList<>();
    private final Map menuActions = new HashMap<>();
    private final FrameListener frameListener = new FrameListener();
    private final Function aboutDialogSupplier;
    private final BiFunction frameFactory;

    /**
     * Create a FrameManager that uses default {@link StatusFrame}s.
     * @param bundle the {@link ResourceBundle} to use for localization of the windows
     * @param singletonPanelSupplier a function that returns the content panels of the singleton windows
     * @param panelFactories a map of the factories for the content panels of the non-singleton windows
     * @param aboutDialogSupplier a factory for creating the {@code About} dialog
     */
    public FrameManager(ResourceBundle bundle, Function singletonPanelSupplier,
            Map>> panelFactories,
            Function aboutDialogSupplier) {
        this(bundle, singletonPanelSupplier, panelFactories, aboutDialogSupplier, StatusFrame::new);
    }

    /**
     * Create a FrameManager that uses custom {@link StatusFrame}s.
     * @param bundle the {@link ResourceBundle} to use for localization of the windows
     * @param singletonPanelSupplier a function that returns the content panels of the singleton windows
     * @param panelFactories a map of the factories for the content panels of the non-singleton windows
     * @param aboutDialogSupplier a factory for creating the {@code About} dialog
     * @param frameFactory a factory for creating {@link StatusFrame}s for the windows
     */
    public FrameManager(ResourceBundle bundle, Function singletonPanelSupplier,
            Map>> panelFactories,
            Function aboutDialogSupplier, BiFunction frameFactory) {
        this.bundle = bundle;
        this.singletonPanelSupplier = singletonPanelSupplier;
        this.panelFactories = panelFactories;
        this.aboutDialogSupplier = aboutDialogSupplier;
        this.frameFactory = frameFactory;
    }

    /**
     * Display a singleton window or create a new instance of a non-singleton window.
     * @param action action describing the window
     * @return the matching window
     */
    public StatusFrame onWindowEvent(ApplicationWindowAction action) {
        Key windowInfo = action.getWindowInfo();
        return windowInfo.isSingleton() ? showSingletonFrame(windowInfo) : showFrame(action);
    }

    @SuppressWarnings("unchecked")
    private PanelFactory> getPanelFactory(Key windowInfo) {
        return ((PanelFactory>) panelFactories.get(windowInfo));
    }

    /**
     * Register a singleton window.
     * @param key the window type
     * @param frame the window
     * @throws IllegalArgumentException if another window has already been registered for the specified {@code key}
     */
    public void addSingletonFrame(Key key, StatusFrame frame) {
        if (!singletonFrames.containsKey(key)) {
            singletonFrames.put(key, frame);
            initializeFrame(frame);
            frame.setContentPane(singletonPanels.computeIfAbsent(key, singletonPanelSupplier));
        }
        else if (singletonFrames.get(key) != frame) {
            throw new IllegalArgumentException("Duplicate JFrame ID: " + key.toString());
        }
    }

    /**
     * Make a singleton window visible or, if it is already visible, bring it to the front.
     * @param key the window type
     * @return the singleton window
     */
    public StatusFrame showSingletonFrame(Key key) {
        StatusFrame frame = singletonFrames.get(key);
        if (frame == null) {
            frame = frameFactory.apply(bundle, key.getResourcePrefix());
            addSingletonFrame(key, frame);
            frame.setVisible(true);
        }
        else {
            frame.toFront();
        }
        return frame;
    }

    /**
     * Make a non-singleton window visible or, if it is already visible, bring it to the front.
     * @param action the action that identifies the window
     * @return the window
     */
    public StatusFrame showFrame(ApplicationWindowAction action) {
        return frames.stream().filter(action::matches).findFirst().map(frame -> {
            frame.toFront();
            return frame;
        }).orElseGet(() -> createFrame(action));
    }

    /**
     * Create and display a non-singleton window.
     * @param action the window action specifying the type of window
     * @return the new window
     */
    protected StatusFrame createFrame(ApplicationWindowAction action) {
        Key windowType = action.getWindowInfo();
        StatusFrame frame = frameFactory.apply(bundle, windowType.getResourcePrefix());
        addFrame(frame, getPanelFactory(windowType).createPanel(action));
        frame.setVisible(true);
        return frame;
    }

    /**
     * Add an existing non-singleton window to this manager and initialize its content pane.
     * @param frame the window
     * @param windowType the window type
     * @return the new content pane
     */
    public Container addFrame(StatusFrame frame, Key windowType) {
        Container contentPane = getPanelFactory(windowType).createPanel();
        addFrame(frame, contentPane);
        return contentPane;
    }

    public void addFrame(StatusFrame frame, Container contentPane) {
        frames.add(frame);
        initializeFrame(frame);
        frame.setContentPane(contentPane);
    }

    private JMenuBar createJMenuBar() {
        JMenuBar menuBar = new JMenuBar();
        menuBar.add(createWindowsMenu());
        menuBar.add(createHelpMenu());
        return menuBar;
    }

    private JMenu createHelpMenu() {
        JMenu menu = ComponentFactory.newMenu(bundle, "menu.help.mnemonicAndName");
        menu.add(LocalizedAction.create(bundle, "menu.help.about",
                (event) -> {
                    Window window = ComponentTreeUtils.findAncestor((Component) event.getSource(), Window.class);
                    aboutDialogSupplier.apply(window).setVisible(true);
                })
        );
        return menu;
    }

    private JMenu createWindowsMenu() {
        JMenu menu = ComponentFactory.newMenu(bundle, WINDOWS_MENU_KEY);
        for (WindowAction action : menuActions.values()) {
            menu.add(new JMenuItem(action));
        }
        return menu;
    }

    private void initializeFrame(StatusFrame frame) {
        frame.addWindowListener(frameListener);
        frame.addPropertyChangeListener("title", frameListener);
        frame.setJMenuBar(createJMenuBar());
        addMenuItem(new WindowAction(frame));
    }

    private void addMenuItem(WindowAction action) {
        menuActions.put(action.frame, action);
        addWindowAction(frames, action);
        addWindowAction(singletonFrames.values(), action);
    }

    private void addWindowAction(Collection windows, WindowAction action) {
        for (StatusFrame window : windows) {
            getWindowsMenu(window.getJMenuBar()).add(new JMenuItem(action));
        }
    }

    private JMenu getWindowsMenu(JMenuBar menuBar) {
        return ComponentTreeUtils.findComponent(menuBar, JMenu.class, IS_WINDOW_MENU);
    }

    private void removeWindow(StatusFrame frame) {
        WindowAction action = menuActions.remove(frame);
        singletonFrames.values().remove(frame);
        frames.remove(frame);
        removeWindowAction(singletonFrames.values(), action);
        removeWindowAction(frames, action);
        // if it's a singleton this will remove the singleton panel from the frame's hierarchy
        frame.getLayeredPane().remove(frame.getContentPane());
    }

    private void removeWindowAction(Collection windows, WindowAction action) {
        for (StatusFrame window : windows) {
            removeMenuItem(getWindowsMenu(window.getJMenuBar()), action);
        }
    }

    private void removeMenuItem(JMenu menu, WindowAction action) {
        for (int i = 0; i < menu.getMenuComponentCount(); i++) {
            Component component = menu.getMenuComponent(i);
            if (getWindowAction(component) == action) {
                menu.remove(component);
            }
        }
    }

    private Action getWindowAction(Component component) {
        return component instanceof JMenuItem ? ((JMenuItem) component).getAction() : null;
    }

    public int getFrameCount() {
        return singletonFrames.size() + frames.size();
    }

    private static class WindowAction extends AbstractAction {
        private StatusFrame frame;

        private WindowAction(StatusFrame frame) {
            super(frame.getTitle());
            this.frame = frame;
        }

        public void actionPerformed(ActionEvent e) {
            frame.toFront();
        }
    }

    private class FrameListener extends WindowAdapter implements PropertyChangeListener {
        public void windowActivated(WindowEvent e) {
            menuActions.get(e.getSource()).setEnabled(false);
        }

        public void windowDeactivated(WindowEvent e) {
            if (menuActions.containsKey(e.getSource())) {
                menuActions.get(e.getSource()).setEnabled(true);
            }
        }

        public void windowClosed(WindowEvent e) {
            removeWindow((StatusFrame) e.getSource());
        }

        public void propertyChange(PropertyChangeEvent evt) {
            JFrame frame = (JFrame) evt.getSource();
            menuActions.get(frame).putValue(Action.NAME, frame.getTitle());
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy