package org.openslx.dozmod.gui; import java.awt.AWTEvent; import java.awt.Component; import java.awt.Dimension; import java.awt.Font; import java.awt.Frame; import java.awt.GraphicsDevice; import java.awt.GraphicsEnvironment; import java.awt.Insets; import java.awt.KeyboardFocusManager; import java.awt.Point; import java.awt.Rectangle; import java.awt.Toolkit; import java.awt.Window; import java.awt.event.AWTEventListener; import java.lang.reflect.InvocationTargetException; import java.net.URISyntaxException; import java.util.concurrent.atomic.AtomicReference; import javax.management.monitor.Monitor; import javax.swing.Icon; import javax.swing.JEditorPane; import javax.swing.JFrame; import javax.swing.JOptionPane; import javax.swing.SwingUtilities; import javax.swing.UIManager; import javax.swing.event.HyperlinkEvent; import javax.swing.event.HyperlinkListener; import javax.swing.text.html.HTMLDocument; import org.apache.log4j.Logger; import org.openslx.dozmod.Config; import org.openslx.dozmod.gui.helper.MessageType; import org.openslx.dozmod.util.DesktopEnvironment; import org.openslx.dozmod.util.ResourceLoader; import org.openslx.util.QuickTimer; public class Gui { private static final Logger LOGGER = Logger.getLogger(Gui.class); private static long lastUserActivity = System.currentTimeMillis(); static { Toolkit.getDefaultToolkit().addAWTEventListener(new AWTEventListener() { @Override public void eventDispatched(AWTEvent event) { lastUserActivity = System.currentTimeMillis(); } }, AWTEvent.MOUSE_EVENT_MASK | AWTEvent.KEY_EVENT_MASK); } private static Rectangle clientArea(GraphicsDevice gd) { Insets inset = Toolkit.getDefaultToolkit().getScreenInsets(gd.getDefaultConfiguration()); Rectangle bounds = gd.getDefaultConfiguration().getBounds(); bounds.x += inset.left; bounds.y += inset.top; bounds.width -= (inset.top + inset.bottom); bounds.height -= (inset.left + inset.right); return bounds; } /** * Center the given shell on the {@link Monitor} it is displayed on. * WARNING: Seems broken on Linux (depending on DE or WM) * * @param shell Some shell */ public static void centerShell(Window shell) { GraphicsDevice activeMonitor = getMonitorFromRectangle(shell.getBounds(), true); Rectangle bounds = clientArea(activeMonitor); Rectangle rect = shell.getBounds(); int x = bounds.x + (bounds.width - rect.width) / 2; int y = bounds.y + (bounds.height - rect.height) / 2; shell.setLocation(x, y); } /** * Take a shell and center it relative to another shell * * @param parent Parent shell, used as reference * @param shellToCenter The shell that should be repositioned */ public static void centerShellOverShell(Window parent, Window shellToCenter) { Rectangle bounds = parent.getBounds(); Rectangle rect = shellToCenter.getBounds(); int x = bounds.x + (bounds.width - rect.width) / 2; int y = bounds.y + (bounds.height - rect.height) / 2; if (x < bounds.x) x = bounds.x; if (y < bounds.y) y = bounds.y; shellToCenter.setLocation(x, y); } /** * Make sure the given shell fits the {@link GraphicsDevice} it is displayed * on. * * @param shell Some shell */ public static void limitShellSize(JFrame window) { GraphicsDevice activeMonitor = getMonitorFromRectangle(window.getBounds(), true); Rectangle bounds = clientArea(activeMonitor); Rectangle rect = window.getBounds(); boolean changed = false; if (rect.width + 20 > bounds.width) { rect.width = bounds.width - 20; changed = true; } if (rect.height + 20 > bounds.height) { rect.height = bounds.height - 20; changed = true; } if (changed) { window.setSize(rect.width, rect.height); rect = window.getBounds(); } changed = false; if (rect.x + rect.width >= bounds.x + bounds.width) { rect.x = 5; changed = true; } if (rect.y + rect.height >= bounds.y + bounds.height) { rect.y = 5; changed = true; } if (changed) { window.setLocation(rect.x, rect.y); } } /** * Gets the given dimension scaled to the saved scaling factor * * @param width starting width to scale * @param height starting height to scale * @return scaled dimension */ public static Dimension getScaledDimension(int width, int height) { int scale = Config.getFontScaling(); return new Dimension(width * scale / 100, height * scale / 100); } /** * Load given icon resource, optionally scaling it while taking * the user's zoom factor into account. * * @param path resource path * @param description resource description * @param maxHeight maximum height of image, which it will be scaled to * @param context context component, for alpha blending * @return scaled icon, if too large, otherwise the unmodified icon */ public static Icon getScaledIconResource(String path, String description, int maxHeight, Component context) { int height = maxHeight * Config.getFontScaling() / 100; return ResourceLoader.getIcon(path, description, height, context); } /** * Get the {@link GraphicsDevice} which the given {@link Point} lies in. * * @param point The point in question * @param defaultToPrimary if no monitor matches the check, return the * primary monitor if true, null otherwise * @return the {@link GraphicsDevice} */ private static GraphicsDevice getMonitorFromPoint(GraphicsDevice[] screens, Point point, boolean defaultToPrimary) { LOGGER.debug("Finding monitor for point " + point.toString()); for (GraphicsDevice dev : screens) { Rectangle bounds = dev.getDefaultConfiguration().getBounds(); LOGGER.debug("Checking monitor " + bounds.toString()); if (bounds.contains(point)) return dev; } if (defaultToPrimary) { LOGGER.debug("Defaulting to primary..."); return GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice(); } return null; } /** * Get the {@link GraphicsDevice} which most of the given rectangle * overlaps. * * @param rect The rectangle to check * @param defaultToPrimary if no monitor matches the check, return the * primary monitor if true, null otherwise * @return the {@link GraphicsDevice} */ public static GraphicsDevice getMonitorFromRectangle(Rectangle rect, boolean defaultToPrimary) { // Make sure rectangle is in bounds. This is not completely accurate // in case there are multiple monitors that have different resolutions. GraphicsDevice[] screens = GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices(); Rectangle bounds = new Rectangle(); for (GraphicsDevice dev : screens) { bounds = bounds.union(dev.getDefaultConfiguration().getBounds()); } LOGGER.debug("Display bounds are " + bounds.toString() + ", rect is " + rect.toString()); if (rect.x + rect.width >= bounds.x + bounds.width) { rect.width -= (rect.x + rect.width) - (bounds.x + bounds.width); if (rect.width < 1) rect.width = 1; } if (rect.y + rect.height >= bounds.y + bounds.height) { rect.height -= (rect.y + rect.height) - (bounds.y + bounds.height); if (rect.height < 1) rect.height = 1; } if (rect.x < bounds.x) { rect.width -= bounds.x - rect.x; rect.x = bounds.x; } if (rect.y < bounds.y) { rect.height -= bounds.y - rect.y; rect.y = bounds.y; } LOGGER.debug("After correction: " + rect.toString()); // Now just use the same code as *FromPoint by using the rectangle's center return getMonitorFromPoint(screens, new Point(rect.x + rect.width / 2, rect.y + rect.height / 2), defaultToPrimary); } /** * Run given task in the GUI thread, blocking the calling thread until the * task is done. * * @param task Task to run * @return return value of the task */ public static T syncExec(final GuiCallable task) { final AtomicReference instance = new AtomicReference<>(); final AtomicReference thrown = new AtomicReference<>(); if (SwingUtilities.isEventDispatchThread()) { return task.run(); } try { SwingUtilities.invokeAndWait(new Runnable() { @Override public void run() { try { instance.set(task.run()); } catch (Throwable e) { thrown.set(e); } } }); } catch (InvocationTargetException | InterruptedException e) { LOGGER.warn("syncExec() failed", e); return null; } if (thrown.get() != null) { throw new RuntimeException("task passed to syncExec() failed", thrown.get()); } return instance.get(); } /** * Run given task as soon as possible in the GUI thread, but don't block * calling thread. * * @param task Task to run */ public static void asyncExec(final Runnable task) { SwingUtilities.invokeLater(task); } /** * Pretty much the same as Callable, but no exceptions are allowed. * * @param return value */ public static interface GuiCallable { T run(); } /** * Exit application - dispose all shells, so mainloop will terminate. * * @param code */ public static void exit(int code) { QuickTimer.cancel(); Window[] ownerlessWindows = Frame.getOwnerlessWindows(); for (Window w : ownerlessWindows) { w.dispose(); } System.exit(code); } /** * Generic helper to show a message box to the user, and optionally log the * message to the log file. * * @param parent parent window (used for positioning/modality). If null, * the active window will be used * @param message Message to display. Can be multi line. * @param messageType Type of message (warning, information) * @param logger Logger instance to log to. Can be null. * @param exception Exception related to this message. Can be null. * @return true if OK or YES was clicked, false for CANCEL/NO/(X) */ public static boolean showMessageBox(Component parent, String message, MessageType messageType, Logger logger, Throwable exception) { if (logger != null) logger.log(messageType.logPriority, message, exception); // Only needs to be done, if parent isn't already a window if (parent == null) { parent = KeyboardFocusManager.getCurrentKeyboardFocusManager().getActiveWindow(); } else if (!(parent instanceof Window)) { Window ancestor = SwingUtilities.getWindowAncestor(parent); if (ancestor != null) { parent = ancestor; } } if (exception != null) { message += "\n\n" + exception.getClass().getSimpleName() + "\n" + exception.getMessage() + "\n" + " (Für Stack-Trace siehe Logdatei)"; } if (message.startsWith("")) { JEditorPane ep = new JEditorPane("text/html", message); ep.setEditable(false); ep.setOpaque(false); Font font = UIManager.getFont("Label.font"); String bodyRule = "body { font-family: " + font.getFamily() + "; " + "font-size: " + font.getSize() + "pt; }"; ((HTMLDocument)ep.getDocument()).getStyleSheet().addRule(bodyRule); ep.addHyperlinkListener(new HyperlinkListener() { @Override public void hyperlinkUpdate(HyperlinkEvent e) { if (e.getEventType().equals(HyperlinkEvent.EventType.ACTIVATED)) { try { DesktopEnvironment.openWebpageUri(e.getURL().toURI()); } catch (URISyntaxException ex) { LOGGER.error("Couldn't parse hyperlink", ex); } } } }); JOptionPane.showMessageDialog(parent, ep, messageType.title, messageType.optionPaneId); return true; } if (messageType.buttons == -1) { JOptionPane.showMessageDialog(parent, message, messageType.title, messageType.optionPaneId); return true; } // TODO set the default button that has focus int ret = JOptionPane.showConfirmDialog(parent, message, messageType.title, messageType.buttons, messageType.optionPaneId); return ret == JOptionPane.OK_OPTION || ret == JOptionPane.YES_OPTION; } /** * Generic helper to show a message box to the user, and optionally log the * message to the log file. * * @param message Message to display. Can be multi line. * @param messageType Type of message (warning, information) * @param logger Logger instance to log to. Can be null. * @param exception Exception related to this message. Can be null. * @return true if OK or YES was clicked, false for CANCEL/NO/(X) */ public static boolean showMessageBox(String message, MessageType messageType, Logger logger, Throwable exception) { return showMessageBox(null, message, messageType, logger, exception); } /** * Show a message box to the user asynchronously, and optionally log the * message to the log file. This is most useful when working from another * thread. * * @param message Message to display. Can be multi line. * @param messageType Type of message (warning, information) * @param logger Logger instance to log to. Can be null. * @param exception Exception related to this message. Can be null. * @return true if OK or YES was clicked, false for CANCEL/NO/(X) */ public static void asyncMessageBox(final String message, final MessageType messageType, final Logger logger, final Throwable exception) { if (SwingUtilities.isEventDispatchThread()) { Gui.showMessageBox(message, messageType, logger, exception); return; } SwingUtilities.invokeLater(new Runnable() { @Override public void run() { showMessageBox(null, message, messageType, logger, exception); } }); } /** * Get last user activity timestamp. * This considers mouse clicks and key presses. */ public static long getLastUserActivityMillis() { return lastUserActivity; } }