/* * Copyright ©1998-2020 by Richard A. Wilkes. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, version 2.0. If a copy of the MPL was not distributed with * this file, You can obtain one at http://mozilla.org/MPL/2.0/. * * This Source Code Form is "Incompatible With Secondary Licenses", as * defined by the Mozilla Public License, version 2.0. */ package com.trollworks.gcs.ui.widget; import com.trollworks.gcs.menu.StdMenuBar; import com.trollworks.gcs.menu.edit.Undoable; import com.trollworks.gcs.menu.file.QuitCommand; import com.trollworks.gcs.menu.file.SaveCommand; import com.trollworks.gcs.menu.file.Saveable; import com.trollworks.gcs.preferences.MenuKeyPreferences; import com.trollworks.gcs.preferences.Preferences; import com.trollworks.gcs.ui.WindowSizeEnforcer; import com.trollworks.gcs.ui.image.Images; import com.trollworks.gcs.ui.layout.FlexRow; import com.trollworks.gcs.utility.FileProxy; import com.trollworks.gcs.utility.FilteredIterator; import com.trollworks.gcs.utility.Log; import com.trollworks.gcs.utility.json.JsonMap; import com.trollworks.gcs.utility.json.JsonWriter; import com.trollworks.gcs.utility.undo.StdUndoManager; import java.awt.AWTEvent; import java.awt.BorderLayout; import java.awt.Component; import java.awt.Container; import java.awt.Frame; import java.awt.Graphics; import java.awt.Insets; import java.awt.Rectangle; import java.awt.Window; import java.awt.event.WindowEvent; import java.awt.event.WindowFocusListener; import java.awt.event.WindowListener; import java.io.IOException; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import javax.swing.JComponent; import javax.swing.JFrame; import javax.swing.JToolBar; import javax.swing.WindowConstants; /** Provides a base OS-level window. */ public class BaseWindow extends JFrame implements Undoable, Comparable<BaseWindow>, WindowListener, WindowFocusListener { private static final ArrayList<BaseWindow> WINDOW_LIST = new ArrayList<>(); private StdUndoManager mUndoManager; private boolean mIsClosed; boolean mWasAlive; /** * Creates a new {@link BaseWindow}. * * @param title The window title. May be {@code null}. */ public BaseWindow(String title) { super(title, null); setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); setLocationByPlatform(true); ((JComponent) getContentPane()).setDoubleBuffered(true); getToolkit().setDynamicLayout(true); addWindowListener(this); addWindowFocusListener(this); WindowSizeEnforcer.monitor(this); MenuKeyPreferences.loadFromPreferences(); setJMenuBar(new StdMenuBar()); setIconImages(Images.APP_ICON_LIST); mUndoManager = new StdUndoManager(); enableEvents(AWTEvent.KEY_EVENT_MASK | AWTEvent.MOUSE_EVENT_MASK); WINDOW_LIST.add(this); } /** @return {@code true} if the window has been closed. */ public final boolean isClosed() { return mIsClosed; } @Override public void setVisible(boolean visible) { if (visible) { mWasAlive = true; } super.setVisible(visible); } /** Call to create the toolbar for this window. */ protected final void createToolBar() { JToolBar toolbar = new JToolBar(); toolbar.setFloatable(false); FlexRow row = new FlexRow(); row.setInsets(new Insets(2, 5, 2, 5)); createToolBarContents(toolbar, row); row.apply(toolbar); add(toolbar, BorderLayout.NORTH); } /** * Called to create the toolbar contents for this window. * * @param toolbar The {@link JToolBar} to add items to. * @param row The {@link FlexRow} layout to add items to. */ protected void createToolBarContents(JToolBar toolbar, FlexRow row) { // Does nothing by default. } @Override public void toFront() { if (!isClosed()) { if (getExtendedState() == ICONIFIED) { setExtendedState(NORMAL); } super.toFront(); if (!isActive() || !isFocused()) { Component focus = getMostRecentFocusOwner(); if (focus != null) { focus.requestFocus(); } else { requestFocus(); } } } } @Override public void dispose() { if (!isClosed()) { WINDOW_LIST.remove(this); } if (!mIsClosed) { try { saveBounds(); super.dispose(); } catch (Exception ex) { // Necessary, since the AWT appears to sometimes spuriously try // to call Container.removeNotify() more than once on itself. } mIsClosed = true; } } @Override public void windowActivated(WindowEvent event) { // Unused } @Override public void windowClosed(WindowEvent event) { QuitCommand.INSTANCE.quitIfNoSignificantWindowsOpen(); } @Override public void windowClosing(WindowEvent event) { if (!hasOwnedWindowsShowing(this)) { List<Saveable> saveables = new ArrayList<>(); collectSaveables(this, saveables); if (SaveCommand.attemptSave(saveables)) { dispose(); } } } private void collectSaveables(Component component, List<Saveable> saveables) { if (component instanceof Container) { Container container = (Container) component; int count = container.getComponentCount(); for (int i = 0; i < count; i++) { collectSaveables(container.getComponent(i), saveables); } } if (component instanceof Saveable) { saveables.add((Saveable) component); } } @Override public void windowDeactivated(WindowEvent event) { Commitable.sendCommitToFocusOwner(); } @Override public void windowDeiconified(WindowEvent event) { // Unused } @Override public void windowIconified(WindowEvent event) { // Unused } @Override public void windowOpened(WindowEvent event) { // On windows, this is necessary to prevent the window from opening in the background. toFront(); } @Override public void windowGainedFocus(WindowEvent event) { if (event.getWindow() == this) { WINDOW_LIST.remove(this); WINDOW_LIST.add(0, this); } } @Override public void windowLostFocus(WindowEvent event) { Commitable.sendCommitToFocusOwner(); } /** * Invalidates the specified component and all of its children, recursively. * * @param comp The root component to start with. */ public void invalidate(Component comp) { comp.invalidate(); comp.repaint(); if (comp instanceof Container) { for (Component child : ((Container) comp).getComponents()) { invalidate(child); } } } /** @return The title to be used for this window in the window menu. */ protected String getTitleForWindowMenu() { return getTitle(); } @Override public int compareTo(BaseWindow other) { if (this != other) { String title = getTitleForWindowMenu(); String otherTitle = other.getTitleForWindowMenu(); if (title != null) { if (otherTitle == null) { return 1; } return title.compareTo(otherTitle); } if (otherTitle != null) { return -1; } } return 0; } /** @return The window's {@link StdUndoManager}. */ @Override public StdUndoManager getUndoManager() { return mUndoManager; } /** * @return The key for the window preferences. If {@code null} is returned from this method, * then no standard window preferences will be saved. Returns {@code null} by default. */ @SuppressWarnings("static-method") public String getWindowPrefsKey() { return null; } /** * Saves the window bounds to preferences. Preferences must be saved for this to have a lasting * effect. */ public void saveBounds() { String key = getWindowPrefsKey(); if (key != null) { boolean wasMaximized = (getExtendedState() & MAXIMIZED_BOTH) != 0; if (wasMaximized || getExtendedState() == ICONIFIED) { setExtendedState(NORMAL); } Preferences.getInstance().putBaseWindowPosition(key, new Position(this)); } } /** Restores the window to its saved location and size. */ public void restoreBounds() { boolean needPack = true; String key = getWindowPrefsKey(); if (key != null) { Position info = Preferences.getInstance().getBaseWindowPosition(key); if (info != null) { info.apply(this); needPack = false; } } if (needPack) { pack(); } WindowUtils.forceOnScreen(this); } /** @return The top-most window. */ public static BaseWindow getTopWindow() { if (!WINDOW_LIST.isEmpty()) { return WINDOW_LIST.get(0); } return null; } /** * @param <T> The window type. * @param type The window type to return. * @return A list of all windows of the specified type. */ public static <T extends BaseWindow> List<T> getWindows(Class<T> type) { List<T> windows = new ArrayList<>(); Frame[] frames = Frame.getFrames(); for (Frame frame : frames) { if (type.isInstance(frame)) { T window = type.cast(frame); if (window.mWasAlive && !window.isClosed()) { windows.add(window); } } } return windows; } /** * @param windowClass The window class to return. * @param <T> The window type. * @return The current visible windows, in order from top to bottom. */ public static <T extends BaseWindow> ArrayList<T> getActiveWindows(Class<T> windowClass) { ArrayList<T> list = new ArrayList<>(); for (T window : new FilteredIterator<>(WINDOW_LIST, windowClass)) { if (window.isShowing()) { list.add(window); } } return list; } /** @return A list of all {@link BaseWindow}s created by this application. */ public static List<BaseWindow> getAllAppWindows() { return getWindows(BaseWindow.class); } /** * @param path The backing file to look for. * @return The {@link FileProxy} associated with the specified backing file. */ public static FileProxy findFileProxy(Path path) { path = path.normalize().toAbsolutePath(); for (BaseWindow window : getAllAppWindows()) { if (window instanceof FileProxy) { FileProxy proxy = (FileProxy) window; Path wPath = proxy.getBackingFile(); if (wPath != null) { if (wPath.normalize().toAbsolutePath().equals(path)) { return proxy; } } } } return null; } /** * @param window The window to check. * @return {@code true} if an owned window is showing. */ public static boolean hasOwnedWindowsShowing(Window window) { if (window != null) { for (Window one : window.getOwnedWindows()) { if (one.isShowing() && one.getType() != Window.Type.POPUP) { return true; } } } return false; } @Override public void paint(Graphics g) { // This is just here to try and capture a log message if something goes wrong in the // drawing code. try { super.paint(g); } catch(Throwable throwable) { Log.error(throwable); } } /** Forces a full repaint and invalidate on all windows, disposing of any window buffers. */ public static void forceRepaintAndInvalidate() { for (BaseWindow window : getAllAppWindows()) { window.invalidate(window.getRootPane()); } } public static class Position { private static final String X = "x"; private static final String Y = "y"; private static final String WIDTH = "width"; private static final String HEIGHT = "height"; private static final String LAST_UPDATED = "last_updated"; public Rectangle mBounds; public long mLastUpdated; public Position(BaseWindow wnd) { mBounds = wnd.getBounds(); mLastUpdated = System.currentTimeMillis(); } public Position(JsonMap m) { mBounds = new Rectangle(m.getIntWithDefault(X, 0), m.getIntWithDefault(Y, 0), m.getIntWithDefault(WIDTH, 1), m.getIntWithDefault(HEIGHT, 1)); mLastUpdated = m.getLong(LAST_UPDATED); } void apply(BaseWindow wnd) { wnd.setBounds(mBounds); } public void toJSON(JsonWriter w) throws IOException { w.startMap(); w.keyValue(X, mBounds.x); w.keyValue(Y, mBounds.y); w.keyValue(WIDTH, mBounds.width); w.keyValue(HEIGHT, mBounds.height); w.keyValue(LAST_UPDATED, mLastUpdated); w.endMap(); } } }