Skip to content

Commit e9cacbf

Browse files
committed
Make OntologyBrowser a standalone resource
1 parent fa87a5a commit e9cacbf

4 files changed

Lines changed: 301 additions & 69 deletions

File tree

‎src/main/java/sc/fiji/snt/SNTUI.java‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ public class SNTUI extends JDialog {
137137

138138
private final PlausibilityMonitor plausibilityMonitor;
139139
private final CurationManager curationManager;
140+
private JDialog ontologyBrowserDialog;
140141

141142
SNT plugin;
142143
private PathAndFillManager pathAndFillManager;
@@ -3320,9 +3321,23 @@ private JMenu utilitiesMenu() {
33203321
ScriptRecorder.setRecordingCall(figureGenerator, "snt.getUI().runCommand(\"Render Illustrations...\")");
33213322
utilitiesMenu.add(figureGenerator);
33223323
utilitiesMenu.add(getRecPlotterMenuItem());
3324+
utilitiesMenu.addSeparator();
3325+
final JMenuItem ontologyItem = new JMenuItem("Ontology Browser...", IconFactory.menuIcon(GLYPH.ATLAS));
3326+
ontologyItem.addActionListener(e -> showOntologyBrowser());
3327+
utilitiesMenu.add(ontologyItem);
33233328
return utilitiesMenu;
33243329
}
33253330

3331+
private void showOntologyBrowser() {
3332+
if (ontologyBrowserDialog != null && ontologyBrowserDialog.isVisible()) {
3333+
ontologyBrowserDialog.toFront();
3334+
return;
3335+
}
3336+
final OntologyBrowser browser = new OntologyBrowser();
3337+
browser.addAllOntologies();
3338+
ontologyBrowserDialog = browser.showModelessDialog(this, "Ontology Browser");
3339+
}
3340+
33263341
private JMenuItem compareReconstructionsMenuItem() {
33273342
final JMenuItem menuItem = new JMenuItem("Compare Arbors/Cell Groups...");
33283343
menuItem.setToolTipText("Statistical comparisons (t-test/ANOVA) between cell groups " +

‎src/main/java/sc/fiji/snt/gui/OntologyBrowser.java‎

Lines changed: 275 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
import javax.swing.text.Position;
3535
import javax.swing.tree.*;
3636
import java.awt.*;
37+
import java.awt.datatransfer.StringSelection;
38+
import java.awt.event.KeyEvent;
3739
import java.io.Serial;
3840
import java.util.ArrayList;
3941
import java.util.Enumeration;
@@ -68,6 +70,15 @@ public class OntologyBrowser extends JPanel {
6870

6971
private static Point lastDialogLocation;
7072

73+
/**
74+
* When {@code true}, typing in the search bar collapses the tree and
75+
* expands only the paths that lead to matching nodes (filter-like UX).
76+
* When {@code false}, the default JIDE behavior is used (scroll to match).
77+
* This is a global flag so it can be toggled easily if behavior becomes
78+
* problematic.
79+
*/
80+
static boolean FILTER_SEARCH = true;
81+
7182
private final JTabbedPane tabbedPane;
7283
private final List<OntologyTab> tabs;
7384
private final List<Consumer<List<String>>> selectionListeners;
@@ -113,6 +124,9 @@ public OntologyTab addOntology(final String tabLabel, final DefaultTreeModel mod
113124
tabbedPane.addTab(tabLabel, tab);
114125
// Hide tab strip when only one ontology is loaded
115126
tabbedPane.putClientProperty("JTabbedPane.tabsVisible", tabs.size() > 1);
127+
// Install default context menu; callers can replace or extend via
128+
// tab.createDefaultPopupMenu() + tab.setPopupMenu()
129+
tab.setPopupMenu(tab.createDefaultPopupMenu());
116130
return tab;
117131
}
118132

@@ -137,9 +151,23 @@ public OntologyTab addAllenCCFOntology(final boolean meshesOnly) {
137151
final OntologyTab tab = addOntology("Allen CCFv" + AllenUtils.VERSION, AllenUtils.getTreeModel(meshesOnly));
138152
tab.setCellRenderer(new AllenCCFRenderer());
139153
tab.setStatusLabelPlaceholder("CCFv" + AllenUtils.VERSION);
154+
// Extend default popup with Allen-specific atlas links
155+
final JPopupMenu pMenu = tab.createDefaultPopupMenu();
156+
pMenu.addSeparator();
157+
pMenu.add(GuiUtils.MenuItems.openHelpURL("Online 2D Atlas Viewer", "https://atlas.brain-map.org/atlas?atlas=602630314"));
158+
pMenu.add(GuiUtils.MenuItems.openHelpURL("Online 3D Atlas Viewer", "https://connectivity.brain-map.org/3d-viewer"));
159+
tab.setPopupMenu(pMenu);
140160
return tab;
141161
}
142162

163+
/**
164+
* Adds all the default ontologies as tabs.
165+
*/
166+
public void addAllOntologies() {
167+
addAllenCCFOntology();
168+
addDrosophilaOntology();
169+
}
170+
143171
/**
144172
* Adds the Drosophila adult brain ontology (FBbt) as a tab.
145173
*
@@ -149,6 +177,11 @@ public OntologyTab addDrosophilaOntology() {
149177
final OntologyTab tab = addOntology("Drosophila FBbt", DrosophilaUtils.getTreeModel());
150178
tab.setCellRenderer(new NoIconRenderer());
151179
tab.setStatusLabelPlaceholder("FBbt");
180+
final JPopupMenu pMenu = tab.createDefaultPopupMenu();
181+
pMenu.addSeparator();
182+
pMenu.add(GuiUtils.MenuItems.openHelpURL("Online VFB Viewer",
183+
"https://v2.virtualflybrain.org/org.geppetto.frontend/geppetto?id=VFB_00101567&i=VFB_00101567"));
184+
tab.setPopupMenu(pMenu);
152185
return tab;
153186
}
154187

@@ -390,7 +423,7 @@ public List<String> showDialog(final Component parent, final String title) {
390423
// Escape key closes the dialog
391424
dialog.getRootPane().registerKeyboardAction(
392425
e -> dialog.dispose(),
393-
KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_ESCAPE, 0),
426+
KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0),
394427
JComponent.WHEN_IN_FOCUSED_WINDOW);
395428

396429
// Place buttons below the tabbed pane (below the tab strip)
@@ -415,6 +448,131 @@ public List<String> showDialog(final Component parent, final String title) {
415448
return null;
416449
}
417450

451+
/**
452+
* Shows this browser in a modeless (non-modal) dialog for reference
453+
* browsing. The dialog stays open while the user works in other windows.
454+
* Includes an info button and a copy-to-clipboard button.
455+
*
456+
* @param parent the parent component for initial positioning
457+
* @param title the dialog title
458+
* @return the modeless {@link JDialog}
459+
*/
460+
public JDialog showModelessDialog(final Component parent, final String title) {
461+
final Window window = (parent == null) ? null : SwingUtilities.getWindowAncestor(parent);
462+
final JDialog dialog = new JDialog(window, title);
463+
dialog.getRootPane().putClientProperty("Window.style", "small");
464+
dialog.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
465+
final JPanel buttonPanel = new JPanel(new GridLayout(1, 2));
466+
final JButton infoButton = new JButton(IconFactory.buttonIcon(IconFactory.GLYPH.INFO, 1f));
467+
infoButton.setToolTipText("Info on checked compartments");
468+
infoButton.addActionListener(e -> showSelectionInfo(dialog));
469+
buttonPanel.add(infoButton);
470+
final JButton copyButton = new JButton(IconFactory.buttonIcon(IconFactory.GLYPH.COPY, 1f));
471+
copyButton.setToolTipText("Copy checked terms to clipboard");
472+
copyButton.addActionListener(e -> copySelectionToClipboard());
473+
buttonPanel.add(copyButton);
474+
475+
// Escape key closes the dialog
476+
dialog.getRootPane().registerKeyboardAction(
477+
e -> dialog.dispose(),
478+
KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0),
479+
JComponent.WHEN_IN_FOCUSED_WINDOW);
480+
481+
final JPanel wrapper = new JPanel(new BorderLayout());
482+
wrapper.add(this, BorderLayout.CENTER);
483+
wrapper.add(buttonPanel, BorderLayout.SOUTH);
484+
dialog.setContentPane(wrapper);
485+
486+
dialog.setPreferredSize(new java.awt.Dimension(350, 550));
487+
dialog.pack();
488+
if (lastDialogLocation != null) {
489+
dialog.setLocation(lastDialogLocation);
490+
} else {
491+
dialog.setLocationRelativeTo(parent);
492+
}
493+
dialog.addWindowListener(new java.awt.event.WindowAdapter() {
494+
@Override
495+
public void windowClosed(final java.awt.event.WindowEvent e) {
496+
lastDialogLocation = dialog.getLocation();
497+
}
498+
});
499+
dialog.setVisible(true);
500+
return dialog;
501+
}
502+
503+
/**
504+
* Displays an HTML info table for the currently checked compartments.
505+
* Works with any {@link BrainAnnotation} user objects (Allen CCF,
506+
* Drosophila FBbt, etc.).
507+
*
508+
* @param parent the parent component for the info dialog
509+
*/
510+
public void showSelectionInfo(final Component parent) {
511+
final List<BrainAnnotation> annotations =
512+
getCheckedUserObjects(BrainAnnotation.class);
513+
if (annotations.isEmpty()) {
514+
new GuiUtils(parent).error("There are no checked ontologies.");
515+
return;
516+
}
517+
annotations.sort((a, b) -> a.name().compareToIgnoreCase(b.name()));
518+
final StringBuilder sb = new StringBuilder("<header>");
519+
sb.append(" <style>");
520+
sb.append(" tr:nth-of-type(odd) {background-color:#ccc;}");
521+
sb.append(" </style>");
522+
sb.append("</header>");
523+
sb.append("<table>");
524+
sb.append("<tr>");
525+
sb.append("<th>Name</th><th>Acronym</th><th>Id</th>")
526+
.append("<th>Parent</th><th>Ontology depth</th><th>Alias(es)</th>");
527+
sb.append("</tr>");
528+
for (final BrainAnnotation a : annotations) {
529+
sb.append("<tr>");
530+
sb.append("<td style='text-align:left'>").append(a.name()).append("</td>");
531+
sb.append("<td style='text-align:center'>").append(a.acronym()).append("</td>");
532+
sb.append("<td style='text-align:center'>").append(a.id()).append("</td>");
533+
final BrainAnnotation par = a.getParent();
534+
sb.append("<td style='text-align:center'>")
535+
.append(par == null ? "-" : par.toString()).append("</td>");
536+
sb.append("<td style='text-align:center'>")
537+
.append(a.getOntologyDepth()).append("</td>");
538+
final String[] aliases = a.aliases();
539+
sb.append("<td style='text-align:center'>")
540+
.append(aliases == null ? "" : String.join(", ", aliases)).append("</td>");
541+
sb.append("</tr>");
542+
}
543+
sb.append("</table>");
544+
new GuiUtils(parent).showHTMLDialog(sb.toString(),
545+
"Info On Selected Compartments", false);
546+
}
547+
548+
/**
549+
* Copies the checked terms to the system clipboard. Each term is placed
550+
* on its own line, formatted as {@code "acronym (id): name"}.
551+
*/
552+
public void copySelectionToClipboard() {
553+
final List<BrainAnnotation> annotations =
554+
getCheckedUserObjects(BrainAnnotation.class);
555+
if (annotations.isEmpty()) {
556+
// Fall back to string-based checked leaves for non-BrainAnnotation trees
557+
final List<String> leaves = getSelectedLeaves();
558+
if (leaves.isEmpty()) return;
559+
final String text = String.join("\n", leaves);
560+
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(text), null);
561+
return;
562+
}
563+
final StringBuilder sb = new StringBuilder();
564+
for (final BrainAnnotation a : annotations) {
565+
if (!sb.isEmpty()) sb.append("\n");
566+
final String acr = a.acronym();
567+
if (acr != null && !acr.isEmpty()) {
568+
sb.append(acr).append(" (").append(a.id()).append("): ");
569+
}
570+
sb.append(a.name());
571+
}
572+
Toolkit.getDefaultToolkit().getSystemClipboard()
573+
.setContents(new StringSelection(sb.toString()), null);
574+
}
575+
418576
/**
419577
* Serializes a {@link TreePath} as a {@value #SEPARATOR}-delimited string,
420578
* excluding the root node.
@@ -461,6 +619,37 @@ public class OntologyTab extends JPanel {
461619
searchableBar.setVisibleButtons(SNTSearchableBar.SHOW_NAVIGATION | SNTSearchableBar.SHOW_HIGHLIGHTS |
462620
SNTSearchableBar.SHOW_STATUS);
463621

622+
// Filter-search: collapse all, then expand only paths to matches.
623+
// Listener is always installed; the FILTER_SEARCH flag is checked
624+
// at runtime so the gear-menu toggle takes effect immediately.
625+
searchableBar.getSearchField().getDocument().addDocumentListener(new javax.swing.event.DocumentListener() {
626+
@Override
627+
public void insertUpdate(final javax.swing.event.DocumentEvent e) { filterTree(); }
628+
@Override
629+
public void removeUpdate(final javax.swing.event.DocumentEvent e) { filterTree(); }
630+
@Override
631+
public void changedUpdate(final javax.swing.event.DocumentEvent e) { filterTree(); }
632+
private void filterTree() {
633+
if (!FILTER_SEARCH) return;
634+
// Run after JIDE's TreeSearchable handler to avoid
635+
// it re-expanding nodes we just collapsed
636+
SwingUtilities.invokeLater(() -> {
637+
final String text = searchableBar.getSearchField().getText();
638+
if (text == null || text.isBlank()) {
639+
// Restore default expansion
640+
GuiUtils.JTrees.collapseAllNodes(tree);
641+
GuiUtils.JTrees.expandToLevel(tree, 3);
642+
} else {
643+
revealMatchingNodes(text);
644+
}
645+
});
646+
}
647+
});
648+
final JCheckBoxMenuItem filterSearchItem = new JCheckBoxMenuItem("Filter Tree on Search", FILTER_SEARCH);
649+
filterSearchItem.setToolTipText("Collapse non-matching branches while searching");
650+
filterSearchItem.addItemListener(e -> FILTER_SEARCH = filterSearchItem.isSelected());
651+
searchableBar.addOptionsMenuItem(filterSearchItem);
652+
464653
// Single-selection: deselect previous choice across all tabs
465654
if (singleSelection) {
466655
tree.getCheckBoxTreeSelectionModel().setSelectionMode(
@@ -630,6 +819,91 @@ public void repaintTree() {
630819
tree.repaint();
631820
}
632821

822+
/**
823+
* Collapses the entire tree, then expands only the ancestor paths of
824+
* nodes whose {@code toString()} contains the search text
825+
* (case-insensitive). Called from the filter-search DocumentListener.
826+
*/
827+
private void revealMatchingNodes(final String text) {
828+
final String lower = text.toLowerCase();
829+
final DefaultMutableTreeNode root =
830+
(DefaultMutableTreeNode) tree.getModel().getRoot();
831+
// Collect ancestor paths of matching leaves
832+
final List<TreePath> pathsToExpand = new ArrayList<>();
833+
final Enumeration<TreeNode> e = root.depthFirstEnumeration();
834+
while (e.hasMoreElements()) {
835+
final DefaultMutableTreeNode node =
836+
(DefaultMutableTreeNode) e.nextElement();
837+
if (node.getUserObject().toString().toLowerCase().contains(lower)) {
838+
// Expand up to the parent so the matching node is visible
839+
final TreeNode[] ancestors = node.getPath();
840+
if (ancestors.length > 1) {
841+
pathsToExpand.add(new TreePath(
842+
((DefaultMutableTreeNode) ancestors[ancestors.length - 2]).getPath()));
843+
}
844+
}
845+
}
846+
GuiUtils.JTrees.collapseAllNodes(tree);
847+
for (final TreePath p : pathsToExpand) {
848+
tree.expandPath(p);
849+
}
850+
// Scroll to first match if any
851+
if (!pathsToExpand.isEmpty()) {
852+
tree.scrollPathToVisible(pathsToExpand.getFirst());
853+
}
854+
}
855+
856+
/**
857+
* Creates the default popup menu with generic tree navigation actions:
858+
* Deselect All, Uncheck All, Collapse/Expand All, Collapse/Expand
859+
* Selected Level, and Auto-select Children toggle. Callers can append tab-specific items
860+
* to the returned menu before installing it via {@link #setPopupMenu(JPopupMenu)}.
861+
*
862+
* @return a new popup menu with standard tree controls
863+
*/
864+
public JPopupMenu createDefaultPopupMenu() {
865+
final JPopupMenu pMenu = new JPopupMenu();
866+
JMenuItem jmi = new JMenuItem("Deselect All");
867+
jmi.addActionListener(e -> tree.clearSelection());
868+
pMenu.add(jmi);
869+
jmi = new JMenuItem("Uncheck All");
870+
jmi.addActionListener(e -> tree.getCheckBoxTreeSelectionModel().clearSelection());
871+
pMenu.add(jmi);
872+
pMenu.addSeparator();
873+
jmi = new JMenuItem("Collapse All");
874+
jmi.addActionListener(e -> GuiUtils.JTrees.collapseAllNodes(tree));
875+
pMenu.add(jmi);
876+
jmi = new JMenuItem("Collapse Selected Level");
877+
jmi.addActionListener(e -> {
878+
final TreePath selectedPath = tree.getSelectionPath();
879+
if (selectedPath != null)
880+
GuiUtils.JTrees.collapseNodesOfSameLevel(tree, selectedPath);
881+
});
882+
pMenu.add(jmi);
883+
pMenu.addSeparator();
884+
jmi = new JMenuItem("Expand All");
885+
jmi.addActionListener(e -> {
886+
GuiUtils.JTrees.expandAllNodes(tree);
887+
if (!searchableBar.getSearchField().getText().isEmpty())
888+
searchableBar.getSearchField().setText(
889+
searchableBar.getSearchField().getText());
890+
});
891+
pMenu.add(jmi);
892+
jmi = new JMenuItem("Expand Selected Level");
893+
jmi.addActionListener(e -> {
894+
final TreePath selectedPath = tree.getSelectionPath();
895+
if (selectedPath != null)
896+
GuiUtils.JTrees.expandNodesOfSameLevel(tree, selectedPath);
897+
});
898+
pMenu.add(jmi);
899+
pMenu.addSeparator();
900+
final JCheckBoxMenuItem jcmi = new JCheckBoxMenuItem("Auto-select Children",
901+
tree.getCheckBoxTreeSelectionModel().isDigIn());
902+
jcmi.addItemListener(e -> tree.getCheckBoxTreeSelectionModel().setDigIn(jcmi.isSelected()));
903+
pMenu.add(jcmi);
904+
return pMenu;
905+
}
906+
633907
List<String> getSelectedPaths() {
634908
final List<String> paths = new ArrayList<>();
635909
final TreePath[] treePaths = tree

‎src/main/java/sc/fiji/snt/gui/SNTSearchableBar.java‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,16 @@ public void setSubFilteringEnabled(final boolean enable) {
399399
this.subFilteringEnabled = enable;
400400
}
401401

402+
/**
403+
* Adds a menu item to the search bar's options (gear) menu.
404+
* The item is appended before the "Tips &amp; Shortcuts" entry.
405+
*
406+
* @param item the menu item to add
407+
*/
408+
public void addOptionsMenuItem(final JMenuItem item) {
409+
appendToOptionsMenu(List.of(item));
410+
}
411+
402412
public void setStatus(final String text) {
403413
super._statusLabel.setText(text);
404414
}

0 commit comments

Comments
 (0)