Skip to content

Commit 8b90dd8

Browse files
intial (buggy) menu search implemntation
1 parent 1546196 commit 8b90dd8

File tree

1 file changed

+195
-40
lines changed

1 file changed

+195
-40
lines changed

enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java

Lines changed: 195 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -6,86 +6,241 @@
66
import org.quiltmc.enigma.gui.element.SearchableElement;
77
import org.quiltmc.enigma.util.I18n;
88
import org.quiltmc.enigma.util.multi_trie.CompositeStringMultiTrie;
9+
import org.quiltmc.enigma.util.multi_trie.MultiTrie;
910
import org.quiltmc.enigma.util.multi_trie.StringMultiTrie;
1011

11-
import javax.swing.JPanel;
12+
import javax.annotation.Nullable;
13+
import javax.swing.JMenuItem;
1214
import javax.swing.MenuElement;
15+
import javax.swing.event.DocumentEvent;
16+
import javax.swing.event.DocumentListener;
1317
import java.util.Arrays;
18+
import java.util.Set;
19+
import java.util.stream.Collectors;
1420
import java.util.stream.Stream;
1521

1622
public class SearchMenusMenu extends AbstractEnigmaMenu {
1723
/**
18-
* @return a breadth-first stream of the passed {@code root} element and all of its sub-elements
24+
* @return a breadth-first stream of the passed {@code root} element and all of its sub-elements,
25+
* excluding {@link SearchMenusMenu}s and their sub-elements
1926
*/
2027
private static Stream<MenuElement> streamElementTree(MenuElement root) {
21-
return Stream.concat(
28+
return root instanceof SearchMenusMenu ? Stream.empty() : Stream.concat(
2229
Stream.of(root),
2330
Arrays.stream(root.getSubElements()).flatMap(SearchMenusMenu::streamElementTree)
2431
);
2532
}
2633

2734
private final PlaceheldTextField field = new PlaceheldTextField();
28-
private final JPanel results = new JPanel();
35+
private final JMenuItem noResults = new JMenuItem();
2936

30-
private StringMultiTrie.View<SearchableElement> elements;
37+
private final ResultManager resultManager = new ResultManager();
3138

3239
protected SearchMenusMenu(Gui gui) {
3340
super(gui);
3441

42+
this.noResults.setEnabled(false);
43+
this.noResults.setVisible(false);
44+
3545
this.add(this.field);
36-
this.add(this.results);
46+
this.add(this.noResults);
3747

38-
SearchMenusMenu.this.field.addHierarchyListener(e -> {
39-
if (SearchMenusMenu.this.field.isShowing()) {
40-
SearchMenusMenu.this.field.requestFocus();
41-
SearchMenusMenu.this.field.selectAll();
48+
this.field.addHierarchyListener(e -> {
49+
if (this.field.isShowing()) {
50+
this.field.requestFocus();
51+
this.field.selectAll();
4252
}
4353
});
4454

45-
// TODO KeyBinds: up/down -> prev/next result, enter -> doClick on selected result
46-
47-
this.retranslate();
48-
}
55+
this.field.getDocument().addDocumentListener(new DocumentListener() {
56+
void updateResultItems() {
57+
final String searchTerm = SearchMenusMenu.this.field.getText();
58+
59+
if (searchTerm.isEmpty()) {
60+
SearchMenusMenu.this.noResults.setVisible(false);
61+
SearchMenusMenu.this.invalidate();
62+
SearchMenusMenu.this.repaint();
63+
SearchMenusMenu.this.resultManager.clearCurrent();
64+
} else {
65+
switch (SearchMenusMenu.this.resultManager.updateResultItems(searchTerm)) {
66+
case NO_RESULTS -> {
67+
SearchMenusMenu.this.noResults.setVisible(false);
68+
69+
SearchMenusMenu.this.getPopupMenu().pack();
70+
SearchMenusMenu.this.getPopupMenu().pack();
71+
}
72+
case SAME_RESULTS -> { }
73+
case DIFFERENT_RESULTS -> {
74+
SearchMenusMenu.this.noResults.setVisible(true);
75+
76+
SearchMenusMenu.this.getPopupMenu().pack();
77+
SearchMenusMenu.this.getPopupMenu().pack();
78+
}
79+
}
80+
}
81+
}
4982

50-
private StringMultiTrie.View<SearchableElement> getElements() {
51-
if (this.elements == null) {
52-
this.elements = this.buildElementsTrie();
53-
}
83+
@Override
84+
public void insertUpdate(DocumentEvent e) {
85+
this.updateResultItems();
86+
}
5487

55-
return this.elements;
56-
}
88+
@Override
89+
public void removeUpdate(DocumentEvent e) {
90+
this.updateResultItems();
91+
}
5792

58-
private void clearElements() {
59-
this.elements = null;
60-
}
93+
@Override
94+
public void changedUpdate(DocumentEvent e) {
95+
this.updateResultItems();
96+
}
97+
});
6198

62-
private StringMultiTrie.View<SearchableElement> buildElementsTrie() {
63-
final CompositeStringMultiTrie<SearchableElement> elementsBuilder = CompositeStringMultiTrie.createHashed();
64-
this.gui.getMenuBar()
65-
.streamMenus()
66-
.flatMap(SearchMenusMenu::streamElementTree)
67-
.<SearchableElement>mapMulti((element, keep) -> {
68-
if (element instanceof SearchableElement searchable) {
69-
keep.accept(searchable);
70-
}
71-
})
72-
.forEach(searchable -> searchable
73-
.streamSearchAliases()
74-
.forEach(alias -> elementsBuilder.put(alias, searchable))
75-
);
99+
// TODO KeyBinds: up/down -> prev/next result, enter -> doClick on selected result
76100

77-
return elementsBuilder.getView();
101+
this.retranslate();
78102
}
79103

80104
@Override
81105
public void updateState(boolean jarOpen, ConnectionState state) {
82-
this.clearElements();
106+
this.resultManager.clear();
83107
}
84108

85109
@Override
86110
public void retranslate() {
87-
this.clearElements();
111+
this.resultManager.clear();
112+
88113
this.setText(I18n.translate("menu.help.search"));
89114
this.field.setPlaceholder(I18n.translate("menu.help.search.placeholder"));
90115
}
116+
117+
private static class Result {
118+
final SearchableElement element;
119+
final String alias;
120+
121+
@Nullable JMenuItem item;
122+
123+
Result(SearchableElement element, String alias) {
124+
this.element = element;
125+
this.alias = alias;
126+
}
127+
128+
JMenuItem getItem() {
129+
if (this.item == null) {
130+
this.item = new JMenuItem(this.alias);
131+
}
132+
133+
return this.item;
134+
}
135+
}
136+
137+
private class ResultManager {
138+
@Nullable
139+
StringMultiTrie.View<Result> resultTrie;
140+
@Nullable
141+
CurrentResults currentResults;
142+
143+
/**
144+
* @return {@code true} if there are any results, or {@code false} otherwise
145+
*/
146+
UpdateOutcome updateResultItems(String searchTerm) {
147+
if (this.currentResults == null || !searchTerm.startsWith(this.currentResults.searchTerm)) {
148+
return this.initializeCurrentResults(searchTerm);
149+
} else {
150+
if (this.currentResults.searchTerm.length() == searchTerm.length()) {
151+
return UpdateOutcome.SAME_RESULTS;
152+
} else {
153+
MultiTrie.Node<Character, Result> resultNode = this.currentResults.results;
154+
for (int i = this.currentResults.searchTerm.length(); i < searchTerm.length(); i++) {
155+
resultNode = resultNode.next(searchTerm.charAt(i));
156+
}
157+
158+
if (resultNode.isEmpty()) {
159+
this.clearCurrent();
160+
161+
return UpdateOutcome.NO_RESULTS;
162+
} else {
163+
final Set<Result> newResults = resultNode.streamValues().collect(Collectors.toSet());
164+
165+
final Set<JMenuItem> excludedResults = this.currentResults.results.streamValues()
166+
.filter(oldResult -> !newResults.contains(oldResult))
167+
.map(Result::getItem)
168+
.collect(Collectors.toSet());
169+
170+
if (excludedResults.isEmpty()) {
171+
return UpdateOutcome.SAME_RESULTS;
172+
} else {
173+
excludedResults
174+
.forEach(SearchMenusMenu.this::remove);
175+
176+
this.currentResults = new CurrentResults(resultNode, searchTerm);
177+
178+
return UpdateOutcome.DIFFERENT_RESULTS;
179+
}
180+
}
181+
}
182+
}
183+
}
184+
185+
UpdateOutcome initializeCurrentResults(String searchTerm) {
186+
final MultiTrie.Node<Character, Result> results = this.getResultTrie().get(searchTerm);
187+
if (results.isEmpty()) {
188+
this.clearCurrent();
189+
190+
return UpdateOutcome.NO_RESULTS;
191+
} else {
192+
this.currentResults = new CurrentResults(results, searchTerm);
193+
this.currentResults.results.streamValues().map(Result::getItem).forEach(SearchMenusMenu.this::add);
194+
195+
return UpdateOutcome.DIFFERENT_RESULTS;
196+
}
197+
}
198+
199+
StringMultiTrie.View<Result> getResultTrie() {
200+
if (this.resultTrie == null) {
201+
this.resultTrie = this.buildResultTrie();
202+
}
203+
204+
return this.resultTrie;
205+
}
206+
207+
void clear() {
208+
this.resultTrie = null;
209+
this.clearCurrent();
210+
}
211+
212+
void clearCurrent() {
213+
if (this.currentResults != null) {
214+
this.currentResults.results.streamValues()
215+
.map(Result::getItem)
216+
.forEach(SearchMenusMenu.this::remove);
217+
218+
this.currentResults = null;
219+
}
220+
}
221+
222+
StringMultiTrie.View<Result> buildResultTrie() {
223+
final CompositeStringMultiTrie<Result> elementsBuilder = CompositeStringMultiTrie.createHashed();
224+
SearchMenusMenu.this.gui.getMenuBar()
225+
.streamMenus()
226+
.flatMap(SearchMenusMenu::streamElementTree)
227+
.<SearchableElement>mapMulti((element, keep) -> {
228+
if (element instanceof SearchableElement searchable) {
229+
keep.accept(searchable);
230+
}
231+
})
232+
.forEach(searchable -> searchable
233+
.streamSearchAliases()
234+
.forEach(alias -> elementsBuilder.put(alias, new Result(searchable, alias)))
235+
);
236+
237+
return elementsBuilder.getView();
238+
}
239+
240+
record CurrentResults(MultiTrie.Node<Character, Result> results, String searchTerm) { }
241+
242+
enum UpdateOutcome {
243+
NO_RESULTS, SAME_RESULTS, DIFFERENT_RESULTS
244+
}
245+
}
91246
}

0 commit comments

Comments
 (0)