From e949041aa04ebc23b21719e7ddf09f215f4726f7 Mon Sep 17 00:00:00 2001 From: fourbites Date: Tue, 17 Feb 2026 10:14:15 +0100 Subject: [PATCH] Add search to admin config --- .../admin/client/ConfigEditorPanel.java | 202 ++++++++++++++++++ 1 file changed, 202 insertions(+) diff --git a/core/src/main/java/com/threerings/admin/client/ConfigEditorPanel.java b/core/src/main/java/com/threerings/admin/client/ConfigEditorPanel.java index 8d7bc56bd..00c932ffb 100644 --- a/core/src/main/java/com/threerings/admin/client/ConfigEditorPanel.java +++ b/core/src/main/java/com/threerings/admin/client/ConfigEditorPanel.java @@ -5,15 +5,27 @@ package com.threerings.admin.client; +import java.awt.Color; import java.awt.Component; +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; +import java.util.ArrayList; import java.util.Comparator; +import java.util.List; + +import javax.swing.AbstractAction; +import javax.swing.KeyStroke; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTabbedPane; +import javax.swing.JTextField; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; import com.samskivert.util.QuickSort; +import com.samskivert.swing.HGroupLayout; import com.samskivert.swing.VGroupLayout; import com.threerings.presents.util.PresentsContext; @@ -48,6 +60,55 @@ public class ConfigEditorPanel extends JPanel setLayout(new VGroupLayout(VGroupLayout.STRETCH, VGroupLayout.STRETCH, VGroupLayout.DEFAULT_GAP, VGroupLayout.CENTER)); + // add a search bar at the top + JPanel searchPanel = new JPanel(new HGroupLayout(HGroupLayout.STRETCH)); + searchPanel.add(new JLabel("Search: "), HGroupLayout.FIXED); + searchPanel.add(_searchField = new JTextField()); + searchPanel.add(_matchLabel = new JLabel(), HGroupLayout.FIXED); + add(searchPanel, VGroupLayout.FIXED); + + _searchField.getDocument().addDocumentListener(new DocumentListener() { + public void insertUpdate (DocumentEvent e) { filterTabs(); } + public void removeUpdate (DocumentEvent e) { filterTabs(); } + public void changedUpdate (DocumentEvent e) { filterTabs(); } + }); + + // bind Ctrl+F to focus the search field + getInputMap(WHEN_IN_FOCUSED_WINDOW).put( + KeyStroke.getKeyStroke(KeyEvent.VK_F, ActionEvent.CTRL_MASK), "focusSearch"); + getActionMap().put("focusSearch", new AbstractAction() { + public void actionPerformed (ActionEvent e) { + _searchField.requestFocusInWindow(); + _searchField.selectAll(); + } + }); + + // bind Escape to clear search and return focus + _searchField.getInputMap().put( + KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "clearSearch"); + _searchField.getActionMap().put("clearSearch", new AbstractAction() { + public void actionPerformed (ActionEvent e) { + _searchField.setText(""); + _oeditors.requestFocusInWindow(); + } + }); + + // bind Enter / Shift+Enter to cycle through matching tabs + _searchField.getInputMap().put( + KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "nextMatch"); + _searchField.getActionMap().put("nextMatch", new AbstractAction() { + public void actionPerformed (ActionEvent e) { + cycleMatch(1); + } + }); + _searchField.getInputMap().put( + KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, ActionEvent.SHIFT_MASK), "prevMatch"); + _searchField.getActionMap().put("prevMatch", new AbstractAction() { + public void actionPerformed (ActionEvent e) { + cycleMatch(-1); + } + }); + // create our objects tabbed pane add(_oeditors = new JTabbedPane(JTabbedPane.LEFT)); @@ -128,6 +189,129 @@ public class ConfigEditorPanel extends JPanel log.warning("Failed to get config info", "reason", reason); } + /** Filters tabs and highlights matching fields based on the current search text. */ + protected void filterTabs () + { + String query = _searchField.getText().trim().toLowerCase(); + boolean empty = query.isEmpty(); + + _matchingTabs.clear(); + int totalMatches = 0; + for (int ii = 0; ii < _oeditors.getTabCount(); ii++) { + boolean tabNameMatches = !empty && + _oeditors.getTitleAt(ii).toLowerCase().contains(query); + int fieldMatchCount = highlightFields(ii, empty ? null : query); + boolean matches = tabNameMatches || fieldMatchCount > 0; + _oeditors.setForegroundAt(ii, (empty || matches) ? null : MISMATCH_COLOR); + if (matches) { + _matchingTabs.add(ii); + } + totalMatches += fieldMatchCount; + } + + // update match count label + if (empty) { + _matchLabel.setText(""); + } else if (_matchingTabs.isEmpty()) { + _matchLabel.setText("No matches"); + } else { + _matchLabel.setText(totalMatches + " field" + (totalMatches != 1 ? "s" : "") + + " in " + _matchingTabs.size() + " tab" + (_matchingTabs.size() != 1 ? "s" : "")); + } + + _matchCursor = 0; + if (!_matchingTabs.isEmpty()) { + _oeditors.setSelectedIndex(_matchingTabs.get(0)); + scrollToFirstMatch(); + } + } + + /** + * Highlights matching field editors in the given tab. Returns the number of matching fields. + * Pass null query to clear all highlights. + */ + protected int highlightFields (int tabIndex, String query) + { + Component comp = _oeditors.getComponentAt(tabIndex); + if (!(comp instanceof JScrollPane)) { + return 0; + } + Component view = ((JScrollPane)comp).getViewport().getView(); + if (!(view instanceof ObjectEditorPanel)) { + return 0; + } + + int matchCount = 0; + for (Component child : ((ObjectEditorPanel)view).getComponents()) { + if (!(child instanceof FieldEditor)) { + continue; + } + FieldEditor editor = (FieldEditor)child; + boolean matches = query != null && fieldMatches(editor, query); + editor.setOpaque(matches); + editor.setBackground(matches ? HIGHLIGHT_COLOR : null); + // Also highlight the label so the color is visible through the child components + editor._label.setOpaque(matches); + editor._label.setBackground(matches ? HIGHLIGHT_COLOR : null); + editor.repaint(); + if (matches) { + matchCount++; + } + } + return matchCount; + } + + /** Checks whether a field editor matches the search query by name or tooltip. */ + protected boolean fieldMatches (JPanel editor, String query) + { + if (editor instanceof FieldEditor) { + if (((FieldEditor)editor)._field.getName().toLowerCase().contains(query)) { + return true; + } + } + String tip = editor.getToolTipText(); + return tip != null && tip.toLowerCase().contains(query); + } + + /** Cycles to the next or previous matching tab. */ + protected void cycleMatch (int direction) + { + if (_matchingTabs.isEmpty()) { + return; + } + _matchCursor = (_matchCursor + direction + _matchingTabs.size()) % _matchingTabs.size(); + _oeditors.setSelectedIndex(_matchingTabs.get(_matchCursor)); + scrollToFirstMatch(); + } + + /** Scrolls the currently selected tab's viewport to show the first matching field. */ + protected void scrollToFirstMatch () + { + int tabIndex = _oeditors.getSelectedIndex(); + if (tabIndex < 0) { + return; + } + Component comp = _oeditors.getComponentAt(tabIndex); + if (!(comp instanceof JScrollPane)) { + return; + } + Component view = ((JScrollPane)comp).getViewport().getView(); + if (!(view instanceof ObjectEditorPanel)) { + return; + } + String query = _searchField.getText().trim().toLowerCase(); + if (query.isEmpty()) { + return; + } + ObjectEditorPanel opanel = (ObjectEditorPanel)view; + for (Component child : opanel.getComponents()) { + if (child instanceof FieldEditor && fieldMatches((FieldEditor)child, query)) { + opanel.scrollRectToVisible(child.getBounds()); + break; + } + } + } + /** Our client context. */ protected PresentsContext _ctx; @@ -136,4 +320,22 @@ public class ConfigEditorPanel extends JPanel /** Our default tab pane. */ protected String _defaultPane; + + /** Search field for filtering config tabs. */ + protected JTextField _searchField; + + /** Label showing the number of search matches. */ + protected JLabel _matchLabel; + + /** Indices of tabs that match the current search query. */ + protected List _matchingTabs = new ArrayList<>(); + + /** Current position in the matching tabs list for Enter/Shift+Enter cycling. */ + protected int _matchCursor; + + /** Color used for non-matching tabs. */ + protected static final Color MISMATCH_COLOR = Color.LIGHT_GRAY; + + /** Background color used for matching field editors. */ + protected static final Color HIGHLIGHT_COLOR = new Color(255, 255, 150); }