/* * 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.ui.UIUtilities; import com.trollworks.gcs.ui.WindowSizeEnforcer; import com.trollworks.gcs.ui.image.Images; import com.trollworks.gcs.utility.Geometry; import com.trollworks.gcs.utility.I18n; import com.trollworks.gcs.utility.Log; import com.trollworks.gcs.utility.Platform; import java.awt.Component; import java.awt.Container; import java.awt.Desktop; import java.awt.Dialog; import java.awt.Dimension; import java.awt.Frame; import java.awt.GraphicsDevice; import java.awt.GraphicsEnvironment; import java.awt.Insets; import java.awt.MouseInfo; import java.awt.Point; import java.awt.Rectangle; import java.awt.Robot; import java.awt.Toolkit; import java.awt.Window; import java.awt.event.InputEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import javax.swing.Icon; import javax.swing.JDialog; import javax.swing.JOptionPane; import javax.swing.text.JTextComponent; /** Utilities for use with windows. */ public class WindowUtils { private static Frame HIDDEN_FRAME; /** * @param comp The {@link Component} to use for determining the parent {@link Frame} or {@link * Dialog}. * @param msg The message to display. */ public static void showError(Component comp, String msg) { JOptionPane.showMessageDialog(comp, msg, I18n.Text("Error"), JOptionPane.ERROR_MESSAGE); } /** * @param comp The {@link Component} to use for determining the parent {@link Frame} or {@link * Dialog}. * @param msg The message to display. */ public static void showError(Component comp, Component msg) { JOptionPane.showMessageDialog(comp, msg, I18n.Text("Error"), JOptionPane.ERROR_MESSAGE); } /** * @param comp The {@link Component} to use for determining the parent {@link Frame} or {@link * Dialog}. * @param msg The message to display. */ public static void showWarning(Component comp, String msg) { JOptionPane.showMessageDialog(comp, msg, I18n.Text("Warning"), JOptionPane.WARNING_MESSAGE); } /** * @param comp The {@link Component} to use for determining the parent {@link Frame} or {@link * Dialog}. * @param msg The message to display. */ public static void showWarning(Component comp, Component msg) { JOptionPane.showMessageDialog(comp, msg, I18n.Text("Warning"), JOptionPane.WARNING_MESSAGE); } /** * Shows a confirmation dialog with custom options. * * @param comp The {@link Component} to use. May be {@code null}. * @param message The message. * @param title The title to use. * @param optionType The type of option dialog. Use the {@link JOptionPane} constants. * @param options The options to display. * @param initialValue The initial option. * @return See the documentation for {@link JOptionPane}. */ public static int showConfirmDialog(Component comp, String message, String title, int optionType, Object[] options, Object initialValue) { return showOptionDialog(comp, message, title, false, optionType, JOptionPane.QUESTION_MESSAGE, null, options, initialValue); } /** * Shows an option dialog. * * @param parentComponent The parent {@link Component} to use. May be {@code null}. * @param message The message. May be a {@link Component}. * @param title The title to use. * @param resizable Whether to allow the dialog to be resized by the user. * @param optionType The type of option dialog. Use the {@link JOptionPane} constants. * @param messageType The type of message. Use the {@link JOptionPane} constants. * @param icon The icon to use. May be {@code null}. * @param options The options to display. May be {@code null}. * @param initialValue The initial option. * @return See the documentation for {@link JOptionPane}. */ public static int showOptionDialog(Component parentComponent, Object message, String title, boolean resizable, int optionType, int messageType, Icon icon, Object[] options, Object initialValue) { JOptionPane pane = new JOptionPane(message, messageType, optionType, icon, options, initialValue); pane.setUI(new SizeAwareBasicOptionPaneUI(pane.getUI())); pane.setInitialValue(initialValue); pane.setComponentOrientation((parentComponent == null ? JOptionPane.getRootFrame() : parentComponent).getComponentOrientation()); JDialog dialog = pane.createDialog(getWindowForComponent(parentComponent), title); WindowSizeEnforcer.monitor(dialog); pane.selectInitialValue(); dialog.setResizable(resizable); Component field = getFirstFocusableField(message); if (field != null) { dialog.addWindowFocusListener(new WindowAdapter() { @Override public void windowGainedFocus(WindowEvent event) { field.requestFocus(); dialog.removeWindowFocusListener(this); } }); } dialog.setVisible(true); dialog.dispose(); pane.setMessage(null); Object selectedValue = pane.getValue(); if (selectedValue != null) { if (options == null) { if (selectedValue instanceof Integer) { return ((Integer) selectedValue).intValue(); } } else { int length = options.length; for (int i = 0; i < length; i++) { if (options[i].equals(selectedValue)) { return i; } } } } return JOptionPane.CLOSED_OPTION; } private static Component getFirstFocusableField(Object comp) { if (comp instanceof JTextComponent || comp instanceof KeyStrokeDisplay) { return (Component) comp; } if (comp instanceof Container) { for (Component child : ((Container) comp).getComponents()) { Component field = getFirstFocusableField(child); if (field != null) { return field; } } } return null; } /** * @param comp The {@link Component} to use. May be {@code null}. * @return The most logical {@link Window} associated with the component. */ public static Window getWindowForComponent(Component comp) { while (true) { if (comp == null) { return JOptionPane.getRootFrame(); } if (comp instanceof Frame || comp instanceof Dialog) { return (Window) comp; } comp = comp.getParent(); } } /** * Looks for the screen device that contains the largest part of the specified window. * * @param window The window to determine the preferred screen device for. * @return The preferred screen device. */ public static GraphicsDevice getPreferredScreenDevice(Window window) { return getPreferredScreenDevice(window.getBounds()); } /** * Looks for the screen device that contains the largest part of the specified global bounds. * * @param bounds The global bounds to determine the preferred screen device for. * @return The preferred screen device. */ public static GraphicsDevice getPreferredScreenDevice(Rectangle bounds) { GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); GraphicsDevice best = ge.getDefaultScreenDevice(); Rectangle overlapBounds = Geometry.intersection(bounds, best.getDefaultConfiguration().getBounds()); int bestOverlap = overlapBounds.width * overlapBounds.height; for (GraphicsDevice gd : ge.getScreenDevices()) { if (gd.getType() == GraphicsDevice.TYPE_RASTER_SCREEN) { overlapBounds = Geometry.intersection(bounds, gd.getDefaultConfiguration().getBounds()); if (overlapBounds.width * overlapBounds.height > bestOverlap) { best = gd; } } } return best; } /** @return The maximum bounds that fits on the main screen. */ public static Rectangle getMaximumWindowBounds() { return getMaximumWindowBounds(GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration().getBounds()); } /** * Determines the screen that most contains the specified window and returns the maximum size * the window can be on that screen. * * @param window The window to determine a maximum bounds for. * @return The maximum bounds that fits on a screen. */ public static Rectangle getMaximumWindowBounds(Window window) { return getMaximumWindowBounds(window.getBounds()); } /** * Determines the screen that most contains the specified panel area and returns the maximum * size a window can be on that screen. * * @param panel The panel that contains the area. * @param area The area within the panel to use when determining the maximum bounds for a * window. * @return The maximum bounds that fits on a screen. */ public static Rectangle getMaximumWindowBounds(Component panel, Rectangle area) { area = new Rectangle(area); UIUtilities.convertRectangleToScreen(area, panel); return getMaximumWindowBounds(area); } /** * Determines the screen that most contains the specified global bounds and returns the maximum * size a window can be on that screen. * * @param bounds The global bounds to use when determining the maximum bounds for a window. * @return The maximum bounds that fits on a screen. */ public static Rectangle getMaximumWindowBounds(Rectangle bounds) { GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); GraphicsDevice gd = getPreferredScreenDevice(bounds); if (gd == ge.getDefaultScreenDevice()) { bounds = ge.getMaximumWindowBounds(); // The Mac (and now Windows as of Java 5) already return the correct // value... try to fix it up for the other platforms. This doesn't // currently work, either, since the other platforms seem to always // return empty insets. if (!Platform.isMacintosh() && !Platform.isWindows()) { Insets insets = Toolkit.getDefaultToolkit().getScreenInsets(gd.getDefaultConfiguration()); // Since this is failing to do the right thing anyway, we're going // to try and come up with some reasonable limitations... if (insets.top == 0 && insets.bottom == 0) { insets.bottom = 48; } bounds.x += insets.left; bounds.y += insets.top; bounds.width -= insets.left + insets.right; bounds.height -= insets.top + insets.bottom; } return bounds; } return gd.getDefaultConfiguration().getBounds(); } public static void packAndCenterWindowOn(Window window, Component centeredOn) { window.pack(); Dimension prefSize = window.getPreferredSize(); Dimension minSize = window.getMinimumSize(); int width = Math.max(prefSize.width, minSize.width); int height = Math.max(prefSize.height, minSize.height); int x; int y; if (centeredOn != null) { Point where = centeredOn.getLocationOnScreen(); Dimension size = centeredOn.getSize(); x = where.x + (size.width - width) / 2; y = where.y + (size.height - height) / 2; } else { Rectangle bounds = getMaximumWindowBounds(window); x = bounds.x + (bounds.width - width) / 2; y = bounds.y + (bounds.height - height) / 2; } window.setLocation(x, y); forceOnScreen(window); } /** * Forces the specified window onscreen. * * @param window The window to force onscreen. */ public static void forceOnScreen(Window window) { Rectangle maxBounds = getMaximumWindowBounds(window); Rectangle bounds = window.getBounds(); Point location = new Point(bounds.x, bounds.y); Dimension size = window.getMinimumSize(); if (bounds.width < size.width) { bounds.width = size.width; } if (bounds.height < size.height) { bounds.height = size.height; } if (bounds.x < maxBounds.x) { bounds.x = maxBounds.x; } else if (bounds.x >= maxBounds.x + maxBounds.width) { bounds.x = maxBounds.x + maxBounds.width - 1; } if (bounds.x + bounds.width >= maxBounds.x + maxBounds.width) { bounds.x = maxBounds.x + maxBounds.width - bounds.width; if (bounds.x < maxBounds.x) { bounds.x = maxBounds.x; bounds.width = maxBounds.width; } } if (bounds.y < maxBounds.y) { bounds.y = maxBounds.y; } else if (bounds.y >= maxBounds.y + maxBounds.height) { bounds.y = maxBounds.y + maxBounds.height - 1; } if (bounds.y + bounds.height >= maxBounds.y + maxBounds.height) { bounds.y = maxBounds.y + maxBounds.height - bounds.height; if (bounds.y < maxBounds.y) { bounds.y = maxBounds.y; bounds.height = maxBounds.height; } } if (location.x != bounds.x || location.y != bounds.y) { window.setBounds(bounds); } else { window.setSize(bounds.width, bounds.height); } window.validate(); } /** @return A {@link Frame} to use when a valid frame of any sort is all that is needed. */ public static Frame getAnyFrame() { Frame frame = BaseWindow.getTopWindow(); if (frame == null) { Frame[] frames = Frame.getFrames(); for (Frame element : frames) { if (element.isDisplayable()) { return element; } } return getHiddenFrame(true); } return frame; } /** Attempts to force the app to the front. */ public static void forceAppToFront() { // Calling Desktop.isDesktopSupported() generally doesn't have the desired effect on Windows boolean force = Platform.isWindows(); if (Desktop.isDesktopSupported()) { try { Desktop.getDesktop().requestForeground(true); } catch (UnsupportedOperationException uoex) { force = true; } } if (force) { BaseWindow topWindow = BaseWindow.getTopWindow(); if (topWindow != null) { if (!topWindow.isVisible()) { topWindow.setVisible(true); } boolean alwaysOnTop = topWindow.isAlwaysOnTop(); topWindow.setExtendedState(Frame.NORMAL); topWindow.toFront(); topWindow.setAlwaysOnTop(true); try { Point savedMouse = MouseInfo.getPointerInfo().getLocation(); Robot robot = new Robot(); robot.mouseMove(topWindow.getX() + 100, topWindow.getY() + 10); robot.mousePress(InputEvent.BUTTON1_DOWN_MASK); robot.mouseRelease(InputEvent.BUTTON1_DOWN_MASK); robot.mouseMove(savedMouse.x, savedMouse.y); } catch (Exception ex) { Log.warn(ex); } finally { topWindow.setAlwaysOnTop(alwaysOnTop); } } } } /** * @param create Whether it should be created if it doesn't already exist. * @return The single instance of a special, hidden window that can be used for various * operations that require a window before you actually have one available. */ public static Frame getHiddenFrame(boolean create) { if (HIDDEN_FRAME == null && create) { HIDDEN_FRAME = new Frame(); HIDDEN_FRAME.setUndecorated(true); HIDDEN_FRAME.setBounds(0, 0, 0, 0); HIDDEN_FRAME.setIconImages(Images.APP_ICON_LIST); } return HIDDEN_FRAME; } /** Forces a full repaint of all windows, disposing of any window buffers. */ public static void forceRepaint() { for (BaseWindow window : BaseWindow.getAllAppWindows()) { window.repaint(); } } }