// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.gui.util; import static org.openstreetmap.josm.tools.I18n.tr; import java.awt.Color; import java.awt.Component; import java.awt.Container; import java.awt.Dialog; import java.awt.Dimension; import java.awt.DisplayMode; import java.awt.Font; import java.awt.Frame; import java.awt.GraphicsDevice; import java.awt.GraphicsEnvironment; import java.awt.GridBagLayout; import java.awt.HeadlessException; import java.awt.Image; import java.awt.Stroke; import java.awt.Toolkit; import java.awt.Window; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.image.FilteredImageSource; import java.lang.reflect.InvocationTargetException; import java.util.Enumeration; import java.util.EventObject; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; import javax.swing.GrayFilter; import javax.swing.Icon; import javax.swing.ImageIcon; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JScrollPane; import javax.swing.Scrollable; import javax.swing.SwingUtilities; import javax.swing.Timer; import javax.swing.ToolTipManager; import javax.swing.UIManager; import javax.swing.plaf.FontUIResource; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.data.preferences.StrokeProperty; import org.openstreetmap.josm.gui.ExtendedDialog; import org.openstreetmap.josm.gui.widgets.HtmlPanel; import org.openstreetmap.josm.tools.CheckParameterUtil; import org.openstreetmap.josm.tools.ColorHelper; import org.openstreetmap.josm.tools.GBC; import org.openstreetmap.josm.tools.ImageOverlay; import org.openstreetmap.josm.tools.ImageProvider; import org.openstreetmap.josm.tools.ImageProvider.ImageSizes; import org.openstreetmap.josm.tools.LanguageInfo; import org.openstreetmap.josm.tools.bugreport.BugReport; import org.openstreetmap.josm.tools.bugreport.ReportedException; /** * basic gui utils */ public final class GuiHelper { private GuiHelper() { // Hide default constructor for utils classes } /** * disable / enable a component and all its child components * @param root component * @param enabled enabled state */ public static void setEnabledRec(Container root, boolean enabled) { root.setEnabled(enabled); Component[] children = root.getComponents(); for (Component child : children) { if (child instanceof Container) { setEnabledRec((Container) child, enabled); } else { child.setEnabled(enabled); } } } public static void executeByMainWorkerInEDT(final Runnable task) { Main.worker.submit(() -> runInEDTAndWait(task)); } /** * Executes asynchronously a runnable in * <a href="http://docs.oracle.com/javase/tutorial/uiswing/concurrency/dispatch.html">Event Dispatch Thread</a>. * @param task The runnable to execute * @see SwingUtilities#invokeLater */ public static void runInEDT(Runnable task) { if (SwingUtilities.isEventDispatchThread()) { task.run(); } else { SwingUtilities.invokeLater(task); } } /** * Executes synchronously a runnable in * <a href="http://docs.oracle.com/javase/tutorial/uiswing/concurrency/dispatch.html">Event Dispatch Thread</a>. * @param task The runnable to execute * @see SwingUtilities#invokeAndWait */ public static void runInEDTAndWait(Runnable task) { if (SwingUtilities.isEventDispatchThread()) { task.run(); } else { try { SwingUtilities.invokeAndWait(task); } catch (InterruptedException | InvocationTargetException e) { Main.error(e); } } } /** * Executes synchronously a runnable in * <a href="http://docs.oracle.com/javase/tutorial/uiswing/concurrency/dispatch.html">Event Dispatch Thread</a>. * <p> * Passes on the exception that was thrown to the thread calling this. * The exception is wrapped using a {@link ReportedException}. * @param task The runnable to execute * @see SwingUtilities#invokeAndWait * @since 10271 */ public static void runInEDTAndWaitWithException(Runnable task) { if (SwingUtilities.isEventDispatchThread()) { task.run(); } else { try { SwingUtilities.invokeAndWait(task); } catch (InterruptedException | InvocationTargetException e) { throw BugReport.intercept(e).put("task", task); } } } /** * Executes synchronously a callable in * <a href="http://docs.oracle.com/javase/tutorial/uiswing/concurrency/dispatch.html">Event Dispatch Thread</a> * and return a value. * @param <V> the result type of method <tt>call</tt> * @param callable The callable to execute * @return The computed result * @since 7204 */ public static <V> V runInEDTAndWaitAndReturn(Callable<V> callable) { if (SwingUtilities.isEventDispatchThread()) { try { return callable.call(); } catch (Exception e) { // NOPMD Main.error(e); return null; } } else { FutureTask<V> task = new FutureTask<>(callable); SwingUtilities.invokeLater(task); try { return task.get(); } catch (InterruptedException | ExecutionException e) { Main.error(e); return null; } } } /** * This function fails if it was not called from the EDT thread. * @throws IllegalStateException if called from wrong thread. * @since 10271 */ public static void assertCallFromEdt() { if (!SwingUtilities.isEventDispatchThread()) { throw new IllegalStateException( "Needs to be called from the EDT thread, not from " + Thread.currentThread().getName()); } } /** * Warns user about a dangerous action requiring confirmation. * @param title Title of dialog * @param content Content of dialog * @param baseActionIcon Unused? FIXME why is this parameter unused? * @param continueToolTip Tooltip to display for "continue" button * @return true if the user wants to cancel, false if they want to continue */ public static boolean warnUser(String title, String content, ImageIcon baseActionIcon, String continueToolTip) { ExtendedDialog dlg = new ExtendedDialog(Main.parent, title, new String[] {tr("Cancel"), tr("Continue")}); dlg.setContent(content); dlg.setButtonIcons(new Icon[] { new ImageProvider("cancel").setMaxSize(ImageSizes.LARGEICON).get(), new ImageProvider("upload").setMaxSize(ImageSizes.LARGEICON).addOverlay( new ImageOverlay(new ImageProvider("warning-small"), 0.5, 0.5, 1.0, 1.0)).get()}); dlg.setToolTipTexts(new String[] { tr("Cancel"), continueToolTip}); dlg.setIcon(JOptionPane.WARNING_MESSAGE); dlg.setCancelButton(1); return dlg.showDialog().getValue() != 2; } /** * Notifies user about an error received from an external source as an HTML page. * @param parent Parent component * @param title Title of dialog * @param message Message displayed at the top of the dialog * @param html HTML content to display (real error message) * @since 7312 */ public static void notifyUserHtmlError(Component parent, String title, String message, String html) { JPanel p = new JPanel(new GridBagLayout()); p.add(new JLabel(message), GBC.eol()); p.add(new JLabel(tr("Received error page:")), GBC.eol()); JScrollPane sp = embedInVerticalScrollPane(new HtmlPanel(html)); sp.setPreferredSize(new Dimension(640, 240)); p.add(sp, GBC.eol().fill(GBC.BOTH)); ExtendedDialog ed = new ExtendedDialog(parent, title, new String[] {tr("OK")}); ed.setButtonIcons(new String[] {"ok.png"}); ed.setContent(p); ed.showDialog(); } /** * Replies the disabled (grayed) version of the specified image. * @param image The image to disable * @return The disabled (grayed) version of the specified image, brightened by 20%. * @since 5484 */ public static Image getDisabledImage(Image image) { return Toolkit.getDefaultToolkit().createImage( new FilteredImageSource(image.getSource(), new GrayFilter(true, 20))); } /** * Replies the disabled (grayed) version of the specified icon. * @param icon The icon to disable * @return The disabled (grayed) version of the specified icon, brightened by 20%. * @since 5484 */ public static ImageIcon getDisabledIcon(ImageIcon icon) { return new ImageIcon(getDisabledImage(icon.getImage())); } /** * Attaches a {@code HierarchyListener} to the specified {@code Component} that * will set its parent dialog resizeable. Use it before a call to JOptionPane#showXXXXDialog * to make it resizeable. * @param pane The component that will be displayed * @param minDimension The minimum dimension that will be set for the dialog. Ignored if null * @return {@code pane} * @since 5493 */ public static Component prepareResizeableOptionPane(final Component pane, final Dimension minDimension) { if (pane != null) { pane.addHierarchyListener(e -> { Window window = SwingUtilities.getWindowAncestor(pane); if (window instanceof Dialog) { Dialog dialog = (Dialog) window; if (!dialog.isResizable()) { dialog.setResizable(true); if (minDimension != null) { dialog.setMinimumSize(minDimension); } } } }); } return pane; } /** * Schedules a new Timer to be run in the future (once or several times). * @param initialDelay milliseconds for the initial and between-event delay if repeatable * @param actionListener an initial listener; can be null * @param repeats specify false to make the timer stop after sending its first action event * @return The (started) timer. * @since 5735 */ public static Timer scheduleTimer(int initialDelay, ActionListener actionListener, boolean repeats) { Timer timer = new Timer(initialDelay, actionListener); timer.setRepeats(repeats); timer.start(); return timer; } /** * Return s new BasicStroke object with given thickness and style * @param code = 3.5 -> thickness=3.5px; 3.5 10 5 -> thickness=3.5px, dashed: 10px filled + 5px empty * @return stroke for drawing * @see StrokeProperty */ public static Stroke getCustomizedStroke(String code) { return StrokeProperty.getFromString(code); } /** * Gets the font used to display monospaced text in a component, if possible. * @param component The component * @return the font used to display monospaced text in a component, if possible * @since 7896 */ public static Font getMonospacedFont(JComponent component) { // Special font for Khmer script if ("km".equals(LanguageInfo.getJOSMLocaleCode())) { return component.getFont(); } else { return new Font("Monospaced", component.getFont().getStyle(), component.getFont().getSize()); } } /** * Gets the font used to display JOSM title in about dialog and splash screen. * @return title font * @since 5797 */ public static Font getTitleFont() { return new Font("SansSerif", Font.BOLD, 23); } /** * Embeds the given component into a new vertical-only scrollable {@code JScrollPane}. * @param panel The component to embed * @return the vertical scrollable {@code JScrollPane} * @since 6666 */ public static JScrollPane embedInVerticalScrollPane(Component panel) { return new JScrollPane(panel, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); } /** * Set the default unit increment for a {@code JScrollPane}. * * This fixes slow mouse wheel scrolling when the content of the {@code JScrollPane} * is a {@code JPanel} or other component that does not implement the {@link Scrollable} * interface. * The default unit increment is 1 pixel. Multiplied by the number of unit increments * per mouse wheel "click" (platform dependent, usually 3), this makes a very * sluggish mouse wheel experience. * This methods sets the unit increment to a larger, more reasonable value. * @param sp the scroll pane * @return the scroll pane (same object) with fixed unit increment * @throws IllegalArgumentException if the component inside of the scroll pane * implements the {@code Scrollable} interface ({@code JTree}, {@code JLayer}, * {@code JList}, {@code JTextComponent} and {@code JTable}) */ public static JScrollPane setDefaultIncrement(JScrollPane sp) { if (sp.getViewport().getView() instanceof Scrollable) { throw new IllegalArgumentException(); } sp.getVerticalScrollBar().setUnitIncrement(10); sp.getHorizontalScrollBar().setUnitIncrement(10); return sp; } /** * Returns extended modifier key used as the appropriate accelerator key for menu shortcuts. * It is advised everywhere to use {@link Toolkit#getMenuShortcutKeyMask()} to get the cross-platform modifier, but: * <ul> * <li>it returns KeyEvent.CTRL_MASK instead of KeyEvent.CTRL_DOWN_MASK. We used the extended * modifier for years, and Oracle recommends to use it instead, so it's best to keep it</li> * <li>the method throws a HeadlessException ! So we would need to handle it for unit tests anyway</li> * </ul> * @return extended modifier key used as the appropriate accelerator key for menu shortcuts * @since 7539 */ public static int getMenuShortcutKeyMaskEx() { return Main.isPlatformOsx() ? KeyEvent.META_DOWN_MASK : KeyEvent.CTRL_DOWN_MASK; } /** * Sets a global font for all UI, replacing default font of current look and feel. * @param name Font name. It is up to the caller to make sure the font exists * @throws IllegalArgumentException if name is null * @since 7896 */ public static void setUIFont(String name) { CheckParameterUtil.ensureParameterNotNull(name, "name"); Main.info("Setting "+name+" as the default UI font"); Enumeration<?> keys = UIManager.getDefaults().keys(); while (keys.hasMoreElements()) { Object key = keys.nextElement(); Object value = UIManager.get(key); if (value instanceof FontUIResource) { FontUIResource fui = (FontUIResource) value; UIManager.put(key, new FontUIResource(name, fui.getStyle(), fui.getSize())); } } } /** * Sets the background color for this component, and adjust the foreground color so the text remains readable. * @param c component * @param background background color * @since 9223 */ public static void setBackgroundReadable(JComponent c, Color background) { c.setBackground(background); c.setForeground(ColorHelper.getForegroundColor(background)); } /** * Gets the size of the screen. On systems with multiple displays, the primary display is used. * This method returns always 800x600 in headless mode (useful for unit tests). * @return the size of this toolkit's screen, in pixels, or 800x600 * @see Toolkit#getScreenSize * @since 9576 */ public static Dimension getScreenSize() { return GraphicsEnvironment.isHeadless() ? new Dimension(800, 600) : Toolkit.getDefaultToolkit().getScreenSize(); } /** * Gets the size of the screen. On systems with multiple displays, * contrary to {@link #getScreenSize()}, the biggest display is used. * This method returns always 800x600 in headless mode (useful for unit tests). * @return the size of maximum screen, in pixels, or 800x600 * @see Toolkit#getScreenSize * @since 10470 */ public static Dimension getMaximumScreenSize() { if (GraphicsEnvironment.isHeadless()) { return new Dimension(800, 600); } int height = 0; int width = 0; for (GraphicsDevice gd: GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices()) { DisplayMode dm = gd.getDisplayMode(); height = Math.max(height, dm.getHeight()); width = Math.max(width, dm.getWidth()); } if (height == 0 || width == 0) { return new Dimension(800, 600); } return new Dimension(width, height); } /** * Returns the first <code>Window</code> ancestor of event source, or * {@code null} if event source is not a component contained inside a <code>Window</code>. * @param e event object * @return a Window, or {@code null} * @since 9916 */ public static Window getWindowAncestorFor(EventObject e) { if (e != null) { Object source = e.getSource(); if (source instanceof Component) { Window ancestor = SwingUtilities.getWindowAncestor((Component) source); if (ancestor != null) { return ancestor; } else { Container parent = ((Component) source).getParent(); if (parent instanceof JPopupMenu) { Component invoker = ((JPopupMenu) parent).getInvoker(); return SwingUtilities.getWindowAncestor(invoker); } } } } return null; } /** * Extends tooltip dismiss delay to a default value of 1 minute for the given component. * @param c component * @since 10024 */ public static void extendTooltipDelay(Component c) { extendTooltipDelay(c, 60_000); } /** * Extends tooltip dismiss delay to the specified value for the given component. * @param c component * @param delay tooltip dismiss delay in milliseconds * @see <a href="http://stackoverflow.com/a/6517902/2257172">http://stackoverflow.com/a/6517902/2257172</a> * @since 10024 */ public static void extendTooltipDelay(Component c, final int delay) { final int defaultDismissTimeout = ToolTipManager.sharedInstance().getDismissDelay(); c.addMouseListener(new MouseAdapter() { @Override public void mouseEntered(MouseEvent me) { ToolTipManager.sharedInstance().setDismissDelay(delay); } @Override public void mouseExited(MouseEvent me) { ToolTipManager.sharedInstance().setDismissDelay(defaultDismissTimeout); } }); } /** * Returns the specified component's <code>Frame</code> without throwing exception in headless mode. * * @param parentComponent the <code>Component</code> to check for a <code>Frame</code> * @return the <code>Frame</code> that contains the component, or <code>getRootFrame</code> * if the component is <code>null</code>, or does not have a valid <code>Frame</code> parent * @see JOptionPane#getFrameForComponent * @see GraphicsEnvironment#isHeadless * @since 10035 */ public static Frame getFrameForComponent(Component parentComponent) { try { return JOptionPane.getFrameForComponent(parentComponent); } catch (HeadlessException e) { Main.debug(e); return null; } } }