Add search to admin config

This commit is contained in:
fourbites
2026-02-17 10:14:15 +01:00
parent 669b93b55a
commit e949041aa0
@@ -5,15 +5,27 @@
package com.threerings.admin.client; package com.threerings.admin.client;
import java.awt.Color;
import java.awt.Component; 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.Comparator;
import java.util.List;
import javax.swing.AbstractAction;
import javax.swing.KeyStroke;
import javax.swing.JLabel; import javax.swing.JLabel;
import javax.swing.JPanel; import javax.swing.JPanel;
import javax.swing.JScrollPane; import javax.swing.JScrollPane;
import javax.swing.JTabbedPane; 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.util.QuickSort;
import com.samskivert.swing.HGroupLayout;
import com.samskivert.swing.VGroupLayout; import com.samskivert.swing.VGroupLayout;
import com.threerings.presents.util.PresentsContext; import com.threerings.presents.util.PresentsContext;
@@ -48,6 +60,55 @@ public class ConfigEditorPanel extends JPanel
setLayout(new VGroupLayout(VGroupLayout.STRETCH, VGroupLayout.STRETCH, setLayout(new VGroupLayout(VGroupLayout.STRETCH, VGroupLayout.STRETCH,
VGroupLayout.DEFAULT_GAP, VGroupLayout.CENTER)); 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 // create our objects tabbed pane
add(_oeditors = new JTabbedPane(JTabbedPane.LEFT)); add(_oeditors = new JTabbedPane(JTabbedPane.LEFT));
@@ -128,6 +189,129 @@ public class ConfigEditorPanel extends JPanel
log.warning("Failed to get config info", "reason", reason); 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. */ /** Our client context. */
protected PresentsContext _ctx; protected PresentsContext _ctx;
@@ -136,4 +320,22 @@ public class ConfigEditorPanel extends JPanel
/** Our default tab pane. */ /** Our default tab pane. */
protected String _defaultPane; 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<Integer> _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);
} }