Add search to admin config
This commit is contained in:
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user