/**************************************************************************
OmegaT - Computer Assisted Translation (CAT) tool
with fuzzy matching, translation memory, keyword search,
glossaries, and translation leveraging into updated projects.
Copyright (C) 2016 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.gui.preferences;
import java.awt.BasicStroke;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
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.ComponentListener;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.geom.Area;
import java.awt.geom.RoundRectangle2D;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import javax.swing.AbstractAction;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JLayeredPane;
import javax.swing.JPanel;
import javax.swing.JRootPane;
import javax.swing.JScrollBar;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;
import javax.swing.WindowConstants;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.event.TreeExpansionEvent;
import javax.swing.event.TreeExpansionListener;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeCellRenderer;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;
import org.omegat.core.Core;
import org.omegat.core.team2.gui.RepositoriesCredentialsController;
import org.omegat.externalfinder.gui.ExternalFinderPreferencesController;
import org.omegat.gui.filters2.FiltersCustomizerController;
import org.omegat.gui.main.ProjectUICommands;
import org.omegat.gui.preferences.IPreferencesController.FurtherActionListener;
import org.omegat.gui.preferences.view.AppearanceController;
import org.omegat.gui.preferences.view.AutoCompleterController;
import org.omegat.gui.preferences.view.AutotextAutoCompleterOptionsController;
import org.omegat.gui.preferences.view.CharTableAutoCompleterOptionsController;
import org.omegat.gui.preferences.view.CustomColorSelectionController;
import org.omegat.gui.preferences.view.DictionaryPreferencesController;
import org.omegat.gui.preferences.view.EditingBehaviorController;
import org.omegat.gui.preferences.view.FontSelectionController;
import org.omegat.gui.preferences.view.GeneralOptionsController;
import org.omegat.gui.preferences.view.GlossaryAutoCompleterOptionsController;
import org.omegat.gui.preferences.view.GlossaryPreferencesController;
import org.omegat.gui.preferences.view.HistoryAutoCompleterOptionsController;
import org.omegat.gui.preferences.view.LanguageToolConfigurationController;
import org.omegat.gui.preferences.view.MachineTranslationPreferencesController;
import org.omegat.gui.preferences.view.PluginsPreferencesController;
import org.omegat.gui.preferences.view.SaveOptionsController;
import org.omegat.gui.preferences.view.SecureStoreController;
import org.omegat.gui.preferences.view.SpellcheckerConfigurationController;
import org.omegat.gui.preferences.view.TMMatchesPreferencesController;
import org.omegat.gui.preferences.view.TagProcessingOptionsController;
import org.omegat.gui.preferences.view.TeamOptionsController;
import org.omegat.gui.preferences.view.UserPassController;
import org.omegat.gui.preferences.view.ViewOptionsController;
import org.omegat.gui.segmentation.SegmentationCustomizerController;
import org.omegat.util.OStrings;
import org.omegat.util.Preferences;
import org.omegat.util.StringUtil;
import org.omegat.util.gui.StaticUIUtils;
/**
* A modal window aggregating all preference "views"
* ({@link IPreferencesController}s). Plugins can provide their own views via
* {@link PreferencesControllers#addSupplier(java.util.function.Supplier)}.
*
* @author Aaron Madlon-Kay
*/
public class PreferencesWindowController implements FurtherActionListener {
private static final Logger LOGGER = Logger.getLogger(PreferencesWindowController.class.getName());
private static final String ACTION_KEY_NEW_SEARCH = "clearSearch";
private static final String ACTION_KEY_CLEAR_OR_CLOSE = "clearSearchOrClose";
private static final String ACTION_KEY_DO_SEARCH = "doSearch";
private JDialog dialog;
private PreferencePanel outerPanel;
private PreferenceViewSelectorPanel innerPanel;
private HighlightablePanel overlay;
private IPreferencesController currentView;
private boolean didLoadGuis;
private final Map<String, Runnable> persistenceRunnables = new HashMap<>();
public void show(Window parent) {
show(parent, null);
}
@SuppressWarnings("serial")
public void show(Window parent, Class<? extends IPreferencesController> initialSelection) {
dialog = new JDialog();
dialog.setTitle(OStrings.getString("PREFERENCES_TITLE_NO_SELECTION"));
dialog.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
dialog.setModal(true);
StaticUIUtils.setWindowIcon(dialog);
outerPanel = new PreferencePanel();
innerPanel = new PreferenceViewSelectorPanel();
outerPanel.prefsViewPanel.add(innerPanel, BorderLayout.CENTER);
dialog.getContentPane().add(outerPanel);
overlay = new HighlightablePanel(dialog.getRootPane(), innerPanel.selectedPrefsScrollPane);
// Prevent ugly white viewport background with GTK LAF
innerPanel.selectedPrefsScrollPane.getViewport().setOpaque(false);
innerPanel.selectedPrefsScrollPane.setBackground(innerPanel.getBackground());
innerPanel.searchTextField.getDocument().addDocumentListener(new DocumentListener() {
@Override
public void removeUpdate(DocumentEvent e) {
SwingUtilities.invokeLater(PreferencesWindowController.this::searchAndFilterTree);
}
@Override
public void insertUpdate(DocumentEvent e) {
SwingUtilities.invokeLater(PreferencesWindowController.this::searchAndFilterTree);
}
@Override
public void changedUpdate(DocumentEvent e) {
SwingUtilities.invokeLater(PreferencesWindowController.this::searchAndFilterTree);
}
});
innerPanel.searchTextField.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_DOWN || e.getKeyCode() == KeyEvent.VK_UP) {
innerPanel.availablePrefsTree.getActionForKeyStroke(KeyStroke.getKeyStrokeForEvent(e))
.actionPerformed(new ActionEvent(innerPanel.availablePrefsTree, 0, null));
innerPanel.availablePrefsTree.requestFocusInWindow();
e.consume();
}
}
});
innerPanel.searchTextField.addFocusListener(new FocusAdapter() {
@Override
public void focusGained(FocusEvent e) {
innerPanel.searchTextField.selectAll();
searchCurrentView();
preloadGuis();
}
});
innerPanel.clearButton.addActionListener(e -> {
innerPanel.searchTextField.clear();
});
innerPanel.availablePrefsTree.getSelectionModel()
.setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
DefaultMutableTreeNode root = createNodeTree();
walkTree(root, node -> {
IPreferencesController view = (IPreferencesController) node.getUserObject();
if (view != null) {
view.addFurtherActionListener(this);
}
});
innerPanel.availablePrefsTree.setModel(new DefaultTreeModel(root));
innerPanel.availablePrefsTree.addTreeSelectionListener(e -> {
handleViewSelection(e);
updateTitle();
});
innerPanel.availablePrefsTree.addTreeExpansionListener(new TreeExpansionListener() {
@Override
public void treeExpanded(TreeExpansionEvent event) {
SwingUtilities.invokeLater(PreferencesWindowController.this::adjustTreeSize);
}
@Override
public void treeCollapsed(TreeExpansionEvent event) {
}
});
innerPanel.selectedPrefsScrollPane.getViewport().setBackground(innerPanel.getBackground());
DefaultTreeCellRenderer renderer = (DefaultTreeCellRenderer) innerPanel.availablePrefsTree
.getCellRenderer();
renderer.setIcon(null);
renderer.setLeafIcon(null);
renderer.setOpenIcon(null);
renderer.setClosedIcon(null);
renderer.setDisabledIcon(null);
outerPanel.okButton.addActionListener(e -> {
if (currentView == null || currentView.validate()) {
new SwingWorker<Void, Void>() {
@Override
protected Void doInBackground() throws Exception {
doSave();
return null;
}
@Override
protected void done() {
if (getIsReloadRequired()) {
SwingUtilities.invokeLater(ProjectUICommands::promptReload);
}
}
}.execute();
StaticUIUtils.closeWindowByEvent(dialog);
}
});
outerPanel.cancelButton.addActionListener(e -> StaticUIUtils.closeWindowByEvent(dialog));
// Hide undo, reset buttons on outer panel
outerPanel.undoButton.setVisible(false);
outerPanel.resetButton.setVisible(false);
// Use ones on inner panel to indicate that actions are view-specific
innerPanel.undoButton.addActionListener(e -> currentView.undoChanges());
innerPanel.resetButton.addActionListener(e -> currentView.restoreDefaults());
dialog.addWindowListener(new WindowAdapter() {
@Override
public void windowOpened(WindowEvent e) {
walkTree(root, node -> {
// Start with tree fully expanded
if (node.getChildCount() > 0) {
innerPanel.availablePrefsTree.expandPath(new TreePath(node.getPath()));
}
});
SwingUtilities.invokeLater(() -> {
if (initialSelection != null) {
selectView(initialSelection);
}
});
}
});
innerPanel.availablePrefsScrollPane.addComponentListener(new ComponentAdapter() {
@Override
public void componentResized(ComponentEvent e) {
SwingUtilities.invokeLater(PreferencesWindowController.this::adjustTreeSize);
}
});
ActionMap actionMap = innerPanel.getActionMap();
InputMap inputMap = innerPanel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
actionMap.put(ACTION_KEY_NEW_SEARCH, new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
innerPanel.searchTextField.requestFocusInWindow();
innerPanel.searchTextField.selectAll();
}
});
KeyStroke searchKeyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_F,
Toolkit.getDefaultToolkit().getMenuShortcutKeyMask());
inputMap.put(searchKeyStroke, ACTION_KEY_NEW_SEARCH);
actionMap.put(ACTION_KEY_CLEAR_OR_CLOSE, new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
if (!innerPanel.searchTextField.isEmpty()) {
// Move focus away from search field
innerPanel.availablePrefsTree.requestFocusInWindow();
innerPanel.clearButton.doClick();
} else {
StaticUIUtils.closeWindowByEvent(dialog);
}
}
});
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), ACTION_KEY_CLEAR_OR_CLOSE);
// Don't let Enter close the dialog
innerPanel.searchTextField.getInputMap(JComponent.WHEN_FOCUSED)
.put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), ACTION_KEY_DO_SEARCH);
innerPanel.searchTextField.getActionMap().put(ACTION_KEY_DO_SEARCH, new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
searchCurrentView();
}
});
String searchKeyText = StaticUIUtils.getKeyStrokeText(searchKeyStroke);
innerPanel.searchTextField.setHintText(OStrings.getString("PREFERENCES_SEARCH_HINT", searchKeyText));
// Set initial state
searchAndFilterTree();
adjustTreeSize();
dialog.getRootPane().setDefaultButton(outerPanel.okButton);
dialog.setPreferredSize(new Dimension(800, 500));
dialog.pack();
// Prevent search field from getting initial focus
innerPanel.availablePrefsTree.requestFocusInWindow();
dialog.setLocationRelativeTo(parent);
dialog.setVisible(true);
}
private static DefaultMutableTreeNode createNodeTree() {
HideableNode root = new HideableNode();
root.add(new HideableNode(new GeneralOptionsController()));
root.add(new HideableNode(new MachineTranslationPreferencesController()));
root.add(new HideableNode(new GlossaryPreferencesController()));
root.add(new HideableNode(new DictionaryPreferencesController()));
HideableNode appearanceNode = new HideableNode(new AppearanceController());
appearanceNode.add(new HideableNode(new FontSelectionController()));
appearanceNode.add(new HideableNode(new CustomColorSelectionController()));
root.add(appearanceNode);
root.add(new HideableNode(new FiltersCustomizerController()));
root.add(new HideableNode(new SegmentationCustomizerController()));
HideableNode acNode = new HideableNode(new AutoCompleterController());
acNode.add(new HideableNode(new GlossaryAutoCompleterOptionsController()));
acNode.add(new HideableNode(new AutotextAutoCompleterOptionsController()));
acNode.add(new HideableNode(new CharTableAutoCompleterOptionsController()));
acNode.add(new HideableNode(new HistoryAutoCompleterOptionsController()));
root.add(acNode);
root.add(new HideableNode(new SpellcheckerConfigurationController()));
root.add(new HideableNode(new LanguageToolConfigurationController()));
root.add(new HideableNode(new ExternalFinderPreferencesController()));
root.add(new HideableNode(new EditingBehaviorController()));
root.add(new HideableNode(new TagProcessingOptionsController()));
HideableNode teamNode = new HideableNode(new TeamOptionsController());
teamNode.add(new HideableNode(new RepositoriesCredentialsController()));
root.add(teamNode);
root.add(new HideableNode(new TMMatchesPreferencesController()));
root.add(new HideableNode(new ViewOptionsController()));
root.add(new HideableNode(new SaveOptionsController()));
root.add(new HideableNode(new UserPassController()));
root.add(new HideableNode(new SecureStoreController()));
HideableNode pluginsNode = new HideableNode(new PluginsPreferencesController());
root.add(pluginsNode);
PreferencesControllers.getSuppliers().forEach(s -> placePluginView(root, s.get()));
return root;
}
private static void placePluginView(HideableNode root, IPreferencesController view) {
Class<? extends IPreferencesController> parentClass = view.getParentViewClass();
Class<? extends IPreferencesController> effectiveParentClass = parentClass == null ? PluginsPreferencesController.class
: parentClass;
walkTree(root, node -> {
IPreferencesController parent = (IPreferencesController) node.getUserObject();
if (parent != null && parent.getClass().equals(effectiveParentClass)) {
node.add(new HideableNode(view));
}
});
}
@Override
public void setRestartRequired(boolean restartRequired) {
updateMessage();
}
@Override
public void setReloadRequired(boolean reloadRequired) {
updateMessage();
}
private HideableNode getRoot() {
return (HideableNode) innerPanel.availablePrefsTree.getModel().getRoot();
}
private boolean getIsRestartRequired() {
return anyInTree(getRoot(), node -> {
IPreferencesController view = (IPreferencesController) node.getUserObject();
return view != null && view.isRestartRequired();
});
}
private boolean getIsReloadRequired() {
if (!Core.getProject().isProjectLoaded()) {
return false;
}
return anyInTree(getRoot(), node -> {
IPreferencesController view = (IPreferencesController) node.getUserObject();
return view != null && view.isReloadRequired();
});
}
private void updateMessage() {
String message = null;
if (getIsRestartRequired()) {
message = OStrings.getString("PREFERENCES_WARNING_NEEDS_RESTART");
} else if (getIsReloadRequired()) {
message = OStrings.getString("PREFERENCES_WARNING_NEEDS_RELOAD");
}
outerPanel.messageTextArea.setText(message);
}
private void handleViewSelection(TreeSelectionEvent e) {
TreePath selectedPath = e.getNewLeadSelectionPath();
if (selectedPath == null) {
return;
}
DefaultMutableTreeNode node = (DefaultMutableTreeNode) selectedPath.getLastPathComponent();
if (node == null) {
return;
}
IPreferencesController oldView = currentView;
Object obj = node.getUserObject();
if (!(obj instanceof IPreferencesController)) {
return;
}
IPreferencesController newView = (IPreferencesController) obj;
if (Objects.equals(oldView, newView)) {
return;
}
if (oldView != null && !oldView.validate()) {
innerPanel.availablePrefsTree.getSelectionModel().setSelectionPath(e.getOldLeadSelectionPath());
return;
}
if (!persistenceRunnables.containsKey(newView.getClass().getName())) {
persistenceRunnables.put(newView.getClass().getName(), newView::persist);
}
overlay.setHighlightComponent(null);
innerPanel.innerViewHolder.removeAll();
innerPanel.innerViewHolder.add(newView.getGui(), BorderLayout.CENTER);
innerPanel.selectedPrefsScrollPane.setViewportView(innerPanel.viewHolder);
currentView = newView;
SwingUtilities.invokeLater(() -> {
adjustSize();
searchCurrentView();
});
}
private void updateTitle() {
if (currentView == null) {
dialog.setTitle(OStrings.getString("PREFERENCES_TITLE_NO_SELECTION"));
} else {
dialog.setTitle(StringUtil.format(OStrings.getString("PREFERENCES_TITLE_WITH_SELECTION"), currentView));
}
}
private void adjustSize() {
Dimension viewPreferredSize = innerPanel.viewHolder.getPreferredSize();
Dimension viewActualSize = innerPanel.selectedPrefsScrollPane.getViewport().getSize();
Dimension dialogSize = dialog.getSize();
boolean shouldAdjust = false;
if (viewPreferredSize.width > viewActualSize.width) {
dialogSize.width += viewPreferredSize.width - viewActualSize.width;
shouldAdjust = true;
}
if (viewPreferredSize.height > viewActualSize.height) {
dialogSize.height += viewPreferredSize.height - viewActualSize.height;
shouldAdjust = true;
}
if (shouldAdjust) {
dialog.setSize(dialogSize);
StaticUIUtils.fitInScreen(dialog);
}
}
private void adjustTreeSize() {
JScrollBar hScrollBar = innerPanel.availablePrefsScrollPane.getHorizontalScrollBar();
if (hScrollBar != null) {
int currentWidth = innerPanel.availablePrefsScrollPane.getViewport().getWidth();
int preferredWidth = hScrollBar.getMaximum();
if (preferredWidth > currentWidth) {
int newWidth = innerPanel.leftPanel.getWidth() + (preferredWidth - currentWidth);
innerPanel.leftPanel.setMinimumSize(new Dimension(newWidth, 0));
innerPanel.mainSplitPane.setDividerLocation(-1);
}
}
}
private void searchAndFilterTree() {
incrementalSearchImpl(true, getRoot());
}
private void searchCurrentView() {
TreePath selection = innerPanel.availablePrefsTree.getSelectionPath();
if (selection != null) {
HideableNode root = (HideableNode) selection.getLastPathComponent();
incrementalSearchImpl(false, root);
}
}
private void incrementalSearchImpl(boolean filterTree, HideableNode root) {
boolean isEmptyQuery = innerPanel.searchTextField.isEmpty();
innerPanel.clearButton.setEnabled(!isEmptyQuery);
if (isEmptyQuery) {
if (filterTree) {
if (setTreeVisible(root, true)) {
((DefaultTreeModel) innerPanel.availablePrefsTree.getModel()).reload();
}
selectView(currentView);
}
overlay.setHighlightComponent(null);
return;
}
String query = innerPanel.searchTextField.getText().trim();
if (currentView != null && !currentView.validate()) {
innerPanel.searchTextField.clear();
return;
}
Pattern pattern = Pattern.compile(".*" + Pattern.quote(query) + ".*",
Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE);
List<GuiSearchResult> results = searchTree(root, pattern.asPredicate());
if (filterTree) {
setTreeVisible(root, false);
}
if (results.isEmpty()) {
innerPanel.searchTextField.setForeground(Color.RED);
if (filterTree) {
((DefaultTreeModel) innerPanel.availablePrefsTree.getModel()).reload();
}
overlay.setHighlightComponent(null);
innerPanel.selectedPrefsScrollPane.setViewportView(innerPanel.selectedPrefsPlaceholderPanel);
currentView = null;
updateTitle();
} else {
GuiSearchResult topResult = results.get(0);
if (filterTree) {
for (GuiSearchResult result : results) {
setNodePathVisible((HideableNode) result.node, true);
}
((DefaultTreeModel) innerPanel.availablePrefsTree.getModel()).reload();
selectNode(topResult.node);
}
if (topResult.comp != null) {
((JComponent) topResult.comp).scrollRectToVisible(topResult.comp.getBounds());
overlay.setHighlightComponent(topResult.comp);
}
innerPanel.searchTextField.setForeground(Color.BLACK);
}
}
private void selectView(IPreferencesController view) {
firstNodeInTree(getRoot(), node -> node.getUserObject() == view)
.ifPresent(this::selectNode);
}
private void selectView(Class<? extends IPreferencesController> viewClass) {
firstNodeInTree(getRoot(), node -> {
Object obj = node.getUserObject();
return obj != null && obj.getClass().equals(viewClass);
}).ifPresent(this::selectNode);
}
void selectNode(DefaultMutableTreeNode node) {
innerPanel.availablePrefsTree.setSelectionPath(new TreePath(node.getPath()));
}
/**
* Set the visibility of the entire subtree starting at root.
*
* @param root
* Root node
* @param isVisible
* Visible or not
* @return true if any of the nodes' visibility values were changed
*/
private static boolean setTreeVisible(HideableNode root, boolean isVisible) {
List<Boolean> changes = mapTree(root, node -> {
HideableNode hideable = (HideableNode) node;
boolean wasVisible = hideable.isVisible;
hideable.setVisible(isVisible);
return wasVisible != isVisible;
});
root.setVisible(true);
return changes.contains(true);
}
private static void setNodePathVisible(HideableNode node, boolean isVisible) {
node.setVisible(isVisible);
for (TreeNode parent : node.getPath()) {
((HideableNode) parent).setVisible(isVisible);
}
}
private static List<GuiSearchResult> searchTree(DefaultMutableTreeNode root, Predicate<String> filter) {
List<GuiSearchResult> result = new ArrayList<>();
walkTree(root, node -> {
IPreferencesController view = (IPreferencesController) node.getUserObject();
if (view != null) {
visitUiStrings(view.getGui(), (str, comp) -> {
if (filter.test(str)) {
result.add(new GuiSearchResult(node, str, comp));
}
});
if (filter.test(view.toString())) {
result.add(new GuiSearchResult(node, view.toString(), null));
}
}
});
return result;
}
static class GuiSearchResult {
final public DefaultMutableTreeNode node;
final public String string;
final public Component comp;
public GuiSearchResult(DefaultMutableTreeNode node, String string, Component comp) {
this.node = node;
this.string = string;
this.comp = comp;
}
}
private static void visitUiStrings(Component comp, BiConsumer<String, Component> consumer) {
StaticUIUtils.visitHierarchy(comp, c -> c.isVisible(), c -> {
try {
Method getText = c.getClass().getMethod("getText");
String str = (String) getText.invoke(c);
if (!StringUtil.isEmpty(str)) {
consumer.accept(str, c);
}
} catch (NoSuchMethodException e) {
// Skip
} catch (SecurityException | IllegalAccessException | IllegalArgumentException
| InvocationTargetException e) {
e.printStackTrace();
}
});
}
private static void walkTree(DefaultMutableTreeNode node, Consumer<DefaultMutableTreeNode> consumer) {
consumer.accept(node);
Enumeration<?> e = node.children();
while (e.hasMoreElements()) {
Object o = e.nextElement();
walkTree((DefaultMutableTreeNode) o, consumer);
}
}
private static <T> List<T> mapTree(DefaultMutableTreeNode node,
Function<DefaultMutableTreeNode, T> function) {
List<T> results = new ArrayList<>();
walkTree(node, n -> {
results.add(function.apply(n));
});
return results;
}
private static boolean anyInTree(DefaultMutableTreeNode node,
Predicate<DefaultMutableTreeNode> predicate) {
return firstNodeInTree(node, predicate).isPresent();
}
private static Optional<DefaultMutableTreeNode> firstNodeInTree(DefaultMutableTreeNode node,
Predicate<DefaultMutableTreeNode> predicate) {
if (predicate.test(node)) {
return Optional.of(node);
} else {
Enumeration<?> e = node.children();
while (e.hasMoreElements()) {
Object o = e.nextElement();
Optional<DefaultMutableTreeNode> child = firstNodeInTree((DefaultMutableTreeNode) o, predicate);
if (child.isPresent()) {
return child;
}
}
}
return Optional.empty();
}
private void preloadGuis() {
if (didLoadGuis) {
return;
}
new SwingWorker<Void, Void>() {
@Override
protected Void doInBackground() throws Exception {
preloadGuisImpl();
didLoadGuis = true;
return null;
}
}.execute();
}
private void preloadGuisImpl() {
walkTree(getRoot(), node -> {
IPreferencesController view = (IPreferencesController) node.getUserObject();
if (view != null) {
try {
view.getGui();
} catch (Throwable t) {
LOGGER.log(Level.SEVERE, t.getMessage(), t);
}
}
});
}
private void doSave() {
persistenceRunnables.entrySet().forEach(e -> {
long start = System.currentTimeMillis();
e.getValue().run();
long end = System.currentTimeMillis();
if (end - start > 100) {
LOGGER.finer(() -> String.format("Persisting %s took %d ms", e.getKey(), end - start));
}
});
Preferences.save();
}
@SuppressWarnings("serial")
static class HideableNode extends DefaultMutableTreeNode {
private boolean isVisible = true;
public HideableNode() {
super();
}
public HideableNode(Object userObject) {
super(userObject);
}
public boolean isVisible() {
return isVisible;
}
public void setVisible(boolean isVisible) {
this.isVisible = isVisible;
}
@Override
public TreeNode getChildAt(int index) {
if (children == null) {
throw new IndexOutOfBoundsException("node has no children");
}
int realIndex = -1;
int visibleIndex = -1;
Enumeration<?> e = children.elements();
while (e.hasMoreElements()) {
HideableNode node = (HideableNode) e.nextElement();
if (node.isVisible()) {
visibleIndex++;
}
realIndex++;
if (visibleIndex == index) {
return (TreeNode) children.elementAt(realIndex);
}
}
throw new IndexOutOfBoundsException("index unmatched");
}
@Override
public int getChildCount() {
if (children == null) {
return 0;
}
int count = 0;
Enumeration<?> e = children.elements();
while (e.hasMoreElements()) {
HideableNode node = (HideableNode) e.nextElement();
if (node.isVisible()) {
count++;
}
}
return count;
}
}
@SuppressWarnings("serial")
static class HighlightablePanel extends JPanel {
private static final Color SHADOW_COLOR = new Color(128, 128, 128, 128);
private static final Color STROKE_COLOR = new Color(238, 210, 00, 128);
private static final int STROKE = 2;
private static final BasicStroke STROKE_OBJ = new BasicStroke(STROKE);
private final transient ComponentListener compListener;
private final transient MouseAdapter mouseAdapter;
private final JRootPane rootPane;
private final Component overlayComponent;
private final Rectangle clipRect = new Rectangle();
private final Rectangle highlightRect = new Rectangle();
private Component comp;
public HighlightablePanel(JRootPane rootPane, Component overlayComponent) {
this.rootPane = rootPane;
this.overlayComponent = overlayComponent;
this.mouseAdapter = new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
forward(e);
}
@Override
public void mouseReleased(MouseEvent e) {
forward(e);
setHighlightComponent(null);
}
@Override
public void mouseClicked(MouseEvent e) {
forward(e);
}
@Override
public void mouseExited(MouseEvent e) {
forward(e);
}
@Override
public void mouseEntered(MouseEvent e) {
forward(e);
}
@Override
public void mouseMoved(MouseEvent e) {
forward(e);
}
@Override
public void mouseDragged(MouseEvent e) {
forward(e);
}
@Override
public void mouseWheelMoved(MouseWheelEvent e) {
forward(e);
}
private void forward(MouseEvent e) {
Container contentPane = rootPane.getContentPane();
Point p = SwingUtilities.convertPoint(HighlightablePanel.this, e.getPoint(), contentPane);
Component target = contentPane.findComponentAt(p);
if (target != null) {
e = SwingUtilities.convertMouseEvent(HighlightablePanel.this, e, target);
target.dispatchEvent(e);
}
}
};
addMouseListener(mouseAdapter);
addMouseMotionListener(mouseAdapter);
addMouseWheelListener(mouseAdapter);
this.compListener = new ComponentAdapter() {
@Override
public void componentResized(ComponentEvent e) {
update();
}
@Override
public void componentMoved(ComponentEvent e) {
update();
}
@Override
public void componentShown(ComponentEvent e) {
update();
}
@Override
public void componentHidden(ComponentEvent e) {
update();
}
};
SwingUtilities.windowForComponent(rootPane).addWindowFocusListener(new WindowAdapter() {
@Override
public void windowLostFocus(WindowEvent e) {
setVisible(false);
}
@Override
public void windowGainedFocus(WindowEvent e) {
setVisible(true);
}
});
setOpaque(false);
}
public void setHighlightComponent(Component comp) {
Component oldComp = this.comp;
this.comp = comp;
if (comp == null) {
uninstall();
} else if (Objects.equals(oldComp, comp)) {
update();
} else {
install();
}
}
private void update() {
Rectangle bounds = SwingUtilities.convertRectangle(overlayComponent.getParent(),
overlayComponent.getBounds(), rootPane.getLayeredPane());
setBounds(bounds);
repaint();
}
private void install() {
JLayeredPane layeredPane = rootPane.getLayeredPane();
if (!layeredPane.isAncestorOf(this)) {
layeredPane.add(this, JLayeredPane.MODAL_LAYER);
overlayComponent.addComponentListener(compListener);
}
update();
}
private void uninstall() {
JLayeredPane layeredPane = rootPane.getLayeredPane();
layeredPane.remove(this);
overlayComponent.removeComponentListener(compListener);
overlayComponent.repaint();
}
@Override
public void paint(Graphics g) {
super.paint(g);
if (comp != null) {
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setStroke(STROKE_OBJ);
paintHighlight(g2);
g2.dispose();
}
}
private void paintHighlight(Graphics2D g) {
g.getClipBounds(clipRect);
Area realClip = new Area(clipRect);
Rectangle rect = getHighlightRect();
Shape knockOut = roundifyRect(rect);
realClip.subtract(new Area(knockOut));
g.setClip(realClip);
g.setColor(SHADOW_COLOR);
g.fillRect(clipRect.x, clipRect.y, clipRect.width, clipRect.height);
g.setClip(clipRect);
g.setColor(STROKE_COLOR);
g.draw(knockOut);
}
private Rectangle getHighlightRect() {
comp.getBounds(highlightRect);
Point p = SwingUtilities.convertPoint(comp.getParent(), highlightRect.x, highlightRect.y, this);
highlightRect.setLocation(p);
return highlightRect;
}
private Shape roundifyRect(Rectangle rect) {
int r = Math.min(10, Math.round(rect.height * 0.5f));
int halfR = r / 2;
RoundRectangle2D roundRect = new RoundRectangle2D.Float(rect.x - halfR, rect.y - halfR,
rect.width + r - (STROKE - 0.5f), rect.height + r - STROKE, r, r);
return roundRect;
}
}
}