Find clients and channels in the server tree
Ctrl+F in the tree opens a find bar below it that washes matching rows, steps the selection through them and can prune the tree down to the matches and the channels leading to them. ServerTreePanel becomes a panel holding the scroll pane and the bar. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -18,7 +18,9 @@ import java.awt.Rectangle;
|
|||||||
import java.awt.RenderingHints;
|
import java.awt.RenderingHints;
|
||||||
import java.awt.event.MouseAdapter;
|
import java.awt.event.MouseAdapter;
|
||||||
import java.awt.event.MouseEvent;
|
import java.awt.event.MouseEvent;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -41,6 +43,8 @@ final class DropIndicatorTree extends JTree {
|
|||||||
private TreePath highlight;
|
private TreePath highlight;
|
||||||
/** The row the pointer is over, or -1 when it is over none. */
|
/** The row the pointer is over, or -1 when it is over none. */
|
||||||
private int hoverRow = -1;
|
private int hoverRow = -1;
|
||||||
|
/** User objects of the rows a find bar matched, compared by identity. */
|
||||||
|
private Set<Object> searchHits = Collections.emptySet();
|
||||||
private Predicate<TreePath> pathEditable = path -> false;
|
private Predicate<TreePath> pathEditable = path -> false;
|
||||||
|
|
||||||
DropIndicatorTree(TreeModel treeModel, ServerModel model, GroupIcons groupIcons) {
|
DropIndicatorTree(TreeModel treeModel, ServerModel model, GroupIcons groupIcons) {
|
||||||
@@ -137,6 +141,11 @@ final class DropIndicatorTree extends JTree {
|
|||||||
super.repaint(tm, visible.x, y, visible.width, height);
|
super.repaint(tm, visible.x, y, visible.width, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setSearchHits(Set<Object> hits) {
|
||||||
|
searchHits = hits;
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
|
|
||||||
void highlightChannel(TreePath path) {
|
void highlightChannel(TreePath path) {
|
||||||
if (path == highlight || (path != null && path.equals(highlight))) return;
|
if (path == highlight || (path != null && path.equals(highlight))) return;
|
||||||
highlight = path;
|
highlight = path;
|
||||||
@@ -148,6 +157,7 @@ final class DropIndicatorTree extends JTree {
|
|||||||
Rectangle clip = g.getClipBounds();
|
Rectangle clip = g.getClipBounds();
|
||||||
g.setColor(getBackground());
|
g.setColor(getBackground());
|
||||||
g.fillRect(clip.x, clip.y, clip.width, clip.height);
|
g.fillRect(clip.x, clip.y, clip.width, clip.height);
|
||||||
|
paintSearchHits(g);
|
||||||
paintSelection(g);
|
paintSelection(g);
|
||||||
paintHover(g);
|
paintHover(g);
|
||||||
super.paintComponent(g);
|
super.paintComponent(g);
|
||||||
@@ -192,6 +202,20 @@ final class DropIndicatorTree extends JTree {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void paintSearchHits(Graphics g) {
|
||||||
|
if (searchHits.isEmpty()) return;
|
||||||
|
Rectangle visible = getVisibleRect();
|
||||||
|
int first = Math.max(0, getClosestRowForLocation(visible.x, visible.y));
|
||||||
|
g.setColor(Theme.searchMatch());
|
||||||
|
for (int row = first; row < getRowCount(); row++) {
|
||||||
|
Rectangle bounds = getRowBounds(row);
|
||||||
|
if (bounds == null) continue;
|
||||||
|
if (bounds.y > visible.y + visible.height) break;
|
||||||
|
Object obj = ((DefaultMutableTreeNode) getPathForRow(row).getLastPathComponent()).getUserObject();
|
||||||
|
if (searchHits.contains(obj)) g.fillRect(visible.x, bounds.y, visible.width, bounds.height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Washes the row under the pointer across the full width it occupies — the same
|
* Washes the row under the pointer across the full width it occupies — the same
|
||||||
* area a click on it acts on. A selected row keeps its selection colour, and
|
* area a click on it acts on. A selected row keeps its selection colour, and
|
||||||
|
|||||||
@@ -10,15 +10,20 @@ import com.ts3client.net.ClientEntry;
|
|||||||
import com.ts3client.net.Group;
|
import com.ts3client.net.Group;
|
||||||
import com.ts3client.net.ServerModel;
|
import com.ts3client.net.ServerModel;
|
||||||
|
|
||||||
|
import javax.swing.AbstractAction;
|
||||||
import javax.swing.DropMode;
|
import javax.swing.DropMode;
|
||||||
|
import javax.swing.JPanel;
|
||||||
import javax.swing.JScrollPane;
|
import javax.swing.JScrollPane;
|
||||||
import javax.swing.JViewport;
|
import javax.swing.JViewport;
|
||||||
|
import javax.swing.KeyStroke;
|
||||||
import javax.swing.SwingUtilities;
|
import javax.swing.SwingUtilities;
|
||||||
import javax.swing.plaf.basic.BasicTreeUI;
|
import javax.swing.plaf.basic.BasicTreeUI;
|
||||||
import javax.swing.tree.DefaultMutableTreeNode;
|
import javax.swing.tree.DefaultMutableTreeNode;
|
||||||
import javax.swing.tree.DefaultTreeModel;
|
import javax.swing.tree.DefaultTreeModel;
|
||||||
import javax.swing.tree.TreePath;
|
import javax.swing.tree.TreePath;
|
||||||
|
import java.awt.BorderLayout;
|
||||||
import java.awt.Rectangle;
|
import java.awt.Rectangle;
|
||||||
|
import java.awt.event.ActionEvent;
|
||||||
import java.awt.event.MouseAdapter;
|
import java.awt.event.MouseAdapter;
|
||||||
import java.awt.event.MouseEvent;
|
import java.awt.event.MouseEvent;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -27,7 +32,7 @@ import java.util.List;
|
|||||||
* The server view: a tree of channels each containing its clients, styled to
|
* The server view: a tree of channels each containing its clients, styled to
|
||||||
* resemble the TeamSpeak 3 client. Talk state colours clients green live.
|
* resemble the TeamSpeak 3 client. Talk state colours clients green live.
|
||||||
*/
|
*/
|
||||||
public final class ServerTreePanel extends JScrollPane {
|
public final class ServerTreePanel extends JPanel {
|
||||||
|
|
||||||
/** Actions the tree can request of the controller. */
|
/** Actions the tree can request of the controller. */
|
||||||
public interface Actions {
|
public interface Actions {
|
||||||
@@ -127,6 +132,16 @@ public final class ServerTreePanel extends JScrollPane {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private final DropIndicatorTree tree;
|
private final DropIndicatorTree tree;
|
||||||
|
private final JScrollPane scroll = new JScrollPane() {
|
||||||
|
@Override
|
||||||
|
public void updateUI() {
|
||||||
|
super.updateUI();
|
||||||
|
// The rows stop at the last channel, so the space below them belongs to the tree
|
||||||
|
// rather than to the window behind it.
|
||||||
|
if (getViewport() != null) getViewport().setBackground(Theme.treeBg());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
private final ServerTreeSearch search;
|
||||||
private final DefaultMutableTreeNode root = new DefaultMutableTreeNode();
|
private final DefaultMutableTreeNode root = new DefaultMutableTreeNode();
|
||||||
/**
|
/**
|
||||||
* The nodes only mirror the connection's model, so an edited row is not written
|
* The nodes only mirror the connection's model, so an edited row is not written
|
||||||
@@ -154,6 +169,7 @@ public final class ServerTreePanel extends JScrollPane {
|
|||||||
|
|
||||||
public ServerTreePanel(ServerModel model, GroupIcons groupIcons, ContactStore contacts, Settings settings,
|
public ServerTreePanel(ServerModel model, GroupIcons groupIcons, ContactStore contacts, Settings settings,
|
||||||
Actions actions) {
|
Actions actions) {
|
||||||
|
super(new BorderLayout());
|
||||||
this.model = model;
|
this.model = model;
|
||||||
this.groupIcons = groupIcons;
|
this.groupIcons = groupIcons;
|
||||||
this.actions = actions;
|
this.actions = actions;
|
||||||
@@ -182,13 +198,26 @@ public final class ServerTreePanel extends JScrollPane {
|
|||||||
tree.setEditable(true);
|
tree.setEditable(true);
|
||||||
tree.setPathEditable(nicknameEditor::editsPath);
|
tree.setPathEditable(nicknameEditor::editsPath);
|
||||||
tree.setInvokesStopCellEditing(true);
|
tree.setInvokesStopCellEditing(true);
|
||||||
setViewportView(tree);
|
scroll.setViewportView(tree);
|
||||||
// The icon strip is drawn against the viewport's right edge, so the blitted
|
// The icon strip is drawn against the viewport's right edge, so the blitted
|
||||||
// pixels a scroll would reuse are stale; repaint the whole viewport instead.
|
// pixels a scroll would reuse are stale; repaint the whole viewport instead.
|
||||||
getViewport().setScrollMode(JViewport.SIMPLE_SCROLL_MODE);
|
scroll.getViewport().setScrollMode(JViewport.SIMPLE_SCROLL_MODE);
|
||||||
// Scrolling slides the rows past a pointer that has not moved, so the outlined
|
// Scrolling slides the rows past a pointer that has not moved, so the outlined
|
||||||
// row has to be worked out again.
|
// row has to be worked out again.
|
||||||
getViewport().addChangeListener(e -> tree.refreshHoverRow());
|
scroll.getViewport().addChangeListener(e -> tree.refreshHoverRow());
|
||||||
|
scroll.getViewport().setBackground(Theme.treeBg());
|
||||||
|
add(scroll, BorderLayout.CENTER);
|
||||||
|
|
||||||
|
search = new ServerTreeSearch(tree, root, contacts, this::rebuild);
|
||||||
|
add(search.bar(), BorderLayout.SOUTH);
|
||||||
|
String find = "ts3-tree-find";
|
||||||
|
getInputMap(WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke("control F"), find);
|
||||||
|
getActionMap().put(find, new AbstractAction() {
|
||||||
|
@Override
|
||||||
|
public void actionPerformed(ActionEvent e) {
|
||||||
|
openFind();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Within the tree a drag moves the client or channel; dropped elsewhere it
|
// Within the tree a drag moves the client or channel; dropped elsewhere it
|
||||||
// yields the TS3 link BBCode, which the chat input accepts as plain text.
|
// yields the TS3 link BBCode, which the chat input accepts as plain text.
|
||||||
@@ -253,6 +282,11 @@ public final class ServerTreePanel extends JScrollPane {
|
|||||||
tree.setSelectionPath(path);
|
tree.setSelectionPath(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Opens the find bar above the tree. */
|
||||||
|
public void openFind() {
|
||||||
|
search.open();
|
||||||
|
}
|
||||||
|
|
||||||
public void setSelfClientId(int id) {
|
public void setSelfClientId(int id) {
|
||||||
this.selfClientId = id;
|
this.selfClientId = id;
|
||||||
}
|
}
|
||||||
@@ -300,14 +334,6 @@ public final class ServerTreePanel extends JScrollPane {
|
|||||||
ChannelMenu.build(channel, actions).show(tree, e.getX(), e.getY());
|
ChannelMenu.build(channel, actions).show(tree, e.getX(), e.getY());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void updateUI() {
|
|
||||||
super.updateUI();
|
|
||||||
// The rows stop at the last channel, so the space below them belongs to the tree
|
|
||||||
// rather than to the window behind it.
|
|
||||||
if (getViewport() != null) getViewport().setBackground(Theme.treeBg());
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The path of the tree node showing {@code target}, or {@code null}. */
|
/** The path of the tree node showing {@code target}, or {@code null}. */
|
||||||
private TreePath pathOf(Object target) {
|
private TreePath pathOf(Object target) {
|
||||||
java.util.Enumeration<?> nodes = root.breadthFirstEnumeration();
|
java.util.Enumeration<?> nodes = root.breadthFirstEnumeration();
|
||||||
@@ -332,8 +358,10 @@ public final class ServerTreePanel extends JScrollPane {
|
|||||||
root.setUserObject(model.getServerName());
|
root.setUserObject(model.getServerName());
|
||||||
root.removeAllChildren();
|
root.removeAllChildren();
|
||||||
List<ChannelNode> roots = model.buildTree();
|
List<ChannelNode> roots = model.buildTree();
|
||||||
|
boolean filtering = search.isFiltering();
|
||||||
for (ChannelNode c : roots) {
|
for (ChannelNode c : roots) {
|
||||||
root.add(buildChannel(c));
|
DefaultMutableTreeNode node = buildChannel(c, filtering);
|
||||||
|
if (node != null) root.add(node);
|
||||||
}
|
}
|
||||||
treeModel.reload();
|
treeModel.reload();
|
||||||
for (int i = 0; i < tree.getRowCount(); i++) {
|
for (int i = 0; i < tree.getRowCount(); i++) {
|
||||||
@@ -350,6 +378,7 @@ public final class ServerTreePanel extends JScrollPane {
|
|||||||
// (e.g. the previously selected channel/client is gone), since it already knows the rest.
|
// (e.g. the previously selected channel/client is gone), since it already knows the rest.
|
||||||
Object nowSelected = selectedUserObject();
|
Object nowSelected = selectedUserObject();
|
||||||
if (nowSelected != selected) actions.onSelectionChanged(nowSelected);
|
if (nowSelected != selected) actions.onSelectionChanged(nowSelected);
|
||||||
|
search.treeRebuilt();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Object selectedUserObject() {
|
private Object selectedUserObject() {
|
||||||
@@ -357,15 +386,22 @@ public final class ServerTreePanel extends JScrollPane {
|
|||||||
return path == null ? null : ((DefaultMutableTreeNode) path.getLastPathComponent()).getUserObject();
|
return path == null ? null : ((DefaultMutableTreeNode) path.getLastPathComponent()).getUserObject();
|
||||||
}
|
}
|
||||||
|
|
||||||
private DefaultMutableTreeNode buildChannel(ChannelNode c) {
|
/**
|
||||||
|
* @param filtering keep only what the find bar matched: a matching channel with its
|
||||||
|
* clients, a matching client, and the channels leading to either
|
||||||
|
* @return the channel's node, or {@code null} when filtering left nothing of it
|
||||||
|
*/
|
||||||
|
private DefaultMutableTreeNode buildChannel(ChannelNode c, boolean filtering) {
|
||||||
DefaultMutableTreeNode node = new DefaultMutableTreeNode(c);
|
DefaultMutableTreeNode node = new DefaultMutableTreeNode(c);
|
||||||
|
boolean channelMatches = filtering && search.matches(c);
|
||||||
for (ClientEntry client : c.clients) {
|
for (ClientEntry client : c.clients) {
|
||||||
node.add(new DefaultMutableTreeNode(client));
|
if (!filtering || channelMatches || search.matches(client)) node.add(new DefaultMutableTreeNode(client));
|
||||||
}
|
}
|
||||||
for (ChannelNode child : c.children) {
|
for (ChannelNode child : c.children) {
|
||||||
node.add(buildChannel(child));
|
DefaultMutableTreeNode childNode = buildChannel(child, filtering);
|
||||||
|
if (childNode != null) node.add(childNode);
|
||||||
}
|
}
|
||||||
return node;
|
return !filtering || channelMatches || node.getChildCount() > 0 ? node : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Clears the tree back to the disconnected placeholder state. */
|
/** Clears the tree back to the disconnected placeholder state. */
|
||||||
@@ -373,6 +409,7 @@ public final class ServerTreePanel extends JScrollPane {
|
|||||||
root.setUserObject("Not connected");
|
root.setUserObject("Not connected");
|
||||||
root.removeAllChildren();
|
root.removeAllChildren();
|
||||||
treeModel.reload();
|
treeModel.reload();
|
||||||
|
search.treeRebuilt();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Repaint only (e.g. talk-state changes) without rebuilding structure. */
|
/** Repaint only (e.g. talk-state changes) without rebuilding structure. */
|
||||||
|
|||||||
@@ -0,0 +1,237 @@
|
|||||||
|
package com.ts3client.ui;
|
||||||
|
|
||||||
|
import com.ts3client.contacts.Contact;
|
||||||
|
import com.ts3client.contacts.ContactStore;
|
||||||
|
import com.ts3client.net.ChannelNode;
|
||||||
|
import com.ts3client.net.ClientEntry;
|
||||||
|
import com.ts3client.text.TextQuery;
|
||||||
|
|
||||||
|
import javax.swing.Icon;
|
||||||
|
import javax.swing.JButton;
|
||||||
|
import javax.swing.JToggleButton;
|
||||||
|
import javax.swing.tree.DefaultMutableTreeNode;
|
||||||
|
import javax.swing.tree.TreePath;
|
||||||
|
import java.awt.Dimension;
|
||||||
|
import java.awt.Insets;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Enumeration;
|
||||||
|
import java.util.IdentityHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find in the server tree: looks for clients or for channels by name, washes every
|
||||||
|
* matching row and selects them one at a time. Optionally prunes the tree down to
|
||||||
|
* the matches (and the channels leading to them).
|
||||||
|
*/
|
||||||
|
final class ServerTreeSearch implements FindBar.Listener {
|
||||||
|
|
||||||
|
private final DropIndicatorTree tree;
|
||||||
|
private final DefaultMutableTreeNode root;
|
||||||
|
private final ContactStore contacts;
|
||||||
|
/** Rebuilds the tree, which asks {@link #isFiltering()} and {@link #matches} what to keep. */
|
||||||
|
private final Runnable rebuild;
|
||||||
|
|
||||||
|
private final FindBar bar = new FindBar(this);
|
||||||
|
private boolean searchChannels;
|
||||||
|
/** Switches between finding clients and finding channels, showing the kind it finds. */
|
||||||
|
private final JButton scope = new JButton() {
|
||||||
|
@Override
|
||||||
|
public Icon getIcon() {
|
||||||
|
return searchChannels ? Icons.channel(true) : Icons.clientIdle();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sized for the wider of both icons, so the field beside it stays put on a switch. */
|
||||||
|
@Override
|
||||||
|
public Dimension getPreferredSize() {
|
||||||
|
Icon client = Icons.clientIdle();
|
||||||
|
Icon channel = Icons.channel(true);
|
||||||
|
Insets in = getInsets();
|
||||||
|
return new Dimension(Math.max(client.getIconWidth(), channel.getIconWidth()) + in.left + in.right,
|
||||||
|
Math.max(client.getIconHeight(), channel.getIconHeight()) + in.top + in.bottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Dimension getMaximumSize() {
|
||||||
|
return getPreferredSize();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
private final JToggleButton onlyMatching;
|
||||||
|
|
||||||
|
private TextQuery query = TextQuery.EMPTY;
|
||||||
|
private boolean filtering;
|
||||||
|
/** Matching rows in tree order. */
|
||||||
|
private final List<DefaultMutableTreeNode> hits = new ArrayList<>();
|
||||||
|
|
||||||
|
ServerTreeSearch(DropIndicatorTree tree, DefaultMutableTreeNode root, ContactStore contacts, Runnable rebuild) {
|
||||||
|
this.tree = tree;
|
||||||
|
this.root = root;
|
||||||
|
this.contacts = contacts;
|
||||||
|
this.rebuild = rebuild;
|
||||||
|
scope.addActionListener(e -> {
|
||||||
|
searchChannels = !searchChannels;
|
||||||
|
scopeChanged();
|
||||||
|
});
|
||||||
|
bar.addLeading(scope);
|
||||||
|
onlyMatching = bar.addFilterToggle("Show only matches");
|
||||||
|
bar.addResettable(() -> !searchChannels, () -> {
|
||||||
|
searchChannels = false;
|
||||||
|
showScope();
|
||||||
|
});
|
||||||
|
showScope();
|
||||||
|
}
|
||||||
|
|
||||||
|
FindBar bar() {
|
||||||
|
return bar;
|
||||||
|
}
|
||||||
|
|
||||||
|
void open() {
|
||||||
|
bar.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Whether the tree should hold only matches and the channels leading to them. */
|
||||||
|
boolean isFiltering() {
|
||||||
|
return filtering;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Whether a channel or client row matches the current query in the current scope. */
|
||||||
|
boolean matches(Object node) {
|
||||||
|
if (query.isEmpty()) return false;
|
||||||
|
if (node instanceof ClientEntry c) {
|
||||||
|
if (searchChannels) return false;
|
||||||
|
Contact contact = contacts.find(c.uniqueId);
|
||||||
|
return query.matches(c.nickname) || (contact != null && query.matches(contact.displayName(c.nickname)));
|
||||||
|
}
|
||||||
|
if (node instanceof ChannelNode ch) {
|
||||||
|
if (!searchChannels) return false;
|
||||||
|
Spacers.Spacer spacer = Spacers.parse(ch.name);
|
||||||
|
if (spacer == null) return query.matches(ch.name);
|
||||||
|
return spacer.align != '*' && query.matches(spacer.caption);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The tree was rebuilt from the model: its rows (and so the matches) may have changed. */
|
||||||
|
void treeRebuilt() {
|
||||||
|
if (!bar.isOpen()) return;
|
||||||
|
collectHits();
|
||||||
|
updateStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void scopeChanged() {
|
||||||
|
showScope();
|
||||||
|
if (bar.isOpen()) bar.fireQueryChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showScope() {
|
||||||
|
scope.setToolTipText(searchChannels ? "Finding channels — click to find clients"
|
||||||
|
: "Finding clients — click to find channels");
|
||||||
|
scope.repaint();
|
||||||
|
bar.setPlaceholder(searchChannels ? "Find channels" : "Find clients");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void queryChanged(TextQuery q) {
|
||||||
|
query = q.isInvalid() ? TextQuery.EMPTY : q;
|
||||||
|
boolean filter = onlyMatching.isSelected() && !query.isEmpty();
|
||||||
|
if (filter || filtering) {
|
||||||
|
filtering = filter;
|
||||||
|
rebuild.run();
|
||||||
|
}
|
||||||
|
collectHits();
|
||||||
|
if (!hits.isEmpty()) select(hits.get(0));
|
||||||
|
updateStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void next() {
|
||||||
|
step(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void previous() {
|
||||||
|
step(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void closed() {
|
||||||
|
query = TextQuery.EMPTY;
|
||||||
|
hits.clear();
|
||||||
|
tree.setSearchHits(Collections.emptySet());
|
||||||
|
if (filtering) {
|
||||||
|
filtering = false;
|
||||||
|
rebuild.run();
|
||||||
|
}
|
||||||
|
TreePath selected = tree.getSelectionPath();
|
||||||
|
if (selected != null) tree.scrollPathToVisible(selected);
|
||||||
|
tree.requestFocusInWindow();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Moves to the match after (or before) the selected row, wrapping around. */
|
||||||
|
private void step(boolean forward) {
|
||||||
|
if (hits.isEmpty()) return;
|
||||||
|
List<DefaultMutableTreeNode> order = preorder();
|
||||||
|
int at = order.indexOf(selectedNode());
|
||||||
|
DefaultMutableTreeNode target = null;
|
||||||
|
if (forward) {
|
||||||
|
for (DefaultMutableTreeNode hit : hits) {
|
||||||
|
if (order.indexOf(hit) > at) {
|
||||||
|
target = hit;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (target == null) target = hits.get(0);
|
||||||
|
} else {
|
||||||
|
for (int i = hits.size() - 1; i >= 0; i--) {
|
||||||
|
if (at < 0 || order.indexOf(hits.get(i)) < at) {
|
||||||
|
target = hits.get(i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (target == null) target = hits.get(hits.size() - 1);
|
||||||
|
}
|
||||||
|
select(target);
|
||||||
|
updateStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void select(DefaultMutableTreeNode node) {
|
||||||
|
TreePath path = new TreePath(node.getPath());
|
||||||
|
tree.setSelectionPath(path);
|
||||||
|
tree.scrollPathToVisible(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void collectHits() {
|
||||||
|
hits.clear();
|
||||||
|
Set<Object> washed = Collections.newSetFromMap(new IdentityHashMap<>());
|
||||||
|
for (DefaultMutableTreeNode n : preorder()) {
|
||||||
|
if (matches(n.getUserObject())) {
|
||||||
|
hits.add(n);
|
||||||
|
washed.add(n.getUserObject());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tree.setSearchHits(washed);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateStatus() {
|
||||||
|
if (query.isEmpty()) {
|
||||||
|
if (!bar.query().isInvalid()) bar.clearCount();
|
||||||
|
bar.setNavigable(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bar.setNavigable(hits.size() > 1 || (hits.size() == 1 && hits.get(0) != selectedNode()));
|
||||||
|
bar.setCount(hits.indexOf(selectedNode()) + 1, hits.size(), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private DefaultMutableTreeNode selectedNode() {
|
||||||
|
TreePath path = tree.getSelectionPath();
|
||||||
|
return path == null ? null : (DefaultMutableTreeNode) path.getLastPathComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<DefaultMutableTreeNode> preorder() {
|
||||||
|
List<DefaultMutableTreeNode> out = new ArrayList<>();
|
||||||
|
Enumeration<?> e = root.preorderEnumeration();
|
||||||
|
while (e.hasMoreElements()) out.add((DefaultMutableTreeNode) e.nextElement());
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user