/* * 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; import com.trollworks.gcs.ui.image.Img; import com.trollworks.gcs.ui.widget.IconButton; import com.trollworks.gcs.utility.Log; import com.trollworks.gcs.utility.Platform; import java.awt.Component; import java.awt.Container; import java.awt.Dialog; import java.awt.Dialog.ModalityType; import java.awt.Dimension; import java.awt.Graphics2D; import java.awt.Insets; import java.awt.Point; import java.awt.Rectangle; import java.awt.Transparency; import java.awt.Window; import java.awt.dnd.DropTargetDragEvent; import java.awt.dnd.DropTargetDropEvent; import java.awt.event.MouseEvent; import java.awt.event.MouseWheelEvent; import javax.swing.AbstractButton; import javax.swing.JComboBox; import javax.swing.JComponent; import javax.swing.JTabbedPane; import javax.swing.JTextField; import javax.swing.JViewport; import javax.swing.RepaintManager; /** Various utility methods for the UI. */ public class UIUtilities { /** * Selects the tab with the specified title. * * @param pane The {@link JTabbedPane} to use. * @param title The title to select. */ public static void selectTab(JTabbedPane pane, String title) { int count = pane.getTabCount(); for (int i = 0; i < count; i++) { if (pane.getTitleAt(i).equals(title)) { pane.setSelectedIndex(i); break; } } } /** * Disables all controls in the specified component and all its children. * * @param comp The {@link Component} to work on. */ public static void disableControls(Component comp) { if (comp instanceof Container) { Container container = (Container) comp; int count = container.getComponentCount(); for (int i = 0; i < count; i++) { disableControls(container.getComponent(i)); } } if (comp instanceof AbstractButton || comp instanceof JComboBox || comp instanceof JTextField || comp instanceof IconButton) { comp.setEnabled(false); } } /** * Sets a {@link Component}'s min, max & preferred sizes to a specific size. * * @param comp The {@link Component} to work on. * @param size The size to set the component to. */ public static void setOnlySize(Component comp, Dimension size) { comp.setMinimumSize(size); comp.setMaximumSize(size); comp.setPreferredSize(size); } /** * Sets a {@link Component}'s min & max sizes to its preferred size. Note that this applies a * fudge factor to the width on the Windows platform to attempt to get around highly inaccurate * font measurements. * * @param comp The {@link Component} to work on. */ public static void setToPreferredSizeOnly(Component comp) { Dimension size = comp.getPreferredSize(); if (Platform.isWindows()) { size.width += 4; // Fudge the width on Windows, since its text measurement seems to be // off in many cases } setOnlySize(comp, size); } /** @param comps The {@link Component}s to set to the same size. */ public static void adjustToSameSize(Component... comps) { Dimension best = new Dimension(); for (Component comp : comps) { Dimension size = comp.getPreferredSize(); if (size.width > best.width) { best.width = size.width; } if (size.height > best.height) { best.height = size.height; } } for (Component comp : comps) { setOnlySize(comp, best); } } /** * Converts a {@link Point} from one component's coordinate system to another's. * * @param pt The point to convert. * @param from The component the point originated in. * @param to The component the point should be translated to. */ public static void convertPoint(Point pt, Component from, Component to) { convertPointToScreen(pt, from); convertPointFromScreen(pt, to); } /** * Converts a {@link Point} from on the screen to a position within the component. * * @param pt The point to convert. * @param component The component the point should be translated to. */ public static void convertPointFromScreen(Point pt, Component component) { while (component != null) { pt.x -= component.getX(); pt.y -= component.getY(); if (component instanceof Window) { break; } component = component.getParent(); } } /** * Converts a {@link Point} in a component to its position on the screen. * * @param pt The point to convert. * @param component The component the point originated in. */ public static void convertPointToScreen(Point pt, Component component) { while (component != null) { pt.x += component.getX(); pt.y += component.getY(); if (component instanceof Window) { break; } component = component.getParent(); } } /** * Converts a {@link Rectangle} from one component's coordinate system to another's. * * @param bounds The rectangle to convert. * @param from The component the rectangle originated in. * @param to The component the rectangle should be translated to. */ public static void convertRectangle(Rectangle bounds, Component from, Component to) { convertRectangleToScreen(bounds, from); convertRectangleFromScreen(bounds, to); } /** * Converts a {@link Rectangle} from on the screen to a position within the component. * * @param bounds The rectangle to convert. * @param component The component the rectangle should be translated to. */ public static void convertRectangleFromScreen(Rectangle bounds, Component component) { while (component != null) { bounds.x -= component.getX(); bounds.y -= component.getY(); if (component instanceof Window) { break; } component = component.getParent(); } } /** * Converts a {@link Rectangle} in a component to its position on the screen. * * @param bounds The rectangle to convert. * @param component The component the rectangle originated in. */ public static void convertRectangleToScreen(Rectangle bounds, Component component) { while (component != null) { bounds.x += component.getX(); bounds.y += component.getY(); if (component instanceof Window) { break; } component = component.getParent(); } } /** * @param parent The parent {@link Container}. * @param child The child {@link Component}. * @return The index of the specified {@link Component}. -1 will be returned if the {@link * Component} isn't a direct child. */ public static int getIndexOf(Container parent, Component child) { if (parent != null) { int count = parent.getComponentCount(); for (int i = 0; i < count; i++) { if (child == parent.getComponent(i)) { return i; } } } return -1; } /** * Clones a {@link MouseEvent}. * * @param event The event to clone. * @return The new {@link MouseEvent}. */ public static final MouseEvent cloneMouseEvent(MouseEvent event) { if (event instanceof MouseWheelEvent) { MouseWheelEvent old = (MouseWheelEvent) event; return new MouseWheelEvent((Component) old.getSource(), old.getID(), System.currentTimeMillis(), old.getModifiersEx(), old.getX(), old.getY(), old.getClickCount(), old.isPopupTrigger(), old.getScrollType(), old.getScrollAmount(), old.getWheelRotation()); } return new MouseEvent((Component) event.getSource(), event.getID(), System.currentTimeMillis(), event.getModifiersEx(), event.getX(), event.getY(), event.getClickCount(), event.isPopupTrigger()); } /** * Clones a {@link MouseEvent}. * * @param event The event to clone. * @param refreshTime Pass in {@code true} to generate a new time stamp. * @return The new {@link MouseEvent}. */ public static final MouseEvent cloneMouseEvent(MouseEvent event, boolean refreshTime) { if (event instanceof MouseWheelEvent) { MouseWheelEvent old = (MouseWheelEvent) event; return new MouseWheelEvent((Component) old.getSource(), old.getID(), refreshTime ? System.currentTimeMillis() : event.getWhen(), old.getModifiersEx(), old.getX(), old.getY(), old.getClickCount(), old.isPopupTrigger(), old.getScrollType(), old.getScrollAmount(), old.getWheelRotation()); } return new MouseEvent((Component) event.getSource(), event.getID(), refreshTime ? System.currentTimeMillis() : event.getWhen(), event.getModifiersEx(), event.getX(), event.getY(), event.getClickCount(), event.isPopupTrigger()); } /** * Clones a {@link MouseEvent}. * * @param event The event to clone. * @param source Pass in a new source. * @param where Pass in a new location. * @param refreshTime Pass in {@code true} to generate a new time stamp. * @return The new {@link MouseEvent}. */ public static final MouseEvent cloneMouseEvent(MouseEvent event, Component source, Point where, boolean refreshTime) { if (event instanceof MouseWheelEvent) { MouseWheelEvent old = (MouseWheelEvent) event; return new MouseWheelEvent(source, old.getID(), refreshTime ? System.currentTimeMillis() : event.getWhen(), old.getModifiersEx(), where.x, where.y, old.getClickCount(), old.isPopupTrigger(), old.getScrollType(), old.getScrollAmount(), old.getWheelRotation()); } return new MouseEvent(source, event.getID(), refreshTime ? System.currentTimeMillis() : event.getWhen(), event.getModifiersEx(), where.x, where.y, event.getClickCount(), event.isPopupTrigger()); } /** * Forwards a {@link MouseEvent} from one component to another. * * @param event The event to forward. * @param from The component that originally received the event. * @param to The component the event should be forwarded to. */ public static void forwardMouseEvent(MouseEvent event, Component from, Component to) { translateMouseEvent(event, from, to); to.dispatchEvent(event); } /** * Translates a {@link MouseEvent} from one component to another. * * @param event The event that will be forwarded. * @param from The component that originally received the event. * @param to The component the event should be forwarded to. */ public static void translateMouseEvent(MouseEvent event, Component from, Component to) { Point evtPt = event.getPoint(); Point pt = new Point(evtPt); convertPoint(pt, from, to); event.setSource(to); event.translatePoint(pt.x - evtPt.x, pt.y - evtPt.y); } /** * @param comp The component to work with. * @return Whether the component should be expanded to fit. */ public static boolean shouldTrackViewportWidth(Component comp) { Container parent = comp.getParent(); if (parent instanceof JViewport) { Dimension available = parent.getSize(); Dimension prefSize = comp.getPreferredSize(); return prefSize.width < available.width; } return false; } /** * @param comp The component to work with. * @return Whether the component should be expanded to fit. */ public static boolean shouldTrackViewportHeight(Component comp) { Container parent = comp.getParent(); if (parent instanceof JViewport) { Dimension available = parent.getSize(); Dimension prefSize = comp.getPreferredSize(); return prefSize.height < available.height; } return false; } /** @param comp The component to revalidate. */ public static void revalidateImmediately(Component comp) { if (comp != null) { RepaintManager mgr = RepaintManager.currentManager(comp); mgr.validateInvalidComponents(); mgr.paintDirtyRegions(); } } /** * @param component The component to be looked at. * @param type The type of component being looked for. * @return The first object that matches, starting with the component itself and working up * through its parents, or {@code null}. */ @SuppressWarnings("unchecked") public static <T> T getSelfOrAncestorOfType(Component component, Class<T> type) { if (component != null) { if (type.isAssignableFrom(component.getClass())) { return (T) component; } return getAncestorOfType(component, type); } return null; } /** * @param component The component whose ancestor chain is to be looked at. * @param type The type of ancestor being looked for. * @return The ancestor, or {@code null}. */ @SuppressWarnings("unchecked") public static <T> T getAncestorOfType(Component component, Class<T> type) { if (component == null) { return null; } Container parent = component.getParent(); while (parent != null && !type.isAssignableFrom(parent.getClass())) { parent = parent.getParent(); } return (T) parent; } /** * Since JComboBox.getSelectedItem() returns a plain Object, this allows us to get the * appropriate type of object instead. */ public static <E> E getTypedSelectedItemFromCombo(JComboBox<E> combo) { int index = combo.getSelectedIndex(); return index == -1 ? null : combo.getItemAt(index); } /** * @param component The component to generate an image of. * @return The newly created image. */ public static Img getImage(JComponent component) { Img offscreen = null; synchronized (component.getTreeLock()) { Graphics2D gc = null; try { Rectangle bounds = component.getVisibleRect(); offscreen = Img.create(component.getGraphicsConfiguration(), bounds.width, bounds.height, Transparency.TRANSLUCENT); gc = offscreen.getGraphics(); gc.translate(-bounds.x, -bounds.y); component.paint(gc); } catch (Exception exception) { Log.error(exception); } finally { if (gc != null) { gc.dispose(); } } } return offscreen; } /** * @param obj The object to extract a {@link Component} for. * @return The {@link Component} to use for the dialog, or {@code null}. */ public static Component getComponentForDialog(Object obj) { return obj instanceof Component ? (Component) obj : null; } /** @return Whether or not the application is currently in a modal state. */ public static boolean inModalState() { for (Window window : Window.getWindows()) { if (window instanceof Dialog) { Dialog dialog = (Dialog) window; if (dialog.isShowing()) { ModalityType type = dialog.getModalityType(); if (type == ModalityType.APPLICATION_MODAL || type == ModalityType.TOOLKIT_MODAL) { return true; } } } } return false; } public static Point convertDropTargetDragPointTo(DropTargetDragEvent dtde, Component comp) { Point pt = dtde.getLocation(); convertPoint(pt, dtde.getDropTargetContext().getComponent(), comp); return pt; } public static void updateDropTargetDragPointTo(DropTargetDragEvent dtde, Component comp) { convertPoint(dtde.getLocation(), dtde.getDropTargetContext().getComponent(), comp); } public static Point convertDropTargetDropPointTo(DropTargetDropEvent dtde, Component comp) { Point pt = dtde.getLocation(); convertPoint(pt, dtde.getDropTargetContext().getComponent(), comp); return pt; } public static void updateDropTargetDropPointTo(DropTargetDropEvent dtde, Component comp) { convertPoint(dtde.getLocation(), dtde.getDropTargetContext().getComponent(), comp); } /** * @param component The {@link JComponent} to work with. * @return The local, inset, bounds of the specified {@link JComponent}. */ public static Rectangle getLocalInsetBounds(JComponent component) { Insets insets = component.getInsets(); return new Rectangle(insets.left, insets.top, component.getWidth() - (insets.left + insets.right), component.getHeight() - (insets.top + insets.bottom)); } }