2222
2323package sc .fiji .snt .gui ;
2424
25+ import com .jidesoft .swing .ListSearchable ;
2526import com .jidesoft .swing .Searchable ;
2627import com .jidesoft .swing .SearchableBar ;
2728import com .jidesoft .swing .SearchableBarIconsFactory ;
29+ import com .jidesoft .swing .TreeSearchable ;
2830import com .jidesoft .swing .WholeWordsSupport ;
2931import com .jidesoft .swing .event .SearchableEvent ;
3032import com .jidesoft .swing .event .SearchableListener ;
3133
3234import sc .fiji .snt .SNTUtils ;
3335
3436import javax .swing .*;
37+ import javax .swing .tree .TreePath ;
3538import java .awt .*;
3639import java .awt .event .KeyEvent ;
3740import java .io .Serial ;
3841import java .util .ArrayList ;
3942import java .util .Collection ;
4043import java .util .List ;
44+ import java .util .function .Function ;
4145
4246/**
4347 * Implements a SearchableBar following SNT's UI.
@@ -48,7 +52,7 @@ public class SNTSearchableBar extends SearchableBar {
4852
4953 @ Serial
5054 private static final long serialVersionUID = 1L ;
51- private static final int DELAY_MS = 100 ;
55+ private static final int DELAY_MS = 200 ;
5256 protected List <AbstractButton > _extraButtons ;
5357
5458 private String statusLabelPlaceholder ;
@@ -61,6 +65,23 @@ public class SNTSearchableBar extends SearchableBar {
6165 protected boolean subFilteringEnabled ;
6266 private final boolean compactNavigation ;
6367
68+
69+ public SNTSearchableBar (final JTree jTree ) {
70+ this (SNTSearchableBar .newTreeSearchable (jTree ));
71+ }
72+
73+ public SNTSearchableBar (final JList <?> list ) {
74+ this (SNTSearchableBar .newListSearchable (list ));
75+ }
76+
77+ public SNTSearchableBar (final JTree jTree , final String placeholder ) {
78+ this (SNTSearchableBar .newTreeSearchable (jTree ), placeholder );
79+ }
80+
81+ public SNTSearchableBar (final JList <?> list , final String placeholder ) {
82+ this (SNTSearchableBar .newListSearchable (list ), placeholder );
83+ }
84+
6485 public SNTSearchableBar (final Searchable searchable ) {
6586 this (searchable , "Type to select" );
6687 }
@@ -81,6 +102,139 @@ public SNTSearchableBar(final Searchable searchable, final String placeholder) {
81102 init (placeholder ); // should be the last call in the constructor
82103 }
83104
105+ /**
106+ * Creates a {@link TreeSearchable} that scrolls only to the current match rather than once per hit.
107+ * A plain {@code TreeSearchable} scrolls on every call to its {@code setSelectedIndex()}, and
108+ * {@code highlightAll()} (on by default here, see {@link #setHighlightAll(boolean)}) calls it once per
109+ * match, so a large/deep tree ends up scrolling repeatedly on every keystroke instead of just once.
110+ * <p>
111+ * Also turns on {@link TreeSearchable#setRecursive(boolean)}, which defaults to {@code false} in jide-oss: with
112+ * it off, matches are only looked for among the currently visible (expanded) rows, so a collapsed branch is
113+ * invisible to the search, not just to the eye
114+ *
115+ * @param tree the tree to be searched
116+ * @return a scroll-throttled, fully-recursive searchable wrapping {@code tree}
117+ */
118+ public static Searchable newTreeSearchable (final JTree tree ) {
119+ final TreeSearchable searchable = new TreeSearchable (tree ) {
120+ private boolean suppressScroll ;
121+ private TreePath firstMatch ;
122+
123+ @ Override
124+ protected void highlightAll () {
125+ firstMatch = null ;
126+ suppressScroll = true ;
127+ try {
128+ super .highlightAll ();
129+ } finally {
130+ suppressScroll = false ;
131+ }
132+ if (firstMatch != null ) GuiUtils .JTrees .scrollDirectlyTo (tree , firstMatch );
133+ }
134+
135+ @ Override
136+ public int findFromCursor (final String s ) {
137+ // with match-count on (see setShowMatchCount()), this walks EVERY element from the start to count all
138+ // matches, calling setSelectedIndex() once per match; If left unsuppressed that alone scrolls through
139+ // the whole tree before the real, single search result is even selected!
140+ final boolean wasSuppressed = suppressScroll ;
141+ suppressScroll = true ;
142+ try {
143+ return super .findFromCursor (s );
144+ } finally {
145+ suppressScroll = wasSuppressed ;
146+ }
147+ }
148+
149+ @ Override
150+ protected void setSelectedIndex (final int index , final boolean incremental ) {
151+ if (!suppressScroll ) {
152+ super .setSelectedIndex (index , incremental );
153+ return ;
154+ }
155+ // same selection logic as TreeSearchable#setSelectedIndex, minus
156+ // the scrollRowToVisible()/scrollPathToVisible() call
157+ if (!isRecursive ()) {
158+ if (incremental ) tree .addSelectionInterval (index , index );
159+ else tree .setSelectionRow (index );
160+ } else if (getElementAt (index ) instanceof TreePath path ) {
161+ if (firstMatch == null ) firstMatch = path ;
162+ if (incremental ) tree .addSelectionPath (path );
163+ else tree .setSelectionPath (path );
164+ }
165+ }
166+ };
167+ searchable .setRecursive (true );
168+ return searchable ;
169+ }
170+
171+ /**
172+ * Same as {@link #newListSearchable(JList, Function)}, with the default {@code toString()}-based element converter
173+ *
174+ * @param list the list to be searched
175+ * @return a scroll-throttled searchable wrapping {@code list}
176+ */
177+ public static Searchable newListSearchable (final JList <?> list ) {
178+ return newListSearchable (list , null );
179+ }
180+
181+ /**
182+ * Creates a {@link ListSearchable} with the same scroll-throttling and match-counting fixes as
183+ * {@link #newTreeSearchable(JTree)}.
184+ *
185+ * @param list the list to be searched
186+ * @param elementConverter optional; converts a list element to the string matched against, or {@code null} to use
187+ * the element's own {@code toString()}
188+ * @return a scroll-throttled searchable wrapping {@code list}
189+ */
190+ public static Searchable newListSearchable (final JList <?> list , final Function <Object , String > elementConverter ) {
191+ return new ListSearchable (list ) {
192+ private boolean suppressScroll ;
193+ private int firstMatch = -1 ;
194+
195+ @ Override
196+ protected String convertElementToString (final Object object ) {
197+ return (elementConverter != null ) ? elementConverter .apply (object ) : super .convertElementToString (object );
198+ }
199+
200+ @ Override
201+ public int findFromCursor (final String s ) {
202+ final boolean wasSuppressed = suppressScroll ;
203+ suppressScroll = true ;
204+ try {
205+ return super .findFromCursor (s );
206+ } finally {
207+ suppressScroll = wasSuppressed ;
208+ }
209+ }
210+
211+ @ Override
212+ protected void highlightAll () {
213+ firstMatch = -1 ;
214+ suppressScroll = true ;
215+ try {
216+ super .highlightAll ();
217+ } finally {
218+ suppressScroll = false ;
219+ }
220+ if (firstMatch != -1 ) list .ensureIndexIsVisible (firstMatch );
221+ }
222+
223+ @ Override
224+ public void setSelectedIndex (final int index , final boolean incremental ) {
225+ if (!suppressScroll ) {
226+ super .setSelectedIndex (index , incremental );
227+ return ;
228+ }
229+ // same selection logic as ListSearchable#setSelectedIndex,
230+ // minus the ensureIndexIsVisible() call
231+ if (incremental ) list .addSelectionInterval (index , index );
232+ else if (list .getSelectedIndex () != index ) list .setSelectedIndex (index );
233+ if (firstMatch == -1 && index >= 0 ) firstMatch = index ;
234+ }
235+ };
236+ }
237+
84238 @ Override
85239 public int getMaxHistoryLength () {
86240 return 0 ; // disabled. We'll use builtinSearchHistory
0 commit comments