/************************************************************************** OmegaT - Computer Assisted Translation (CAT) tool with fuzzy matching, translation memory, keyword search, glossaries, and translation leveraging into updated projects. Copyright (C) 2006 Henry Pijffers 2013 Yu Tang 2014-2015 Aaron Madlon-Kay Home page: http://www.omegat.org/ Support center: http://groups.yahoo.com/group/OmegaT/ This file is part of OmegaT. OmegaT is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. OmegaT is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. **************************************************************************/ package org.omegat.util.gui; import java.awt.Color; import java.awt.Component; import java.awt.FontMetrics; import java.awt.Graphics; import java.awt.GraphicsEnvironment; import java.awt.Image; import java.awt.Rectangle; import java.awt.Toolkit; import java.awt.Window; import java.awt.event.ActionEvent; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.awt.event.FocusAdapter; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.awt.event.KeyEvent; import java.awt.event.MouseWheelEvent; import java.awt.event.WindowEvent; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Optional; import java.util.function.Consumer; import java.util.function.Predicate; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.JComponent; import javax.swing.JDialog; import javax.swing.JFrame; import javax.swing.JRootPane; import javax.swing.KeyStroke; import javax.swing.Timer; import javax.swing.text.Caret; import javax.swing.text.DefaultCaret; import javax.swing.text.JTextComponent; import javax.swing.undo.UndoManager; import org.omegat.util.Platform; import org.omegat.util.Preferences; import org.omegat.util.StringUtil; /** * @author Henry Pijffers * @author Yu-Tang * @author Aaron Madlon-Kay */ public class StaticUIUtils { private static final KeyStroke ESC_KEYSTROKE = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0, false); /** * Make a dialog closeable by pressing the Esc key. * {@link JDialog#dispose()} will be called. * * @param dialog */ public static void setEscapeClosable(JDialog dialog) { setEscapeAction(dialog.getRootPane(), makeCloseAction(dialog)); } /** * Make a dialog closeable by pressing the Esc key. * {@link JFrame#dispose()} will be called. * * @param frame */ public static void setEscapeClosable(JFrame frame) { setEscapeAction(frame.getRootPane(), makeCloseAction(frame)); } @SuppressWarnings("serial") public static Action makeCloseAction(final Window window) { return new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { closeWindowByEvent(window); } }; } /** * Send a {@link WindowEvent#WINDOW_CLOSING} event to the supplied window. * This mimics closing by clicking the window close button. */ public static void closeWindowByEvent(Window window) { window.dispatchEvent(new WindowEvent(window, WindowEvent.WINDOW_CLOSING)); } /** * Associate a custom action to be called when the Esc key is pressed. * * @param dialog * @param action */ public static void setEscapeAction(JDialog dialog, Action action) { setEscapeAction(dialog.getRootPane(), action); } /** * Associate a custom action to be called when the Esc key is pressed. * * @param frame * @param action */ public static void setEscapeAction(JFrame frame, Action action) { setEscapeAction(frame.getRootPane(), action); } /** * Associate a custom action to be called when the Esc key is pressed. * * @param pane * @param action */ public static void setEscapeAction(JRootPane pane, Action action) { // Handle escape key to close the window pane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(ESC_KEYSTROKE, "ESCAPE"); pane.getActionMap().put("ESCAPE", action); } /** * Truncate the supplied text so that it fits within the width (minus margin) * of the supplied component. Truncation is achieved by replacing a chunk from * the center of the string with an ellipsis. * * @param text Text to truncate * @param comp Component to fit text into * @param margin Additional space to leave empty * @return A truncated string */ public static String truncateToFit(String text, JComponent comp, int margin) { if (text == null || text.isEmpty() || comp == null) { return text; } final int targetWidth = comp.getWidth(); // Early out if component is not visible if (targetWidth < 1) { return text; } Graphics graphics = comp.getGraphics(); if (graphics == null) { return text; } FontMetrics metrics = graphics.getFontMetrics(); final int fullWidth = metrics.stringWidth(text); // Early out if string + margin already fits if (fullWidth + margin < targetWidth) { return text; } final int truncateCharWidth = metrics.charWidth(StringUtil.TRUNCATE_CHAR); final int middle = text.offsetByCodePoints(0, text.codePointCount(0, text.length()) / 2); int chompStart = middle, chompEnd = middle; String chomp = null; // Calculate size when removing progressively larger chunks from the middle while (true) { if (chompStart == 0 || chompEnd == text.length()) { break; } chomp = text.substring(chompStart, chompEnd); int newWidth = fullWidth - metrics.stringWidth(chomp) + truncateCharWidth + margin; if (newWidth <= targetWidth) { break; } chompStart = text.offsetByCodePoints(chompStart, -1); chompEnd = text.offsetByCodePoints(chompEnd, 1); } if (chomp != null) { text = text.substring(0, chompStart) + StringUtil.TRUNCATE_CHAR + text.substring(chompEnd, text.length()); } return text; } public static void forwardMouseWheelEvent(Component target, MouseWheelEvent evt) { Toolkit.getDefaultToolkit().getSystemEventQueue().postEvent( new MouseWheelEvent(target, evt.getID(), evt.getWhen(), evt.getModifiers(), evt.getX(), evt.getY(), evt.getClickCount(), evt.isPopupTrigger(), evt.getScrollType(), evt.getScrollAmount(), evt.getWheelRotation())); } public static void fitInScreen(Component comp) { Rectangle maxBounds = GraphicsEnvironment.getLocalGraphicsEnvironment().getMaximumWindowBounds(); Rectangle compBounds = comp.getBounds(); Rectangle newBounds = new Rectangle(Math.max(compBounds.x, maxBounds.x), Math.max(compBounds.y, maxBounds.y), Math.min(compBounds.width, maxBounds.width - maxBounds.y), Math.min(compBounds.height, maxBounds.height - maxBounds.y)); if (newBounds.x + newBounds.width > maxBounds.width) { newBounds.x = Math.max(maxBounds.x, maxBounds.width - newBounds.width); } if (newBounds.y + newBounds.height > maxBounds.height) { newBounds.y = Math.max(maxBounds.y, maxBounds.height - newBounds.height); } if (!newBounds.equals(compBounds)) { comp.setBounds(newBounds); } } public static void setCaretUpdateEnabled(JTextComponent comp, boolean updateEnabled) { Caret caret = comp.getCaret(); if (caret instanceof DefaultCaret) { ((DefaultCaret) caret).setUpdatePolicy(updateEnabled ? DefaultCaret.UPDATE_WHEN_ON_EDT : DefaultCaret.NEVER_UPDATE); } } /** * Make caret visible even when the {@link JTextComponent} is not editable. */ public static FocusListener makeCaretAlwaysVisible(final JTextComponent comp) { FocusListener listener = new FocusAdapter() { @Override public void focusGained(FocusEvent e) { Caret caret = comp.getCaret(); caret.setVisible(true); caret.setSelectionVisible(true); } }; comp.addFocusListener(listener); return listener; } /** * Ensure the frame width is OK. This is really just a workaround for * <a href="https://bugs.openjdk.java.net/browse/JDK-8065739">JDK-8065739 * </a>, a Java bug specific to Java 1.8 on OS X whereby a frame too close * to the width of the screen will warp to one corner with tiny dimensions. * * @param width * Proposed window width * @return A safe window width */ public static int correctFrameWidth(int width) { if (Platform.isMacOSX() && System.getProperty("java.version").startsWith("1.8")) { int screenWidth = GraphicsEnvironment.getLocalGraphicsEnvironment().getMaximumWindowBounds().width; // 50 is a magic number. Can be as low as 11 (tested on OS X // 10.10.2, Java 1.8.0_31). width = Math.min(width, screenWidth - 50); } return width; } /** * Toggle the enabled property of an entire hierarchy of components. * * @param parent * The parent component, which will also be toggled * @param isEnabled * Enabled or not */ public static void setHierarchyEnabled(Component parent, boolean isEnabled) { visitHierarchy(parent, c -> c.setEnabled(isEnabled)); } public static void visitHierarchy(Component parent, Consumer<Component> consumer) { visitHierarchy(parent, c -> true, consumer); } public static void visitHierarchy(Component parent, Predicate<Component> filter, Consumer<Component> consumer) { if (filter.test(parent)) { consumer.accept(parent); if (parent instanceof JComponent) { for (Component child : ((JComponent) parent).getComponents()) { visitHierarchy(child, filter, consumer); } } } } public static List<Component> listHierarchy(Component parent) { List<Component> cs = new ArrayList<>(); visitHierarchy(parent, cs::add); return cs; } private static Optional<Rectangle> getStoredRectangle(String key) { try { int x = Integer.parseInt(Preferences.getPreference(key + "_x")); int y = Integer.parseInt(Preferences.getPreference(key + "_y")); int w = correctFrameWidth(Integer.parseInt(Preferences.getPreference(key + "_width"))); int h = Integer.parseInt(Preferences.getPreference(key + "_height")); return Optional.of(new Rectangle(x, y, w, h)); } catch (NumberFormatException e) { return Optional.empty(); } } public static void persistGeometry(Window window, String key) { persistGeometry(window, key, null); } public static void persistGeometry(Window window, String key, Runnable extraProcessing) { getStoredRectangle(key).ifPresent(window::setBounds); String xKey = key + "_x"; String yKey = key + "_y"; String widthKey = key + "_width"; String heightKey = key + "_height"; Timer timer = new Timer(500, e -> { Rectangle bounds = window.getBounds(); Preferences.setPreference(xKey, bounds.x); Preferences.setPreference(yKey, bounds.y); Preferences.setPreference(widthKey, bounds.width); Preferences.setPreference(heightKey, bounds.height); if (extraProcessing != null) { extraProcessing.run(); } }); timer.setRepeats(false); window.addComponentListener(new ComponentAdapter() { @Override public void componentMoved(ComponentEvent e) { timer.restart(); } @Override public void componentResized(ComponentEvent e) { timer.restart(); } }); } public static void setWindowIcon(Window window) { List<Image> icons; if (Platform.isMacOSX()) { icons = Arrays.asList(OSXIntegration.APP_ICON_MAC); } else { icons = Arrays.asList(ResourcesUtil.APP_ICON_16X16, ResourcesUtil.APP_ICON_32X32); } window.setIconImages(icons); } /** * Calculate a highlight color from a base color, with a given amount of * adjustment. * <p> * The adjustment is added to each of the base color's RGB values and * rebounds from the boundaries [0, 255]. E.g. 250 + 4 -> 254 but 250 + 11 * -> 249, and 5 - 4 -> 1 but 5 - 11 -> 6. */ public static Color getHighlightColor(Color base, int adjustment) { return new Color(reboundClamp(0, 255, base.getRed() + adjustment), reboundClamp(0, 255, base.getGreen() + adjustment), reboundClamp(0, 255, base.getBlue() + adjustment), base.getAlpha()); } /** * Convenience method for {@link #getHighlightColor(Color, int)} using the * default adjustment (10 darker than base). */ public static Color getHighlightColor(Color base) { return getHighlightColor(base, -10); } /** * Clamp value between min and max by "rebounding" within the range [min, * max]. */ static int reboundClamp(int min, int max, int value) { if (value < min) { return reboundClamp(min, max, min + (min - value)); } else if (value > max) { return reboundClamp(min, max, max - (value - max)); } else { return value; } } public static String getKeyStrokeText(KeyStroke ks) { StringBuilder sb = new StringBuilder(); String modifierText = KeyEvent.getKeyModifiersText(ks.getModifiers()); sb.append(modifierText); String keyText = KeyEvent.getKeyText(ks.getKeyCode()); if (!keyText.isEmpty() && !modifierText.contains(keyText)) { if (sb.length() > 0) { sb.append('+'); } sb.append(keyText); } return sb.toString(); } @SuppressWarnings("serial") public static void makeUndoable(JTextComponent comp) { UndoManager manager = new UndoManager(); comp.getDocument().addUndoableEditListener(manager); // Handle undo (Ctrl/Cmd+Z); KeyStroke undo = KeyStroke.getKeyStroke(KeyEvent.VK_Z, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask(), false); Action undoAction = new AbstractAction() { public void actionPerformed(ActionEvent e) { if (manager.canUndo()) { manager.undo(); } } }; comp.getInputMap().put(undo, "UNDO"); comp.getActionMap().put("UNDO", undoAction); // Handle redo (Ctrl/Cmd+Y); KeyStroke redo = KeyStroke.getKeyStroke(KeyEvent.VK_Y, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask(), false); Action redoAction = new AbstractAction() { public void actionPerformed(ActionEvent e) { if (manager.canRedo()) { manager.redo(); } } }; comp.getInputMap().put(redo, "REDO"); comp.getActionMap().put("REDO", redoAction); } }