From a8b4d7df68213a0357a21267520a7af30949d0cf Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sun, 9 Nov 2025 15:34:36 -0800 Subject: [PATCH 001/124] add non-functional SearchMenusMenu with a search field and an empty results panel --- .../enigma/gui/element/menu_bar/HelpMenu.java | 5 +++ .../gui/element/menu_bar/SearchMenusMenu.java | 38 +++++++++++++++++++ enigma/src/main/resources/lang/en_us.json | 1 + 3 files changed, 44 insertions(+) create mode 100644 enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/HelpMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/HelpMenu.java index 6d89d69d6..6ea4edd41 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/HelpMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/HelpMenu.java @@ -10,12 +10,16 @@ public class HelpMenu extends AbstractEnigmaMenu { private final JMenuItem aboutItem = new JMenuItem(); private final JMenuItem githubItem = new JMenuItem(); + private final SearchMenusMenu searchItem; public HelpMenu(Gui gui) { super(gui); + this.searchItem = new SearchMenusMenu(gui); + this.add(this.aboutItem); this.add(this.githubItem); + this.add(this.searchItem); this.aboutItem.addActionListener(e -> AboutDialog.show(this.gui.getFrame())); this.githubItem.addActionListener(e -> this.onGithubClicked()); @@ -26,6 +30,7 @@ public void retranslate() { this.setText(I18n.translate("menu.help")); this.aboutItem.setText(I18n.translate("menu.help.about")); this.githubItem.setText(I18n.translate("menu.help.github")); + this.searchItem.retranslate(); } private void onGithubClicked() { diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java new file mode 100644 index 000000000..6acc334ac --- /dev/null +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java @@ -0,0 +1,38 @@ +package org.quiltmc.enigma.gui.element.menu_bar; + +import org.quiltmc.enigma.gui.ConnectionState; +import org.quiltmc.enigma.gui.Gui; +import org.quiltmc.enigma.util.I18n; + +import javax.swing.JPanel; +import javax.swing.JTextField; + +public class SearchMenusMenu extends AbstractEnigmaMenu { + final JTextField field = new JTextField(); + final JPanel results = new JPanel(); + + protected SearchMenusMenu(Gui gui) { + super(gui); + + this.add(this.field); + this.add(this.results); + + // TODO focus field on open + + // TODO KeyBinds: up/down -> prev/next result, enter -> doClick on selected result + + this.retranslate(); + } + + @Override + public void updateState(boolean jarOpen, ConnectionState state) { + // TODO check any caching + } + + @Override + public void retranslate() { + // TODO check any caching + this.setText(I18n.translate("menu.help.search")); + // TODO translate field placeholder + } +} diff --git a/enigma/src/main/resources/lang/en_us.json b/enigma/src/main/resources/lang/en_us.json index 7fce5ddaa..97795e688 100644 --- a/enigma/src/main/resources/lang/en_us.json +++ b/enigma/src/main/resources/lang/en_us.json @@ -108,6 +108,7 @@ "menu.help.about.version": "Version: %s", "menu.help.about.version.external": "%s Version: %s", "menu.help.github": "Github Page", + "menu.help.search": "Search menus", "popup_menu.rename": "Rename", "popup_menu.paste": "Paste text", From 9ed3b2ab59e58b48919f0fbde17fe53d65ffb326 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sun, 9 Nov 2025 17:42:03 -0800 Subject: [PATCH 002/124] add PlaceheldTextField --- .../gui/element/PlaceheldTextField.java | 124 ++++++++++++++++++ .../gui/element/menu_bar/SearchMenusMenu.java | 6 +- enigma/src/main/resources/lang/en_us.json | 1 + 3 files changed, 128 insertions(+), 3 deletions(-) create mode 100644 enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/PlaceheldTextField.java diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/PlaceheldTextField.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/PlaceheldTextField.java new file mode 100644 index 000000000..bab20e378 --- /dev/null +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/PlaceheldTextField.java @@ -0,0 +1,124 @@ +package org.quiltmc.enigma.gui.element; + +import javax.annotation.Nullable; +import javax.swing.JTextField; +import javax.swing.text.Document; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Insets; +import java.awt.RenderingHints; +import java.awt.Toolkit; +import java.util.Map; + +/** + * A text field that displays placeholder text when it's empty. + */ +public class PlaceheldTextField extends JTextField { + private static final String DESKTOP_FONT_HINTS_KEY = "awt.font.desktophints"; + + @Nullable + private static Map desktopFontHints = Toolkit.getDefaultToolkit().getDesktopProperty(DESKTOP_FONT_HINTS_KEY) + instanceof Map map ? map : null; + + static { + Toolkit.getDefaultToolkit().addPropertyChangeListener(DESKTOP_FONT_HINTS_KEY, e -> { + desktopFontHints = e.getNewValue() instanceof Map map ? map : null; + }); + } + + @Nullable + private String placeholder; + + @Nullable + private Color placeholderColor; + + /** + * Constructs a new field with the default {@link Document}, {@code 0} columns, and no initial text or placeholder. + */ + public PlaceheldTextField() { + this(null, null); + } + + /** + * Constructs a new field with the default {@link Document}, {@code 0} columns, and the passed initial + * {@code text} and {@code placeholder}. + * + * @param text the initial text; may be {@code null} + * @param placeholder the initial placeholder; may be {@code null} + */ + public PlaceheldTextField(String text, String placeholder) { + this(null, text, placeholder, 0); + } + + /** + * Constructs a new field. + * + * @param doc see {@link JTextField#JTextField(Document, String, int)} + * @param text the initial text; may be {@code null} + * @param placeholder the initial placeholder; may be {@code null} + * @param columns see {@link JTextField#JTextField(Document, String, int)} + * + * @exception IllegalArgumentException if {@code columns} is negative + */ + public PlaceheldTextField( + @Nullable Document doc, @Nullable String text, @Nullable String placeholder, int columns + ) { + super(doc, text, columns); + + this.placeholder = placeholder; + } + + @Override + public Dimension getPreferredSize() { + final Dimension size = super.getPreferredSize(); + + if (this.placeholder != null) { + final Insets insets = this.getInsets(); + + final int placeholderWidth = this.getFontMetrics(this.getFont()).stringWidth(this.placeholder); + + size.width = Math.max(insets.left + placeholderWidth + insets.right, size.width); + } + + return size; + } + + @Override + protected void paintComponent(Graphics graphics) { + super.paintComponent(graphics); + + if (this.placeholder != null && this.getText().isEmpty()) { + if (graphics instanceof Graphics2D graphics2D) { + if (desktopFontHints == null) { + graphics2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + } else { + graphics2D.setRenderingHints(desktopFontHints); + } + } + + graphics.setColor(this.placeholderColor == null ? this.getForeground() : this.placeholderColor); + graphics.setFont(this.getFont()); + + final Insets insets = this.getInsets(); + final int baseY = graphics.getFontMetrics().getMaxAscent() + insets.top; + graphics.drawString(this.placeholder, insets.left, baseY); + } + } + + /** + * @param placeholder the placeholder text for this field; if {@code null}, no placeholder will be shown + */ + public void setPlaceholder(@Nullable String placeholder) { + this.placeholder = placeholder; + } + + /** + * @param color the placeholder color for this field; if {@code null}, the + * {@linkplain #getForeground() foreground color} will be used + */ + public void setPlaceholderColor(@Nullable Color color) { + this.placeholderColor = color; + } +} diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java index 6acc334ac..679f0a92e 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java @@ -2,13 +2,13 @@ import org.quiltmc.enigma.gui.ConnectionState; import org.quiltmc.enigma.gui.Gui; +import org.quiltmc.enigma.gui.element.PlaceheldTextField; import org.quiltmc.enigma.util.I18n; import javax.swing.JPanel; -import javax.swing.JTextField; public class SearchMenusMenu extends AbstractEnigmaMenu { - final JTextField field = new JTextField(); + final PlaceheldTextField field = new PlaceheldTextField(); final JPanel results = new JPanel(); protected SearchMenusMenu(Gui gui) { @@ -33,6 +33,6 @@ public void updateState(boolean jarOpen, ConnectionState state) { public void retranslate() { // TODO check any caching this.setText(I18n.translate("menu.help.search")); - // TODO translate field placeholder + this.field.setPlaceholder(I18n.translate("menu.help.search.placeholder")); } } diff --git a/enigma/src/main/resources/lang/en_us.json b/enigma/src/main/resources/lang/en_us.json index 97795e688..6243f7be9 100644 --- a/enigma/src/main/resources/lang/en_us.json +++ b/enigma/src/main/resources/lang/en_us.json @@ -109,6 +109,7 @@ "menu.help.about.version.external": "%s Version: %s", "menu.help.github": "Github Page", "menu.help.search": "Search menus", + "menu.help.search.placeholder": "Search menus...", "popup_menu.rename": "Rename", "popup_menu.paste": "Paste text", From e247070644e6aa65f832a00fde9204e013896c2f Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sun, 9 Nov 2025 20:58:39 -0800 Subject: [PATCH 003/124] first pass at MultiTrie abstraction --- .../collection/trie/AbstractMapMultiTrie.java | 59 +++++++++ .../trie/AbstractMutableMapMultiTrie.java | 114 ++++++++++++++++++ .../util/collection/trie/EmptyNode.java | 44 +++++++ .../util/collection/trie/MultiTrie.java | 47 ++++++++ .../collection/trie/MutableMultiTrie.java | 19 +++ .../collection/trie/StringHashMultiTrie.java | 29 +++++ .../util/collection/trie/StringMultiTrie.java | 40 ++++++ 7 files changed, 352 insertions(+) create mode 100644 enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/AbstractMapMultiTrie.java create mode 100644 enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/AbstractMutableMapMultiTrie.java create mode 100644 enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/EmptyNode.java create mode 100644 enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/MultiTrie.java create mode 100644 enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/MutableMultiTrie.java create mode 100644 enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/StringHashMultiTrie.java create mode 100644 enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/StringMultiTrie.java diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/AbstractMapMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/AbstractMapMultiTrie.java new file mode 100644 index 000000000..44da3aa7d --- /dev/null +++ b/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/AbstractMapMultiTrie.java @@ -0,0 +1,59 @@ +package org.quiltmc.enigma.util.collection.trie; + +import com.google.common.collect.Multimap; +import org.quiltmc.enigma.util.collection.trie.AbstractMapMultiTrie.MapNode; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Map; +import java.util.stream.Stream; + +public abstract class AbstractMapMultiTrie> implements MultiTrie { + protected final N root; + + protected AbstractMapMultiTrie(N root) { + this.root = root; + } + + @Override + public Node getRoot() { + return null; + } + + protected static class MapNode> implements Node { + protected final Map children; + protected final Multimap leaves; + + protected MapNode(Map children, Multimap leaves) { + this.children = children; + this.leaves = leaves; + } + + @Override + public Stream streamLeaves() { + return this.leaves.values().stream(); + } + + @Override + public Stream streamBranches() { + return this.children.values().stream().flatMap(MapNode::streamValues); + } + + @Override + public Stream streamValues() { + return Stream.concat(this.streamLeaves(), this.streamBranches()); + } + + @Override + @Nonnull + public Node next(K key) { + final MapNode next = this.nextImpl(key); + return next == null ? EmptyNode.get() : next; + } + + @Nullable + protected N nextImpl(K key) { + return this.children.get(key); + } + } +} diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/AbstractMutableMapMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/AbstractMutableMapMultiTrie.java new file mode 100644 index 000000000..262ca720b --- /dev/null +++ b/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/AbstractMutableMapMultiTrie.java @@ -0,0 +1,114 @@ +package org.quiltmc.enigma.util.collection.trie; + +import org.quiltmc.enigma.util.collection.trie.AbstractMutableMapMultiTrie.MutableMapNode; +import com.google.common.collect.Multimap; + +import java.util.Map; + +public abstract class AbstractMutableMapMultiTrie> + extends AbstractMapMultiTrie + implements MutableMultiTrie { + private final View view = new View(); + + protected AbstractMutableMapMultiTrie(N root) { + super(root); + } + + @Override + public void put(S sequence, V value) { + this.root.put(sequence, value); + } + + @Override + public boolean remove(S sequence, V value) { + return this.root.remove(sequence, value); + } + + @Override + public boolean removeAll(S sequence) { + return this.root.removeAll(sequence); + } + + @Override + public MultiTrie view() { + return this.view; + } + + protected abstract static class MutableMapNode> + extends MapNode + implements MutableMultiTrie.Node { + protected MutableMapNode(Map children, Multimap leaves) { + super(children, leaves); + } + + @Override + public void put(S sequence, V value) { + final FirstSplit split = this.splitFirst(sequence); + if (this.isEmptySequence(split.suffix)) { + this.leaves.put(split.first, value); + } else { + final N empty = this.createEmpty(); + empty.put(split.suffix, value); + this.children.put(split.first, empty); + } + } + + @Override + public boolean remove(S sequence, V value) { + final FirstSplit split = this.splitFirst(sequence); + if (this.isEmptySequence(split.suffix)) { + return this.leaves.remove(split.first, value); + } else { + for (final N child : this.children.values()) { + if (child.remove(split.suffix, value)) { + return true; + } + } + + return false; + } + } + + @Override + public boolean removeAll(S sequence) { + final FirstSplit split = this.splitFirst(sequence); + if (this.isEmptySequence(split.suffix)) { + return !this.leaves.removeAll(split.first).isEmpty(); + } else { + for (final N child : this.children.values()) { + if (child.removeAll(split.suffix)) { + return true; + } + } + + return false; + } + } + + protected abstract N createEmpty(); + + /** + * Splits the first key from the passed {@code sequence} and returns that key and the remaining suffix. + * + * @implNote when invoked by {@link AbstractMutableMapMultiTrie}, the passed {@code sequence} is guaranteed + * non-empty according to {@link #isEmptySequence(Object)} + */ + protected abstract FirstSplit splitFirst(S sequence); + + protected abstract boolean isEmptySequence(S sequence); + + protected record FirstSplit(K first, S suffix) { } + } + + private class View implements MultiTrie { + @Override + public Node getRoot() { + return AbstractMutableMapMultiTrie.this.root; + } + + @Override + public Node get(S prefix) { + return AbstractMutableMapMultiTrie.this.get(prefix); + } + } +} diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/EmptyNode.java b/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/EmptyNode.java new file mode 100644 index 000000000..0ed6bdfef --- /dev/null +++ b/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/EmptyNode.java @@ -0,0 +1,44 @@ +package org.quiltmc.enigma.util.collection.trie; + +import javax.annotation.Nonnull; +import java.util.stream.Stream; + +/** + * An empty, immutable, singleton {@link MultiTrie.Node} implementation. + * + *

{@link MultiTrie.Node#next(Object)} implementations may return {@linkplain #get() the empty node} + * when nodes have no branches. + * + * @implNote not intended to be stored in tries + */ +public final class EmptyNode implements MultiTrie.Node { + private static final EmptyNode INSTANCE = new EmptyNode<>(); + + @SuppressWarnings("unchecked") + public static EmptyNode get() { + return (EmptyNode) INSTANCE; + } + + private EmptyNode() { } + + @Override + public Stream streamLeaves() { + return Stream.empty(); + } + + @Override + public Stream streamBranches() { + return Stream.empty(); + } + + @Override + public Stream streamValues() { + return Stream.empty(); + } + + @Override + @Nonnull + public MultiTrie.Node next(K key) { + return this; + } +} diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/MultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/MultiTrie.java new file mode 100644 index 000000000..a45e54a3b --- /dev/null +++ b/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/MultiTrie.java @@ -0,0 +1,47 @@ +package org.quiltmc.enigma.util.collection.trie; + +import java.util.stream.Stream; + +/** + * A multi-trie (or prefix tree) associates a sequence of keys with one or more values. + * + *

Values can be looked up by a prefix of their key sequence; all values associated with a sequence beginning with + * the prefix will be returned. + * + * @param the type of keys + * @param the type of sequences + * @param the type of values + */ +public interface MultiTrie { + Node getRoot(); + + default Node start(K key) { + return this.getRoot().next(key); + } + + Node get(S prefix); + + default long getSize() { + return this.getRoot().getSize(); + } + + default boolean isEmpty() { + return this.getSize() == 0; + } + + interface Node { + Stream streamLeaves(); + Stream streamBranches(); + Stream streamValues(); + + default long getSize() { + return this.streamValues().count(); + } + + default boolean isEmpty() { + return this.getSize() == 0; + } + + Node next(K key); + } +} diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/MutableMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/MutableMultiTrie.java new file mode 100644 index 000000000..85728fc0a --- /dev/null +++ b/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/MutableMultiTrie.java @@ -0,0 +1,19 @@ +package org.quiltmc.enigma.util.collection.trie; + +public interface MutableMultiTrie extends MultiTrie { + void put(S sequence, V value); + + boolean remove(S sequence, V value); + + boolean removeAll(S sequence); + + MultiTrie view(); + + interface Node extends MultiTrie.Node { + void put(S sequence, V value); + + boolean remove(S sequence, V value); + + boolean removeAll(S sequence); + } +} diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/StringHashMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/StringHashMultiTrie.java new file mode 100644 index 000000000..bf6f62727 --- /dev/null +++ b/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/StringHashMultiTrie.java @@ -0,0 +1,29 @@ +package org.quiltmc.enigma.util.collection.trie; + +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; +import org.quiltmc.enigma.util.collection.trie.StringHashMultiTrie.Node; + +import java.util.HashMap; +import java.util.Map; + +public final class StringHashMultiTrie extends StringMultiTrie> { + private static Node createEmptyNode() { + return new Node<>(new HashMap<>(), HashMultimap.create()); + } + + public StringHashMultiTrie() { + super(createEmptyNode()); + } + + static final class Node extends StringMultiTrie.StringNode> { + private Node(Map> children, Multimap leaves) { + super(children, leaves); + } + + @Override + protected Node createEmpty() { + return createEmptyNode(); + } + } +} diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/StringMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/StringMultiTrie.java new file mode 100644 index 000000000..8592b8115 --- /dev/null +++ b/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/StringMultiTrie.java @@ -0,0 +1,40 @@ +package org.quiltmc.enigma.util.collection.trie; + +import com.google.common.collect.Multimap; +import org.quiltmc.enigma.util.collection.trie.StringMultiTrie.StringNode; + +import java.util.Map; + +public class StringMultiTrie> + extends AbstractMutableMapMultiTrie { + protected StringMultiTrie(N root) { + super(root); + } + + @Override + public MultiTrie.Node get(String prefix) { + N node = this.root; + for (int i = 0; i < prefix.length() && node != null; i++) { + node = node.nextImpl(prefix.charAt(i)); + } + + return node == null ? EmptyNode.get() : node; + } + + protected abstract static class StringNode> + extends AbstractMutableMapMultiTrie.MutableMapNode { + protected StringNode(Map children, Multimap leaves) { + super(children, leaves); + } + + @Override + protected FirstSplit splitFirst(String sequence) { + return new FirstSplit<>(sequence.charAt(0), sequence.substring(1)); + } + + @Override + protected boolean isEmptySequence(String sequence) { + return sequence.isEmpty(); + } + } +} From f14013c00d29cd6771b459c3ca19dc0bdf863242 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sun, 9 Nov 2025 21:40:33 -0800 Subject: [PATCH 004/124] rename all inner MultiTrie Node classes -> Node --- .../collection/trie/AbstractMapMultiTrie.java | 16 ++++++++-------- .../trie/AbstractMutableMapMultiTrie.java | 10 +++++----- .../collection/trie/StringHashMultiTrie.java | 2 +- .../util/collection/trie/StringMultiTrie.java | 10 +++++----- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/AbstractMapMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/AbstractMapMultiTrie.java index 44da3aa7d..d09013798 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/AbstractMapMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/AbstractMapMultiTrie.java @@ -1,14 +1,14 @@ package org.quiltmc.enigma.util.collection.trie; import com.google.common.collect.Multimap; -import org.quiltmc.enigma.util.collection.trie.AbstractMapMultiTrie.MapNode; +import org.quiltmc.enigma.util.collection.trie.AbstractMapMultiTrie.Node; import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.util.Map; import java.util.stream.Stream; -public abstract class AbstractMapMultiTrie> implements MultiTrie { +public abstract class AbstractMapMultiTrie> implements MultiTrie { protected final N root; protected AbstractMapMultiTrie(N root) { @@ -16,15 +16,15 @@ protected AbstractMapMultiTrie(N root) { } @Override - public Node getRoot() { + public MultiTrie.Node getRoot() { return null; } - protected static class MapNode> implements Node { + protected static class Node> implements MultiTrie.Node { protected final Map children; protected final Multimap leaves; - protected MapNode(Map children, Multimap leaves) { + protected Node(Map children, Multimap leaves) { this.children = children; this.leaves = leaves; } @@ -36,7 +36,7 @@ public Stream streamLeaves() { @Override public Stream streamBranches() { - return this.children.values().stream().flatMap(MapNode::streamValues); + return this.children.values().stream().flatMap(Node::streamValues); } @Override @@ -46,8 +46,8 @@ public Stream streamValues() { @Override @Nonnull - public Node next(K key) { - final MapNode next = this.nextImpl(key); + public MultiTrie.Node next(K key) { + final Node next = this.nextImpl(key); return next == null ? EmptyNode.get() : next; } diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/AbstractMutableMapMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/AbstractMutableMapMultiTrie.java index 262ca720b..d7b9891a2 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/AbstractMutableMapMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/AbstractMutableMapMultiTrie.java @@ -1,11 +1,11 @@ package org.quiltmc.enigma.util.collection.trie; -import org.quiltmc.enigma.util.collection.trie.AbstractMutableMapMultiTrie.MutableMapNode; +import org.quiltmc.enigma.util.collection.trie.AbstractMutableMapMultiTrie.Node; import com.google.common.collect.Multimap; import java.util.Map; -public abstract class AbstractMutableMapMultiTrie> +public abstract class AbstractMutableMapMultiTrie> extends AbstractMapMultiTrie implements MutableMultiTrie { private final View view = new View(); @@ -34,10 +34,10 @@ public MultiTrie view() { return this.view; } - protected abstract static class MutableMapNode> - extends MapNode + protected abstract static class Node> + extends AbstractMapMultiTrie.Node implements MutableMultiTrie.Node { - protected MutableMapNode(Map children, Multimap leaves) { + protected Node(Map children, Multimap leaves) { super(children, leaves); } diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/StringHashMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/StringHashMultiTrie.java index bf6f62727..3d12ef1bc 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/StringHashMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/StringHashMultiTrie.java @@ -16,7 +16,7 @@ public StringHashMultiTrie() { super(createEmptyNode()); } - static final class Node extends StringMultiTrie.StringNode> { + static final class Node extends StringMultiTrie.Node> { private Node(Map> children, Multimap leaves) { super(children, leaves); } diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/StringMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/StringMultiTrie.java index 8592b8115..41cfae55c 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/StringMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/StringMultiTrie.java @@ -1,11 +1,11 @@ package org.quiltmc.enigma.util.collection.trie; import com.google.common.collect.Multimap; -import org.quiltmc.enigma.util.collection.trie.StringMultiTrie.StringNode; +import org.quiltmc.enigma.util.collection.trie.StringMultiTrie.Node; import java.util.Map; -public class StringMultiTrie> +public class StringMultiTrie> extends AbstractMutableMapMultiTrie { protected StringMultiTrie(N root) { super(root); @@ -21,9 +21,9 @@ public MultiTrie.Node get(String prefix) { return node == null ? EmptyNode.get() : node; } - protected abstract static class StringNode> - extends AbstractMutableMapMultiTrie.MutableMapNode { - protected StringNode(Map children, Multimap leaves) { + protected abstract static class Node> + extends AbstractMutableMapMultiTrie.Node { + protected Node(Map children, Multimap leaves) { super(children, leaves); } From e149fcfdf091839de9ef1de0f90dcb280480a071 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sun, 9 Nov 2025 21:42:11 -0800 Subject: [PATCH 005/124] add Nonnull to base Node::next method --- .../java/org/quiltmc/enigma/util/collection/trie/MultiTrie.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/MultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/MultiTrie.java index a45e54a3b..577a9f363 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/MultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/MultiTrie.java @@ -1,5 +1,6 @@ package org.quiltmc.enigma.util.collection.trie; +import javax.annotation.Nonnull; import java.util.stream.Stream; /** @@ -42,6 +43,7 @@ default boolean isEmpty() { return this.getSize() == 0; } + @Nonnull Node next(K key); } } From 02e9e3d1f516c66f8c9bf9e8e23d176d0c57e82c Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sun, 9 Nov 2025 21:46:37 -0800 Subject: [PATCH 006/124] add @Nonnull to MultiTrie::get --- .../util/collection/trie/AbstractMutableMapMultiTrie.java | 2 ++ .../java/org/quiltmc/enigma/util/collection/trie/MultiTrie.java | 1 + .../quiltmc/enigma/util/collection/trie/StringMultiTrie.java | 2 ++ 3 files changed, 5 insertions(+) diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/AbstractMutableMapMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/AbstractMutableMapMultiTrie.java index d7b9891a2..a9007df0f 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/AbstractMutableMapMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/AbstractMutableMapMultiTrie.java @@ -3,6 +3,7 @@ import org.quiltmc.enigma.util.collection.trie.AbstractMutableMapMultiTrie.Node; import com.google.common.collect.Multimap; +import javax.annotation.Nonnull; import java.util.Map; public abstract class AbstractMutableMapMultiTrie> @@ -106,6 +107,7 @@ public Node getRoot() { return AbstractMutableMapMultiTrie.this.root; } + @Nonnull @Override public Node get(S prefix) { return AbstractMutableMapMultiTrie.this.get(prefix); diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/MultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/MultiTrie.java index 577a9f363..90d3f30b3 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/MultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/MultiTrie.java @@ -20,6 +20,7 @@ default Node start(K key) { return this.getRoot().next(key); } + @Nonnull Node get(S prefix); default long getSize() { diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/StringMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/StringMultiTrie.java index 41cfae55c..3855fe3da 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/StringMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/StringMultiTrie.java @@ -3,6 +3,7 @@ import com.google.common.collect.Multimap; import org.quiltmc.enigma.util.collection.trie.StringMultiTrie.Node; +import javax.annotation.Nonnull; import java.util.Map; public class StringMultiTrie> @@ -11,6 +12,7 @@ protected StringMultiTrie(N root) { super(root); } + @Nonnull @Override public MultiTrie.Node get(String prefix) { N node = this.root; From 21f89019ec39d7f402837a02de0c5d2e4525f174 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sun, 9 Nov 2025 22:01:44 -0800 Subject: [PATCH 007/124] promote View --- .../trie/AbstractMutableMapMultiTrie.java | 16 +----------- .../collection/trie/MutableMultiTrie.java | 6 +++++ .../enigma/util/collection/trie/View.java | 25 +++++++++++++++++++ 3 files changed, 32 insertions(+), 15 deletions(-) create mode 100644 enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/View.java diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/AbstractMutableMapMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/AbstractMutableMapMultiTrie.java index a9007df0f..27c6e43b6 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/AbstractMutableMapMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/AbstractMutableMapMultiTrie.java @@ -3,13 +3,12 @@ import org.quiltmc.enigma.util.collection.trie.AbstractMutableMapMultiTrie.Node; import com.google.common.collect.Multimap; -import javax.annotation.Nonnull; import java.util.Map; public abstract class AbstractMutableMapMultiTrie> extends AbstractMapMultiTrie implements MutableMultiTrie { - private final View view = new View(); + private final View view = new View<>(this); protected AbstractMutableMapMultiTrie(N root) { super(root); @@ -100,17 +99,4 @@ public boolean removeAll(S sequence) { protected record FirstSplit(K first, S suffix) { } } - - private class View implements MultiTrie { - @Override - public Node getRoot() { - return AbstractMutableMapMultiTrie.this.root; - } - - @Nonnull - @Override - public Node get(S prefix) { - return AbstractMutableMapMultiTrie.this.get(prefix); - } - } } diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/MutableMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/MutableMultiTrie.java index 85728fc0a..4626baceb 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/MutableMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/MutableMultiTrie.java @@ -7,8 +7,14 @@ public interface MutableMultiTrie extends MultiTrie { boolean removeAll(S sequence); + /** + * @return a live, unmodifiable view of this trie + */ MultiTrie view(); + /** + * @implSpec implementations should not have {@code public} visibility; users should never see node mutation methods + */ interface Node extends MultiTrie.Node { void put(S sequence, V value); diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/View.java b/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/View.java new file mode 100644 index 000000000..1b3dc4818 --- /dev/null +++ b/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/View.java @@ -0,0 +1,25 @@ +package org.quiltmc.enigma.util.collection.trie; + +import javax.annotation.Nonnull; + +/** + * An unmodifiable view of a {@link MutableMultiTrie}, for use in {@link MutableMultiTrie#view()} implementations. + */ +public final class View implements MultiTrie { + private final MutableMultiTrie viewed; + + public View(MutableMultiTrie viewed) { + this.viewed = viewed; + } + + @Override + public Node getRoot() { + return this.viewed.getRoot(); + } + + @Nonnull + @Override + public Node get(S prefix) { + return this.viewed.get(prefix); + } +} From 5ff85f3df2e05cf93c42db0adaa0802cdd95508d Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sun, 9 Nov 2025 22:09:35 -0800 Subject: [PATCH 008/124] require non-null field assignments --- enigma/src/main/java/org/quiltmc/enigma/util/Utils.java | 8 ++++++++ .../enigma/util/collection/trie/AbstractMapMultiTrie.java | 5 +++-- .../org/quiltmc/enigma/util/collection/trie/View.java | 4 +++- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/Utils.java b/enigma/src/main/java/org/quiltmc/enigma/util/Utils.java index d68d1b15b..a44006400 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/Utils.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/Utils.java @@ -196,4 +196,12 @@ public static float clamp(double value, float min, float max) { public static double clamp(double value, double min, double max) { return Math.min(max, Math.max(value, min)); } + + public static T requireNonNull(T value, String name) { + if (value == null) { + throw new NullPointerException(name + " must not be null!"); + } else { + return value; + } + } } diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/AbstractMapMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/AbstractMapMultiTrie.java index d09013798..959b83b05 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/AbstractMapMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/AbstractMapMultiTrie.java @@ -1,6 +1,7 @@ package org.quiltmc.enigma.util.collection.trie; import com.google.common.collect.Multimap; +import org.quiltmc.enigma.util.Utils; import org.quiltmc.enigma.util.collection.trie.AbstractMapMultiTrie.Node; import javax.annotation.Nonnull; @@ -25,8 +26,8 @@ protected static class Node> implements MultiTrie. protected final Multimap leaves; protected Node(Map children, Multimap leaves) { - this.children = children; - this.leaves = leaves; + this.children = Utils.requireNonNull(children, "children"); + this.leaves = Utils.requireNonNull(leaves, "leaves"); } @Override diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/View.java b/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/View.java index 1b3dc4818..e389257e5 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/View.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/View.java @@ -1,5 +1,7 @@ package org.quiltmc.enigma.util.collection.trie; +import org.quiltmc.enigma.util.Utils; + import javax.annotation.Nonnull; /** @@ -9,7 +11,7 @@ public final class View implements MultiTrie { private final MutableMultiTrie viewed; public View(MutableMultiTrie viewed) { - this.viewed = viewed; + this.viewed = Utils.requireNonNull(viewed, "viewed"); } @Override From 28022d49cbbf2bd5eb9d6ecdc8d133c39b520eda Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sun, 9 Nov 2025 22:19:56 -0800 Subject: [PATCH 009/124] rename package collection.trie -> multi_trie --- .../{collection/trie => multi_trie}/AbstractMapMultiTrie.java | 4 ++-- .../trie => multi_trie}/AbstractMutableMapMultiTrie.java | 4 ++-- .../util/{collection/trie => multi_trie}/EmptyNode.java | 2 +- .../util/{collection/trie => multi_trie}/MultiTrie.java | 2 +- .../{collection/trie => multi_trie}/MutableMultiTrie.java | 2 +- .../{collection/trie => multi_trie}/StringHashMultiTrie.java | 4 ++-- .../util/{collection/trie => multi_trie}/StringMultiTrie.java | 4 ++-- .../enigma/util/{collection/trie => multi_trie}/View.java | 2 +- 8 files changed, 12 insertions(+), 12 deletions(-) rename enigma/src/main/java/org/quiltmc/enigma/util/{collection/trie => multi_trie}/AbstractMapMultiTrie.java (92%) rename enigma/src/main/java/org/quiltmc/enigma/util/{collection/trie => multi_trie}/AbstractMutableMapMultiTrie.java (95%) rename enigma/src/main/java/org/quiltmc/enigma/util/{collection/trie => multi_trie}/EmptyNode.java (95%) rename enigma/src/main/java/org/quiltmc/enigma/util/{collection/trie => multi_trie}/MultiTrie.java (95%) rename enigma/src/main/java/org/quiltmc/enigma/util/{collection/trie => multi_trie}/MutableMultiTrie.java (92%) rename enigma/src/main/java/org/quiltmc/enigma/util/{collection/trie => multi_trie}/StringHashMultiTrie.java (84%) rename enigma/src/main/java/org/quiltmc/enigma/util/{collection/trie => multi_trie}/StringMultiTrie.java (89%) rename enigma/src/main/java/org/quiltmc/enigma/util/{collection/trie => multi_trie}/View.java (92%) diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/AbstractMapMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/AbstractMapMultiTrie.java similarity index 92% rename from enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/AbstractMapMultiTrie.java rename to enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/AbstractMapMultiTrie.java index 959b83b05..2c5b29375 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/AbstractMapMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/AbstractMapMultiTrie.java @@ -1,8 +1,8 @@ -package org.quiltmc.enigma.util.collection.trie; +package org.quiltmc.enigma.util.multi_trie; import com.google.common.collect.Multimap; import org.quiltmc.enigma.util.Utils; -import org.quiltmc.enigma.util.collection.trie.AbstractMapMultiTrie.Node; +import org.quiltmc.enigma.util.multi_trie.AbstractMapMultiTrie.Node; import javax.annotation.Nonnull; import javax.annotation.Nullable; diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/AbstractMutableMapMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/AbstractMutableMapMultiTrie.java similarity index 95% rename from enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/AbstractMutableMapMultiTrie.java rename to enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/AbstractMutableMapMultiTrie.java index 27c6e43b6..aec21b9d8 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/AbstractMutableMapMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/AbstractMutableMapMultiTrie.java @@ -1,6 +1,6 @@ -package org.quiltmc.enigma.util.collection.trie; +package org.quiltmc.enigma.util.multi_trie; -import org.quiltmc.enigma.util.collection.trie.AbstractMutableMapMultiTrie.Node; +import org.quiltmc.enigma.util.multi_trie.AbstractMutableMapMultiTrie.Node; import com.google.common.collect.Multimap; import java.util.Map; diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/EmptyNode.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/EmptyNode.java similarity index 95% rename from enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/EmptyNode.java rename to enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/EmptyNode.java index 0ed6bdfef..41d0326c9 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/EmptyNode.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/EmptyNode.java @@ -1,4 +1,4 @@ -package org.quiltmc.enigma.util.collection.trie; +package org.quiltmc.enigma.util.multi_trie; import javax.annotation.Nonnull; import java.util.stream.Stream; diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/MultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MultiTrie.java similarity index 95% rename from enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/MultiTrie.java rename to enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MultiTrie.java index 90d3f30b3..abe26b6bc 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/MultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MultiTrie.java @@ -1,4 +1,4 @@ -package org.quiltmc.enigma.util.collection.trie; +package org.quiltmc.enigma.util.multi_trie; import javax.annotation.Nonnull; import java.util.stream.Stream; diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/MutableMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMultiTrie.java similarity index 92% rename from enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/MutableMultiTrie.java rename to enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMultiTrie.java index 4626baceb..ac5ab25ba 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/MutableMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMultiTrie.java @@ -1,4 +1,4 @@ -package org.quiltmc.enigma.util.collection.trie; +package org.quiltmc.enigma.util.multi_trie; public interface MutableMultiTrie extends MultiTrie { void put(S sequence, V value); diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/StringHashMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringHashMultiTrie.java similarity index 84% rename from enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/StringHashMultiTrie.java rename to enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringHashMultiTrie.java index 3d12ef1bc..c7929e768 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/StringHashMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringHashMultiTrie.java @@ -1,8 +1,8 @@ -package org.quiltmc.enigma.util.collection.trie; +package org.quiltmc.enigma.util.multi_trie; import com.google.common.collect.HashMultimap; import com.google.common.collect.Multimap; -import org.quiltmc.enigma.util.collection.trie.StringHashMultiTrie.Node; +import org.quiltmc.enigma.util.multi_trie.StringHashMultiTrie.Node; import java.util.HashMap; import java.util.Map; diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/StringMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java similarity index 89% rename from enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/StringMultiTrie.java rename to enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java index 3855fe3da..859709b5a 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/StringMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java @@ -1,7 +1,7 @@ -package org.quiltmc.enigma.util.collection.trie; +package org.quiltmc.enigma.util.multi_trie; import com.google.common.collect.Multimap; -import org.quiltmc.enigma.util.collection.trie.StringMultiTrie.Node; +import org.quiltmc.enigma.util.multi_trie.StringMultiTrie.Node; import javax.annotation.Nonnull; import java.util.Map; diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/View.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/View.java similarity index 92% rename from enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/View.java rename to enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/View.java index e389257e5..f7aff16a5 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/collection/trie/View.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/View.java @@ -1,4 +1,4 @@ -package org.quiltmc.enigma.util.collection.trie; +package org.quiltmc.enigma.util.multi_trie; import org.quiltmc.enigma.util.Utils; From 8a88bc74b1f25d1063d38c2ef8412c8c7216aef0 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sun, 9 Nov 2025 22:23:40 -0800 Subject: [PATCH 010/124] fix accidentally returning null root --- .../quiltmc/enigma/util/multi_trie/AbstractMapMultiTrie.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/AbstractMapMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/AbstractMapMultiTrie.java index 2c5b29375..4f7a528be 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/AbstractMapMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/AbstractMapMultiTrie.java @@ -18,7 +18,7 @@ protected AbstractMapMultiTrie(N root) { @Override public MultiTrie.Node getRoot() { - return null; + return this.root; } protected static class Node> implements MultiTrie.Node { From 4cb0dd04f5209306afa763fb2b644b015cff0892 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sun, 9 Nov 2025 22:25:08 -0800 Subject: [PATCH 011/124] require non-null root field assignment --- .../quiltmc/enigma/util/multi_trie/AbstractMapMultiTrie.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/AbstractMapMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/AbstractMapMultiTrie.java index 4f7a528be..6702056a4 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/AbstractMapMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/AbstractMapMultiTrie.java @@ -13,7 +13,7 @@ public abstract class AbstractMapMultiTrie> imp protected final N root; protected AbstractMapMultiTrie(N root) { - this.root = root; + this.root = Utils.requireNonNull(root, "root"); } @Override From 40b29366d62bfc30d52912064b0d1eae3c03081d Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sun, 9 Nov 2025 22:37:47 -0800 Subject: [PATCH 012/124] rename view -> getView --- .../enigma/util/multi_trie/AbstractMutableMapMultiTrie.java | 2 +- .../org/quiltmc/enigma/util/multi_trie/MutableMultiTrie.java | 2 +- .../src/main/java/org/quiltmc/enigma/util/multi_trie/View.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/AbstractMutableMapMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/AbstractMutableMapMultiTrie.java index aec21b9d8..55cebd1f1 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/AbstractMutableMapMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/AbstractMutableMapMultiTrie.java @@ -30,7 +30,7 @@ public boolean removeAll(S sequence) { } @Override - public MultiTrie view() { + public MultiTrie getView() { return this.view; } diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMultiTrie.java index ac5ab25ba..85a4c1c95 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMultiTrie.java @@ -10,7 +10,7 @@ public interface MutableMultiTrie extends MultiTrie { /** * @return a live, unmodifiable view of this trie */ - MultiTrie view(); + MultiTrie getView(); /** * @implSpec implementations should not have {@code public} visibility; users should never see node mutation methods diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/View.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/View.java index f7aff16a5..871cda005 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/View.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/View.java @@ -5,7 +5,7 @@ import javax.annotation.Nonnull; /** - * An unmodifiable view of a {@link MutableMultiTrie}, for use in {@link MutableMultiTrie#view()} implementations. + * An unmodifiable view of a {@link MutableMultiTrie}, for use in {@link MutableMultiTrie#getView()} implementations. */ public final class View implements MultiTrie { private final MutableMultiTrie viewed; From da5727ff1f658bc61ca20f005fd2f1b5fb4431ea Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sun, 9 Nov 2025 22:38:35 -0800 Subject: [PATCH 013/124] improve javadoc --- .../src/main/java/org/quiltmc/enigma/util/multi_trie/View.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/View.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/View.java index 871cda005..ef25b80b6 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/View.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/View.java @@ -5,7 +5,8 @@ import javax.annotation.Nonnull; /** - * An unmodifiable view of a {@link MutableMultiTrie}, for use in {@link MutableMultiTrie#getView()} implementations. + * A live, unmodifiable view of a {@link MutableMultiTrie}, + * for use in {@link MutableMultiTrie#getView()} implementations. */ public final class View implements MultiTrie { private final MutableMultiTrie viewed; From 2a7b163a28ab7972a7cbe2fe0590a292bda98029 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Mon, 10 Nov 2025 18:03:25 -0800 Subject: [PATCH 014/124] annotate createEmpty --- .../util/multi_trie/AbstractMutableMapMultiTrie.java | 7 +++++++ .../enigma/util/multi_trie/StringHashMultiTrie.java | 2 ++ 2 files changed, 9 insertions(+) diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/AbstractMutableMapMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/AbstractMutableMapMultiTrie.java index 55cebd1f1..f25bb3709 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/AbstractMutableMapMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/AbstractMutableMapMultiTrie.java @@ -1,8 +1,10 @@ package org.quiltmc.enigma.util.multi_trie; +import org.checkerframework.dataflow.qual.Pure; import org.quiltmc.enigma.util.multi_trie.AbstractMutableMapMultiTrie.Node; import com.google.common.collect.Multimap; +import javax.annotation.Nonnull; import java.util.Map; public abstract class AbstractMutableMapMultiTrie> @@ -85,6 +87,11 @@ public boolean removeAll(S sequence) { } } + /** + * @return a new, empty node instance + */ + @Nonnull + @Pure protected abstract N createEmpty(); /** diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringHashMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringHashMultiTrie.java index c7929e768..a62f3ebd7 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringHashMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringHashMultiTrie.java @@ -4,6 +4,7 @@ import com.google.common.collect.Multimap; import org.quiltmc.enigma.util.multi_trie.StringHashMultiTrie.Node; +import javax.annotation.Nonnull; import java.util.HashMap; import java.util.Map; @@ -22,6 +23,7 @@ private Node(Map> children, Multimap leaves) { } @Override + @Nonnull protected Node createEmpty() { return createEmptyNode(); } From bccc03af36827a9fd63b4467b32708dea8d1a1dd Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Mon, 10 Nov 2025 18:08:43 -0800 Subject: [PATCH 015/124] refactor+rename StringHashMultiTrie -> CompositeStringMultiTrie --- .../multi_trie/CompositeStringMultiTrie.java | 46 +++++++++++++++++++ .../util/multi_trie/StringHashMultiTrie.java | 31 ------------- 2 files changed, 46 insertions(+), 31 deletions(-) create mode 100644 enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java delete mode 100644 enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringHashMultiTrie.java diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java new file mode 100644 index 000000000..6ccb30b00 --- /dev/null +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java @@ -0,0 +1,46 @@ +package org.quiltmc.enigma.util.multi_trie; + +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; +import org.quiltmc.enigma.util.multi_trie.CompositeStringMultiTrie.Node; + +import javax.annotation.Nonnull; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Supplier; + +public final class CompositeStringMultiTrie extends StringMultiTrie> { + public static CompositeStringMultiTrie createHashed() { + return new CompositeStringMultiTrie<>(HashMap::new, HashMultimap::create); + } + + private static Node createNode( + Supplier>> childrenFactory, + Supplier> leavesFactory + ) { + return new Node<>(childrenFactory.get(), leavesFactory.get(), () -> createNode(childrenFactory, leavesFactory)); + } + + private CompositeStringMultiTrie( + Supplier>> childrenFactory, + Supplier> leavesFactory + ) { + super(createNode(childrenFactory, leavesFactory)); + } + + protected static final class Node extends StringMultiTrie.Node> { + private final Supplier> factory; + + private Node(Map> children, Multimap leaves, Supplier> factory) { + super(children, leaves); + + this.factory = factory; + } + + @Nonnull + @Override + protected Node createEmpty() { + return this.factory.get(); + } + } +} diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringHashMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringHashMultiTrie.java deleted file mode 100644 index a62f3ebd7..000000000 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringHashMultiTrie.java +++ /dev/null @@ -1,31 +0,0 @@ -package org.quiltmc.enigma.util.multi_trie; - -import com.google.common.collect.HashMultimap; -import com.google.common.collect.Multimap; -import org.quiltmc.enigma.util.multi_trie.StringHashMultiTrie.Node; - -import javax.annotation.Nonnull; -import java.util.HashMap; -import java.util.Map; - -public final class StringHashMultiTrie extends StringMultiTrie> { - private static Node createEmptyNode() { - return new Node<>(new HashMap<>(), HashMultimap.create()); - } - - public StringHashMultiTrie() { - super(createEmptyNode()); - } - - static final class Node extends StringMultiTrie.Node> { - private Node(Map> children, Multimap leaves) { - super(children, leaves); - } - - @Override - @Nonnull - protected Node createEmpty() { - return createEmptyNode(); - } - } -} From 078db3ba193ee37257378de59d907c5bada4085b Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Mon, 10 Nov 2025 18:57:41 -0800 Subject: [PATCH 016/124] add CompositeStringMultiTrieTest fix storing leaves a level too high fix overriting children instead of adding to them --- .../util/multi_trie/AbstractMapMultiTrie.java | 10 +- .../AbstractMutableMapMultiTrie.java | 64 ++++++------ .../multi_trie/CompositeStringMultiTrie.java | 12 +-- .../util/multi_trie/MutableMultiTrie.java | 2 + .../util/multi_trie/StringMultiTrie.java | 4 +- .../util/CompositeStringMultiTrieTest.java | 98 +++++++++++++++++++ 6 files changed, 146 insertions(+), 44 deletions(-) create mode 100644 enigma/src/test/java/org/quiltmc/enigma/util/CompositeStringMultiTrieTest.java diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/AbstractMapMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/AbstractMapMultiTrie.java index 6702056a4..0840fc017 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/AbstractMapMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/AbstractMapMultiTrie.java @@ -1,11 +1,11 @@ package org.quiltmc.enigma.util.multi_trie; -import com.google.common.collect.Multimap; import org.quiltmc.enigma.util.Utils; import org.quiltmc.enigma.util.multi_trie.AbstractMapMultiTrie.Node; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import java.util.Collection; import java.util.Map; import java.util.stream.Stream; @@ -23,16 +23,16 @@ public MultiTrie.Node getRoot() { protected static class Node> implements MultiTrie.Node { protected final Map children; - protected final Multimap leaves; + protected final Collection leaves; - protected Node(Map children, Multimap leaves) { + protected Node(Map children, Collection leaves) { this.children = Utils.requireNonNull(children, "children"); this.leaves = Utils.requireNonNull(leaves, "leaves"); } @Override public Stream streamLeaves() { - return this.leaves.values().stream(); + return this.leaves.stream(); } @Override @@ -48,7 +48,7 @@ public Stream streamValues() { @Override @Nonnull public MultiTrie.Node next(K key) { - final Node next = this.nextImpl(key); + final Node next = this.nextImpl(Utils.requireNonNull(key, "key")); return next == null ? EmptyNode.get() : next; } diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/AbstractMutableMapMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/AbstractMutableMapMultiTrie.java index f25bb3709..7e63d507e 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/AbstractMutableMapMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/AbstractMutableMapMultiTrie.java @@ -1,15 +1,19 @@ package org.quiltmc.enigma.util.multi_trie; import org.checkerframework.dataflow.qual.Pure; +import org.quiltmc.enigma.util.Utils; import org.quiltmc.enigma.util.multi_trie.AbstractMutableMapMultiTrie.Node; -import com.google.common.collect.Multimap; import javax.annotation.Nonnull; +import java.util.Collection; import java.util.Map; public abstract class AbstractMutableMapMultiTrie> extends AbstractMapMultiTrie implements MutableMultiTrie { + private static final String SEQUENCE = "sequence"; + private static final String VALUE = "value"; + private final View view = new View<>(this); protected AbstractMutableMapMultiTrie(N root) { @@ -18,17 +22,17 @@ protected AbstractMutableMapMultiTrie(N root) { @Override public void put(S sequence, V value) { - this.root.put(sequence, value); + this.root.put(Utils.requireNonNull(sequence, SEQUENCE), Utils.requireNonNull(value, VALUE)); } @Override public boolean remove(S sequence, V value) { - return this.root.remove(sequence, value); + return this.root.remove(Utils.requireNonNull(sequence, SEQUENCE), Utils.requireNonNull(value, VALUE)); } @Override public boolean removeAll(S sequence) { - return this.root.removeAll(sequence); + return this.root.removeAll(Utils.requireNonNull(sequence, SEQUENCE)); } @Override @@ -39,51 +43,49 @@ public MultiTrie getView() { protected abstract static class Node> extends AbstractMapMultiTrie.Node implements MutableMultiTrie.Node { - protected Node(Map children, Multimap leaves) { + protected Node(Map children, Collection leaves) { super(children, leaves); } @Override public void put(S sequence, V value) { - final FirstSplit split = this.splitFirst(sequence); - if (this.isEmptySequence(split.suffix)) { - this.leaves.put(split.first, value); + if (this.isEmptySequence(sequence)) { + this.leaves.add(value); } else { - final N empty = this.createEmpty(); - empty.put(split.suffix, value); - this.children.put(split.first, empty); + final FirstSplit split = this.splitFirst(sequence); + + this.children + .computeIfAbsent(split.first, ignored -> this.createEmpty()) + .put(split.suffix, value); } } @Override public boolean remove(S sequence, V value) { - final FirstSplit split = this.splitFirst(sequence); - if (this.isEmptySequence(split.suffix)) { - return this.leaves.remove(split.first, value); + if (this.isEmptySequence(sequence)) { + return this.leaves.remove(value); } else { - for (final N child : this.children.values()) { - if (child.remove(split.suffix, value)) { - return true; - } - } - - return false; + final FirstSplit split = this.splitFirst(sequence); + final N next = this.nextImpl(split.first); + return next != null && next.remove(split.suffix, value); } } @Override public boolean removeAll(S sequence) { - final FirstSplit split = this.splitFirst(sequence); - if (this.isEmptySequence(split.suffix)) { - return !this.leaves.removeAll(split.first).isEmpty(); - } else { - for (final N child : this.children.values()) { - if (child.removeAll(split.suffix)) { - return true; - } + if (this.isEmptySequence(sequence)) { + if (this.leaves.isEmpty()) { + // this should only happen for roots + return false; + } else { + this.leaves.clear(); + + return true; } - - return false; + } else { + final FirstSplit split = this.splitFirst(sequence); + final N next = this.nextImpl(split.first); + return next != null && next.removeAll(split.suffix); } } diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java index 6ccb30b00..2ae22a1cf 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java @@ -1,29 +1,29 @@ package org.quiltmc.enigma.util.multi_trie; -import com.google.common.collect.HashMultimap; -import com.google.common.collect.Multimap; import org.quiltmc.enigma.util.multi_trie.CompositeStringMultiTrie.Node; import javax.annotation.Nonnull; +import java.util.Collection; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; import java.util.function.Supplier; public final class CompositeStringMultiTrie extends StringMultiTrie> { public static CompositeStringMultiTrie createHashed() { - return new CompositeStringMultiTrie<>(HashMap::new, HashMultimap::create); + return new CompositeStringMultiTrie<>(HashMap::new, HashSet::new); } private static Node createNode( Supplier>> childrenFactory, - Supplier> leavesFactory + Supplier> leavesFactory ) { return new Node<>(childrenFactory.get(), leavesFactory.get(), () -> createNode(childrenFactory, leavesFactory)); } private CompositeStringMultiTrie( Supplier>> childrenFactory, - Supplier> leavesFactory + Supplier> leavesFactory ) { super(createNode(childrenFactory, leavesFactory)); } @@ -31,7 +31,7 @@ private CompositeStringMultiTrie( protected static final class Node extends StringMultiTrie.Node> { private final Supplier> factory; - private Node(Map> children, Multimap leaves, Supplier> factory) { + private Node(Map> children, Collection leaves, Supplier> factory) { super(children, leaves); this.factory = factory; diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMultiTrie.java index 85a4c1c95..0c0766b53 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMultiTrie.java @@ -21,5 +21,7 @@ interface Node extends MultiTrie.Node { boolean remove(S sequence, V value); boolean removeAll(S sequence); + + // TODO trim method, for empty nodes } } diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java index 859709b5a..a4c654712 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java @@ -1,9 +1,9 @@ package org.quiltmc.enigma.util.multi_trie; -import com.google.common.collect.Multimap; import org.quiltmc.enigma.util.multi_trie.StringMultiTrie.Node; import javax.annotation.Nonnull; +import java.util.Collection; import java.util.Map; public class StringMultiTrie> @@ -25,7 +25,7 @@ public MultiTrie.Node get(String prefix) { protected abstract static class Node> extends AbstractMutableMapMultiTrie.Node { - protected Node(Map children, Multimap leaves) { + protected Node(Map children, Collection leaves) { super(children, leaves); } diff --git a/enigma/src/test/java/org/quiltmc/enigma/util/CompositeStringMultiTrieTest.java b/enigma/src/test/java/org/quiltmc/enigma/util/CompositeStringMultiTrieTest.java new file mode 100644 index 000000000..018663bab --- /dev/null +++ b/enigma/src/test/java/org/quiltmc/enigma/util/CompositeStringMultiTrieTest.java @@ -0,0 +1,98 @@ +package org.quiltmc.enigma.util; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMultimap; +import org.junit.jupiter.api.Test; +import org.quiltmc.enigma.util.multi_trie.MultiTrie.Node; +import org.quiltmc.enigma.util.multi_trie.CompositeStringMultiTrie; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.arrayContainingInAnyOrder; + +public class CompositeStringMultiTrieTest { + @Test + void testAssociatedPrefixes() { + final CompositeStringMultiTrie trie = CompositeStringMultiTrie.createHashed(); + + Association.BY_PREFIX.values().stream().distinct().forEach(association -> { + trie.put(association.key, association); + }); + + Association.BY_PREFIX.asMap().forEach((prefix, associations) -> { + final Node node = trie.get(prefix); + + assertThat( + "Unexpected values for prefix " + prefix, + node.streamValues().toArray(), + arrayContainingInAnyOrder(associations.toArray()) + ); + + assertThat( + "Unexpected leaves for prefix " + prefix, + node.streamLeaves().toArray(), + arrayContainingInAnyOrder(associations.stream().filter(a -> a.isLeafOf(prefix)).toArray()) + ); + + assertThat( + "Unexpected branches for prefix " + prefix, + node.streamBranches().toArray(), + arrayContainingInAnyOrder(associations.stream().filter(a -> a.isBranchOf(prefix)).toArray()) + ); + }); + } + + record Association(String key) { + static final Association A = new Association("A"); + static final Association AB = new Association("AB"); + static final Association ABC = new Association("ABC"); + + static final Association BA = new Association("BA"); + static final Association CBA = new Association("CBA"); + + static final Association I = new Association("I"); + static final Association II = new Association("II"); + static final Association III = new Association("III"); + + static final Association ONE = new Association("ONE"); + static final Association TWO = new Association("TWO"); + static final Association THREE = new Association("THREE"); + + static final Association ENO = new Association("ENO"); + static final Association OWT = new Association("OWT"); + static final Association EERHT = new Association("EERHT"); + + static final ImmutableList NON_EMPTY = ImmutableList.of( + A, AB, ABC, + BA, CBA, + I, II, III, + ONE, TWO, THREE, + ENO, OWT, EERHT + ); + + static final ImmutableMultimap BY_PREFIX; + + static { + final ImmutableMultimap.Builder byPrefix = ImmutableMultimap.builder(); + + NON_EMPTY.forEach(association -> { + if (association.key.isEmpty()) { + throw new IllegalStateException("NON_EMPTY contains empty association!"); + } + + for (int i = 1; i <= association.key.length(); i++) { + byPrefix.put(association.key.substring(0, i), association); + } + }); + + BY_PREFIX = byPrefix.build(); + } + + boolean isLeafOf(String prefix) { + return this.key.equals(prefix); + } + + boolean isBranchOf(String prefix) { + return this.key.length() > prefix.length() && this.key.startsWith(prefix); + } + } +} From 7aa948c742ca7e641ff0f21524a1b9ea1838ac3c Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Mon, 10 Nov 2025 19:15:01 -0800 Subject: [PATCH 017/124] test empty string key --- .../util/CompositeStringMultiTrieTest.java | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/enigma/src/test/java/org/quiltmc/enigma/util/CompositeStringMultiTrieTest.java b/enigma/src/test/java/org/quiltmc/enigma/util/CompositeStringMultiTrieTest.java index 018663bab..ed9251737 100644 --- a/enigma/src/test/java/org/quiltmc/enigma/util/CompositeStringMultiTrieTest.java +++ b/enigma/src/test/java/org/quiltmc/enigma/util/CompositeStringMultiTrieTest.java @@ -22,19 +22,19 @@ void testAssociatedPrefixes() { final Node node = trie.get(prefix); assertThat( - "Unexpected values for prefix " + prefix, + "Unexpected values for prefix \"%s\"".formatted(prefix), node.streamValues().toArray(), arrayContainingInAnyOrder(associations.toArray()) ); assertThat( - "Unexpected leaves for prefix " + prefix, + "Unexpected leaves for prefix \"%s\"".formatted(prefix), node.streamLeaves().toArray(), arrayContainingInAnyOrder(associations.stream().filter(a -> a.isLeafOf(prefix)).toArray()) ); assertThat( - "Unexpected branches for prefix " + prefix, + "Unexpected branches for prefix \"%s\"".formatted(prefix), node.streamBranches().toArray(), arrayContainingInAnyOrder(associations.stream().filter(a -> a.isBranchOf(prefix)).toArray()) ); @@ -42,6 +42,8 @@ void testAssociatedPrefixes() { } record Association(String key) { + static final Association EMPTY = new Association(""); + static final Association A = new Association("A"); static final Association AB = new Association("AB"); static final Association ABC = new Association("ABC"); @@ -61,7 +63,8 @@ record Association(String key) { static final Association OWT = new Association("OWT"); static final Association EERHT = new Association("EERHT"); - static final ImmutableList NON_EMPTY = ImmutableList.of( + static final ImmutableList ALL = ImmutableList.of( + EMPTY, A, AB, ABC, BA, CBA, I, II, III, @@ -74,12 +77,8 @@ record Association(String key) { static { final ImmutableMultimap.Builder byPrefix = ImmutableMultimap.builder(); - NON_EMPTY.forEach(association -> { - if (association.key.isEmpty()) { - throw new IllegalStateException("NON_EMPTY contains empty association!"); - } - - for (int i = 1; i <= association.key.length(); i++) { + ALL.forEach(association -> { + for (int i = 0; i <= association.key.length(); i++) { byPrefix.put(association.key.substring(0, i), association); } }); From 5415f3efe9219f945e623d0eef3f7ccbb48f6b77 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Mon, 10 Nov 2025 19:35:22 -0800 Subject: [PATCH 018/124] propoerly conceal node mutation methods --- .../AbstractMutableMapMultiTrie.java | 19 +++++++++++ .../util/multi_trie/MutableMultiTrie.java | 8 +++-- .../enigma/util/multi_trie/NodeView.java | 33 +++++++++++++++++++ .../util/multi_trie/StringMultiTrie.java | 2 +- 4 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/NodeView.java diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/AbstractMutableMapMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/AbstractMutableMapMultiTrie.java index 7e63d507e..f2550fb83 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/AbstractMutableMapMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/AbstractMutableMapMultiTrie.java @@ -20,6 +20,17 @@ protected AbstractMutableMapMultiTrie(N root) { super(root); } + @Override + public MultiTrie.Node getRoot() { + return this.root.getView(); + } + + @Override + public MultiTrie.Node start(K key) { + final N next = this.root.nextImpl(key); + return next == null ? EmptyNode.get() : next.getView(); + } + @Override public void put(S sequence, V value) { this.root.put(Utils.requireNonNull(sequence, SEQUENCE), Utils.requireNonNull(value, VALUE)); @@ -43,6 +54,8 @@ public MultiTrie getView() { protected abstract static class Node> extends AbstractMapMultiTrie.Node implements MutableMultiTrie.Node { + private final NodeView view = new NodeView<>(this); + protected Node(Map children, Collection leaves) { super(children, leaves); } @@ -60,6 +73,7 @@ public void put(S sequence, V value) { } } + // TODO trim from parent when empty @Override public boolean remove(S sequence, V value) { if (this.isEmptySequence(sequence)) { @@ -89,6 +103,11 @@ public boolean removeAll(S sequence) { } } + @Override + public MultiTrie.Node getView() { + return this.view; + } + /** * @return a new, empty node instance */ diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMultiTrie.java index 0c0766b53..7ad1f326b 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMultiTrie.java @@ -13,7 +13,11 @@ public interface MutableMultiTrie extends MultiTrie { MultiTrie getView(); /** - * @implSpec implementations should not have {@code public} visibility; users should never see node mutation methods + * @implSpec mutable nodes should not be returned from public methods, return a + * {@linkplain #getView() view} instead; users should never see node mutation methods + * + * @implNote most implementations should remove themselves from any + * backing data structures when the node becomes empty */ interface Node extends MultiTrie.Node { void put(S sequence, V value); @@ -22,6 +26,6 @@ interface Node extends MultiTrie.Node { boolean removeAll(S sequence); - // TODO trim method, for empty nodes + MultiTrie.Node getView(); } } diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/NodeView.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/NodeView.java new file mode 100644 index 000000000..d482aa550 --- /dev/null +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/NodeView.java @@ -0,0 +1,33 @@ +package org.quiltmc.enigma.util.multi_trie; + +import javax.annotation.Nonnull; +import java.util.stream.Stream; + +public class NodeView implements MultiTrie.Node { + private final MutableMultiTrie.Node viewed; + + public NodeView(MutableMultiTrie.Node viewed) { + this.viewed = viewed; + } + + @Override + public Stream streamLeaves() { + return this.viewed.streamLeaves(); + } + + @Override + public Stream streamBranches() { + return this.viewed.streamBranches(); + } + + @Override + public Stream streamValues() { + return this.viewed.streamValues(); + } + + @Nonnull + @Override + public MultiTrie.Node next(K key) { + return this.viewed.next(key); + } +} diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java index a4c654712..68edce8c2 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java @@ -20,7 +20,7 @@ public MultiTrie.Node get(String prefix) { node = node.nextImpl(prefix.charAt(i)); } - return node == null ? EmptyNode.get() : node; + return node == null ? EmptyNode.get() : node.getView(); } protected abstract static class Node> From 0f99ef96c3e71a38a7a3855fcbb43dee053d264e Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Tue, 11 Nov 2025 09:27:01 -0800 Subject: [PATCH 019/124] implement empty node pruning improve javadocs --- .../util/multi_trie/AbstractMapMultiTrie.java | 12 +++- .../AbstractMutableMapMultiTrie.java | 62 ++++++++++++++----- .../multi_trie/CompositeStringMultiTrie.java | 37 +++++++---- .../enigma/util/multi_trie/MultiTrie.java | 16 +++-- .../util/multi_trie/MutableMultiTrie.java | 10 +++ .../util/multi_trie/StringMultiTrie.java | 7 ++- .../quiltmc/enigma/util/multi_trie/View.java | 1 + .../util/CompositeStringMultiTrieTest.java | 6 +- 8 files changed, 111 insertions(+), 40 deletions(-) diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/AbstractMapMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/AbstractMapMultiTrie.java index 0840fc017..47044d168 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/AbstractMapMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/AbstractMapMultiTrie.java @@ -1,12 +1,12 @@ package org.quiltmc.enigma.util.multi_trie; +import com.google.common.collect.BiMap; import org.quiltmc.enigma.util.Utils; import org.quiltmc.enigma.util.multi_trie.AbstractMapMultiTrie.Node; import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.util.Collection; -import java.util.Map; import java.util.stream.Stream; public abstract class AbstractMapMultiTrie> implements MultiTrie { @@ -16,16 +16,22 @@ protected AbstractMapMultiTrie(N root) { this.root = Utils.requireNonNull(root, "root"); } + @Nonnull @Override public MultiTrie.Node getRoot() { return this.root; } + /** + * @param the type of keys + * @param the type of values + * @param the type of this node + */ protected static class Node> implements MultiTrie.Node { - protected final Map children; + protected final BiMap children; protected final Collection leaves; - protected Node(Map children, Collection leaves) { + protected Node(BiMap children, Collection leaves) { this.children = Utils.requireNonNull(children, "children"); this.leaves = Utils.requireNonNull(leaves, "leaves"); } diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/AbstractMutableMapMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/AbstractMutableMapMultiTrie.java index f2550fb83..80bd2f891 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/AbstractMutableMapMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/AbstractMutableMapMultiTrie.java @@ -1,12 +1,13 @@ package org.quiltmc.enigma.util.multi_trie; +import com.google.common.collect.BiMap; import org.checkerframework.dataflow.qual.Pure; import org.quiltmc.enigma.util.Utils; import org.quiltmc.enigma.util.multi_trie.AbstractMutableMapMultiTrie.Node; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.util.Collection; -import java.util.Map; public abstract class AbstractMutableMapMultiTrie> extends AbstractMapMultiTrie @@ -20,17 +21,12 @@ protected AbstractMutableMapMultiTrie(N root) { super(root); } + @Nonnull @Override public MultiTrie.Node getRoot() { return this.root.getView(); } - @Override - public MultiTrie.Node start(K key) { - final N next = this.root.nextImpl(key); - return next == null ? EmptyNode.get() : next.getView(); - } - @Override public void put(S sequence, V value) { this.root.put(Utils.requireNonNull(sequence, SEQUENCE), Utils.requireNonNull(value, VALUE)); @@ -54,10 +50,14 @@ public MultiTrie getView() { protected abstract static class Node> extends AbstractMapMultiTrie.Node implements MutableMultiTrie.Node { + @Nullable + protected final Node parent; + private final NodeView view = new NodeView<>(this); - protected Node(Map children, Collection leaves) { + protected Node(@Nullable Node parent, BiMap children, Collection leaves) { super(children, leaves); + this.parent = parent; } @Override @@ -73,9 +73,36 @@ public void put(S sequence, V value) { } } - // TODO trim from parent when empty @Override public boolean remove(S sequence, V value) { + final boolean removed = this.removeImpl(sequence, value); + if (removed) { + this.pruneIfEmpty(); + + return true; + } else { + return false; + } + } + + @Override + public boolean removeAll(S sequence) { + final boolean removed = this.removeAllImpl(sequence); + if (removed) { + this.pruneIfEmpty(); + + return true; + } else { + return false; + } + } + + @Override + public MultiTrie.Node getView() { + return this.view; + } + + protected boolean removeImpl(S sequence, V value) { if (this.isEmptySequence(sequence)) { return this.leaves.remove(value); } else { @@ -85,11 +112,9 @@ public boolean remove(S sequence, V value) { } } - @Override - public boolean removeAll(S sequence) { + protected boolean removeAllImpl(S sequence) { if (this.isEmptySequence(sequence)) { if (this.leaves.isEmpty()) { - // this should only happen for roots return false; } else { this.leaves.clear(); @@ -103,11 +128,18 @@ public boolean removeAll(S sequence) { } } - @Override - public MultiTrie.Node getView() { - return this.view; + protected void pruneIfEmpty() { + if (this.parent != null && this.isEmpty()) { + this.parent.children.inverse().remove(this.getSelf()); + } } + /** + * @return this node + */ + @Pure + protected abstract N getSelf(); + /** * @return a new, empty node instance */ diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java index 2ae22a1cf..3f006a5e1 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java @@ -1,46 +1,61 @@ package org.quiltmc.enigma.util.multi_trie; +import com.google.common.collect.BiMap; +import com.google.common.collect.HashBiMap; import org.quiltmc.enigma.util.multi_trie.CompositeStringMultiTrie.Node; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.util.Collection; -import java.util.HashMap; import java.util.HashSet; -import java.util.Map; import java.util.function.Supplier; +import java.util.function.UnaryOperator; public final class CompositeStringMultiTrie extends StringMultiTrie> { public static CompositeStringMultiTrie createHashed() { - return new CompositeStringMultiTrie<>(HashMap::new, HashSet::new); + return new CompositeStringMultiTrie<>(HashBiMap::create, HashSet::new); } private static Node createNode( - Supplier>> childrenFactory, + @Nullable Node parent, + Supplier>> childrenFactory, Supplier> leavesFactory ) { - return new Node<>(childrenFactory.get(), leavesFactory.get(), () -> createNode(childrenFactory, leavesFactory)); + return new Node<>( + parent, + childrenFactory.get(), leavesFactory.get(), + self -> createNode(self, childrenFactory, leavesFactory) + ); } private CompositeStringMultiTrie( - Supplier>> childrenFactory, + Supplier>> childrenFactory, Supplier> leavesFactory ) { - super(createNode(childrenFactory, leavesFactory)); + super(createNode(null, childrenFactory, leavesFactory)); } protected static final class Node extends StringMultiTrie.Node> { - private final Supplier> factory; + private final UnaryOperator> factory; - private Node(Map> children, Collection leaves, Supplier> factory) { - super(children, leaves); + private Node( + @Nullable Node parent, BiMap> children, Collection leaves, + UnaryOperator> factory + ) { + super(parent, children, leaves); this.factory = factory; } + @Override + protected Node getSelf() { + return this; + } + @Nonnull @Override protected Node createEmpty() { - return this.factory.get(); + return this.factory.apply(this); } } } diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MultiTrie.java index abe26b6bc..86210a1e9 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MultiTrie.java @@ -7,19 +7,21 @@ * A multi-trie (or prefix tree) associates a sequence of keys with one or more values. * *

Values can be looked up by a prefix of their key sequence; all values associated with a sequence beginning with - * the prefix will be returned. + * the prefix will be returned.
+ * The prefix can be passed either all at once to {@link #get}, + * or key-by-key to {@link Node#next} starting with {@link #getRoot}. + * + * @implSpec {@code S} sequence types should represent an ordered sequence of keys of type {@code K}; + * sequences that represent the same sequence of keys should be equivalent * * @param the type of keys * @param the type of sequences * @param the type of values */ public interface MultiTrie { + @Nonnull Node getRoot(); - default Node start(K key) { - return this.getRoot().next(key); - } - @Nonnull Node get(S prefix); @@ -31,6 +33,10 @@ default boolean isEmpty() { return this.getSize() == 0; } + /** + * @param the type of keys + * @param the type of values + */ interface Node { Stream streamLeaves(); Stream streamBranches(); diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMultiTrie.java index 7ad1f326b..c9cb64b41 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMultiTrie.java @@ -1,5 +1,12 @@ package org.quiltmc.enigma.util.multi_trie; +/** + * A multi-trie that allows modification which can also provide unmodifiable views of its contents. + * + * @param the type of keys + * @param the type of sequences + * @param the type of values + */ public interface MutableMultiTrie extends MultiTrie { void put(S sequence, V value); @@ -26,6 +33,9 @@ interface Node extends MultiTrie.Node { boolean removeAll(S sequence); + /** + * @return al live, unmodifiable view of this node + */ MultiTrie.Node getView(); } } diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java index 68edce8c2..0b1c57cc1 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java @@ -1,10 +1,11 @@ package org.quiltmc.enigma.util.multi_trie; +import com.google.common.collect.BiMap; import org.quiltmc.enigma.util.multi_trie.StringMultiTrie.Node; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.util.Collection; -import java.util.Map; public class StringMultiTrie> extends AbstractMutableMapMultiTrie { @@ -25,8 +26,8 @@ public MultiTrie.Node get(String prefix) { protected abstract static class Node> extends AbstractMutableMapMultiTrie.Node { - protected Node(Map children, Collection leaves) { - super(children, leaves); + protected Node(@Nullable Node parent, BiMap children, Collection leaves) { + super(parent, children, leaves); } @Override diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/View.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/View.java index ef25b80b6..937cbd226 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/View.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/View.java @@ -15,6 +15,7 @@ public View(MutableMultiTrie viewed) { this.viewed = Utils.requireNonNull(viewed, "viewed"); } + @Nonnull @Override public Node getRoot() { return this.viewed.getRoot(); diff --git a/enigma/src/test/java/org/quiltmc/enigma/util/CompositeStringMultiTrieTest.java b/enigma/src/test/java/org/quiltmc/enigma/util/CompositeStringMultiTrieTest.java index ed9251737..a0460c37e 100644 --- a/enigma/src/test/java/org/quiltmc/enigma/util/CompositeStringMultiTrieTest.java +++ b/enigma/src/test/java/org/quiltmc/enigma/util/CompositeStringMultiTrieTest.java @@ -22,19 +22,19 @@ void testAssociatedPrefixes() { final Node node = trie.get(prefix); assertThat( - "Unexpected values for prefix \"%s\"".formatted(prefix), + "Unexpected values for prefix \"%s\"!".formatted(prefix), node.streamValues().toArray(), arrayContainingInAnyOrder(associations.toArray()) ); assertThat( - "Unexpected leaves for prefix \"%s\"".formatted(prefix), + "Unexpected leaves for prefix \"%s\"!".formatted(prefix), node.streamLeaves().toArray(), arrayContainingInAnyOrder(associations.stream().filter(a -> a.isLeafOf(prefix)).toArray()) ); assertThat( - "Unexpected branches for prefix \"%s\"".formatted(prefix), + "Unexpected branches for prefix \"%s\"!".formatted(prefix), node.streamBranches().toArray(), arrayContainingInAnyOrder(associations.stream().filter(a -> a.isBranchOf(prefix)).toArray()) ); From 85bfbcc004db4020ec91a62d6e2a9d687a83310f Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Tue, 11 Nov 2025 11:29:32 -0800 Subject: [PATCH 020/124] avoid the need for SplitFirst objects more javadocs --- .../AbstractMutableMapMultiTrie.java | 72 +++++++++---------- .../multi_trie/CompositeStringMultiTrie.java | 10 +-- .../enigma/util/multi_trie/MultiTrie.java | 16 +++++ .../util/multi_trie/MutableMultiTrie.java | 71 ++++++++++++++++-- .../util/multi_trie/StringMultiTrie.java | 20 ++---- 5 files changed, 126 insertions(+), 63 deletions(-) diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/AbstractMutableMapMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/AbstractMutableMapMultiTrie.java index 80bd2f891..05d41e196 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/AbstractMutableMapMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/AbstractMutableMapMultiTrie.java @@ -27,19 +27,33 @@ public MultiTrie.Node getRoot() { return this.root.getView(); } + @Nonnull + @Override + public MultiTrie.Node get(S prefix) { + N node = this.root; + for (int i = 0; i < node.getLength(prefix); i++) { + node = node.nextImpl(node.getKey(prefix, i)); + if (node == null) { + return EmptyNode.get(); + } + } + + return node.getView(); + } + @Override public void put(S sequence, V value) { - this.root.put(Utils.requireNonNull(sequence, SEQUENCE), Utils.requireNonNull(value, VALUE)); + this.root.put(Utils.requireNonNull(sequence, SEQUENCE), Utils.requireNonNull(value, VALUE), 0); } @Override public boolean remove(S sequence, V value) { - return this.root.remove(Utils.requireNonNull(sequence, SEQUENCE), Utils.requireNonNull(value, VALUE)); + return this.root.remove(Utils.requireNonNull(sequence, SEQUENCE), Utils.requireNonNull(value, VALUE), 0); } @Override public boolean removeAll(S sequence) { - return this.root.removeAll(Utils.requireNonNull(sequence, SEQUENCE)); + return this.root.removeAll(Utils.requireNonNull(sequence, SEQUENCE), 0); } @Override @@ -61,21 +75,19 @@ protected Node(@Nullable Node parent, BiMap children, Collecti } @Override - public void put(S sequence, V value) { - if (this.isEmptySequence(sequence)) { + public void put(S sequence, V value, int startIndex) { + if (this.getLength(sequence) <= startIndex) { this.leaves.add(value); } else { - final FirstSplit split = this.splitFirst(sequence); - this.children - .computeIfAbsent(split.first, ignored -> this.createEmpty()) - .put(split.suffix, value); + .computeIfAbsent(this.getKey(sequence, startIndex), ignored -> this.createChild()) + .put(sequence, value, startIndex + 1); } } @Override - public boolean remove(S sequence, V value) { - final boolean removed = this.removeImpl(sequence, value); + public boolean remove(S sequence, V value, int startIndex) { + final boolean removed = this.removeImpl(sequence, value, startIndex); if (removed) { this.pruneIfEmpty(); @@ -86,8 +98,8 @@ public boolean remove(S sequence, V value) { } @Override - public boolean removeAll(S sequence) { - final boolean removed = this.removeAllImpl(sequence); + public boolean removeAll(S sequence, int startIndex) { + final boolean removed = this.removeAllImpl(sequence, startIndex); if (removed) { this.pruneIfEmpty(); @@ -102,18 +114,17 @@ public MultiTrie.Node getView() { return this.view; } - protected boolean removeImpl(S sequence, V value) { - if (this.isEmptySequence(sequence)) { + protected boolean removeImpl(S sequence, V value, int startIndex) { + if (this.getLength(sequence) <= startIndex) { return this.leaves.remove(value); } else { - final FirstSplit split = this.splitFirst(sequence); - final N next = this.nextImpl(split.first); - return next != null && next.remove(split.suffix, value); + final N next = this.nextImpl(this.getKey(sequence, startIndex)); + return next != null && next.remove(sequence, value, startIndex + 1); } } - protected boolean removeAllImpl(S sequence) { - if (this.isEmptySequence(sequence)) { + protected boolean removeAllImpl(S sequence, int startIndex) { + if (this.getLength(sequence) <= startIndex) { if (this.leaves.isEmpty()) { return false; } else { @@ -122,9 +133,8 @@ protected boolean removeAllImpl(S sequence) { return true; } } else { - final FirstSplit split = this.splitFirst(sequence); - final N next = this.nextImpl(split.first); - return next != null && next.removeAll(split.suffix); + final N next = this.nextImpl(this.getKey(sequence, startIndex)); + return next != null && next.removeAll(sequence, startIndex + 1); } } @@ -141,22 +151,10 @@ protected void pruneIfEmpty() { protected abstract N getSelf(); /** - * @return a new, empty node instance + * @return a new, empty child node instance */ @Nonnull @Pure - protected abstract N createEmpty(); - - /** - * Splits the first key from the passed {@code sequence} and returns that key and the remaining suffix. - * - * @implNote when invoked by {@link AbstractMutableMapMultiTrie}, the passed {@code sequence} is guaranteed - * non-empty according to {@link #isEmptySequence(Object)} - */ - protected abstract FirstSplit splitFirst(S sequence); - - protected abstract boolean isEmptySequence(S sequence); - - protected record FirstSplit(K first, S suffix) { } + protected abstract N createChild(); } } diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java index 3f006a5e1..5676a7ae8 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java @@ -36,15 +36,15 @@ private CompositeStringMultiTrie( } protected static final class Node extends StringMultiTrie.Node> { - private final UnaryOperator> factory; + private final UnaryOperator> childFactory; private Node( @Nullable Node parent, BiMap> children, Collection leaves, - UnaryOperator> factory + UnaryOperator> childFactory ) { super(parent, children, leaves); - this.factory = factory; + this.childFactory = childFactory; } @Override @@ -54,8 +54,8 @@ protected Node getSelf() { @Nonnull @Override - protected Node createEmpty() { - return this.factory.apply(this); + protected Node createChild() { + return this.childFactory.apply(this); } } } diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MultiTrie.java index 86210a1e9..2ea3f332b 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MultiTrie.java @@ -34,12 +34,28 @@ default boolean isEmpty() { } /** + * Represents values associated with a prefix in a {@link MultiTrie}. + * * @param the type of keys * @param the type of values */ interface Node { + /** + * @return a {@link Stream} containing all values with no more keys in their associated sequence;
+ * i.e. the prefix this node is associated with is the whole sequence the values are associated with + */ Stream streamLeaves(); + + /** + * @return a {@link Stream} containing all values with more keys in their associated sequence;
+ * i.e. the prefix this node is associated with is not + * the whole sequence the values are associated with + */ Stream streamBranches(); + + /** + * @return a {@link Stream} containing all values associated with the prefix this node is associated with + */ Stream streamValues(); default long getSize() { diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMultiTrie.java index c9cb64b41..372d2f84f 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMultiTrie.java @@ -1,17 +1,39 @@ package org.quiltmc.enigma.util.multi_trie; +import org.checkerframework.dataflow.qual.Pure; + /** * A multi-trie that allows modification which can also provide unmodifiable views of its contents. * * @param the type of keys - * @param the type of sequences + * @param the type of sequences; should generally support constant-time random key access for fast + * {@link Node#getKey(Object, int)} implementations * @param the type of values */ public interface MutableMultiTrie extends MultiTrie { + /** + * Associates the passed {@code value} with the passed {@code sequence} of keys. + */ void put(S sequence, V value); + /** + * Removes any association between the passed {@code sequence} of keys and the passed {@code value}. + * + *

Note: this removes {@linkplain Node#streamLeaves() leaves}, + * not {@linkplain Node#streamBranches() branches} + * + * @return {@code true} if the value was dissociated, or {@code false} otherwise + */ boolean remove(S sequence, V value); + /** + * Removes all associations with the passed {@code sequence} of keys. + * + *

Note: this removes {@linkplain Node#streamLeaves() leaves}, + * not {@linkplain Node#streamBranches() branches} + * + * @return {@code true} if any values were dissociated, or {@code false} otherwise + */ boolean removeAll(S sequence); /** @@ -27,15 +49,54 @@ public interface MutableMultiTrie extends MultiTrie { * backing data structures when the node becomes empty */ interface Node extends MultiTrie.Node { - void put(S sequence, V value); + /** + * @param value the value to associate + * @param sequence the full sequence the value will be associated with in this node's trie + * @param startIndex the index in the passed {@code sequence} at which this node's suffix starts; + * may be greater than or equal to the passed {@code sequence}'s + * {@linkplain #getLength(Object) length}, indicating that the value is a leaf + */ + void put(S sequence, V value, int startIndex); - boolean remove(S sequence, V value); + /** + * @param value the value to dissociate + * @param sequence the full sequence the value may be associated with in this node's trie + * @param startIndex the index in the passed {@code sequence} at which this node's suffix starts; + * may be greater than or equal to the passed {@code sequence}'s + * {@linkplain #getLength(Object) length}, indicating that the value is a leaf + * + * @return {@code true} if a value was dissociated, or {@code false} otherwise + */ + boolean remove(S sequence, V value, int startIndex); - boolean removeAll(S sequence); + /** + * @param sequence the full sequence whose values are to be dissociated + * @param startIndex the index in the passed {@code sequence} at which this node's suffix starts; + * may be greater than or equal to the passed {@code sequence}'s + * {@linkplain #getLength(Object) length}, indicating that the value is a leaf + * + * @return {@code true} if any values were dissociated, or {@code false} otherwise + */ + boolean removeAll(S sequence, int startIndex); /** - * @return al live, unmodifiable view of this node + * @return a live, unmodifiable view of this node */ MultiTrie.Node getView(); + + /** + * @return the number of keys in the passed {@code sequence} + */ + @Pure + int getLength(S sequence); + + /** + * @return the key at the passed {@code index} within the passed {@code sequence} + * + * @implSpec should only be passed valid {@code index}es; i.e. non-negative {@code index}es which are less than + * the {@linkplain #getLength(Object) length} of the passed {@code sequence} + */ + @Pure + K getKey(S sequence, int index); } } diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java index 0b1c57cc1..a26a0492d 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java @@ -3,7 +3,6 @@ import com.google.common.collect.BiMap; import org.quiltmc.enigma.util.multi_trie.StringMultiTrie.Node; -import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.util.Collection; @@ -13,17 +12,6 @@ protected StringMultiTrie(N root) { super(root); } - @Nonnull - @Override - public MultiTrie.Node get(String prefix) { - N node = this.root; - for (int i = 0; i < prefix.length() && node != null; i++) { - node = node.nextImpl(prefix.charAt(i)); - } - - return node == null ? EmptyNode.get() : node.getView(); - } - protected abstract static class Node> extends AbstractMutableMapMultiTrie.Node { protected Node(@Nullable Node parent, BiMap children, Collection leaves) { @@ -31,13 +19,13 @@ protected Node(@Nullable Node parent, BiMap children, Collec } @Override - protected FirstSplit splitFirst(String sequence) { - return new FirstSplit<>(sequence.charAt(0), sequence.substring(1)); + public int getLength(String sequence) { + return sequence.length(); } @Override - protected boolean isEmptySequence(String sequence) { - return sequence.isEmpty(); + public Character getKey(String sequence, int index) { + return sequence.charAt(index); } } } From 14f3db0bea53d3cd78dff65ebcc8a791f7677bc7 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Tue, 11 Nov 2025 12:38:30 -0800 Subject: [PATCH 021/124] test remove --- .../AbstractMapMultiTrieAccessor.java | 21 +++++++ .../CompositeStringMultiTrieTest.java | 59 ++++++++++++++++--- 2 files changed, 72 insertions(+), 8 deletions(-) create mode 100644 enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/AbstractMapMultiTrieAccessor.java rename enigma/src/test/java/org/quiltmc/enigma/util/{ => multi_trie}/CompositeStringMultiTrieTest.java (66%) diff --git a/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/AbstractMapMultiTrieAccessor.java b/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/AbstractMapMultiTrieAccessor.java new file mode 100644 index 000000000..dcedb17bd --- /dev/null +++ b/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/AbstractMapMultiTrieAccessor.java @@ -0,0 +1,21 @@ +package org.quiltmc.enigma.util.multi_trie; + +import javax.annotation.Nonnull; + +public class AbstractMapMultiTrieAccessor> + extends AbstractMapMultiTrie { + public static int getRootChildCount(AbstractMapMultiTrie trie) { + final AbstractMapMultiTrieAccessor accessor = new AbstractMapMultiTrieAccessor<>(trie); + return accessor.root.children.size(); + } + + public AbstractMapMultiTrieAccessor(AbstractMapMultiTrie accessed) { + super(accessed.root); + } + + @Nonnull + @Override + public MultiTrie.Node get(S prefix) { + throw new UnsupportedOperationException(); + } +} diff --git a/enigma/src/test/java/org/quiltmc/enigma/util/CompositeStringMultiTrieTest.java b/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java similarity index 66% rename from enigma/src/test/java/org/quiltmc/enigma/util/CompositeStringMultiTrieTest.java rename to enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java index a0460c37e..7e1a312a9 100644 --- a/enigma/src/test/java/org/quiltmc/enigma/util/CompositeStringMultiTrieTest.java +++ b/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java @@ -1,22 +1,19 @@ -package org.quiltmc.enigma.util; +package org.quiltmc.enigma.util.multi_trie; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMultimap; import org.junit.jupiter.api.Test; import org.quiltmc.enigma.util.multi_trie.MultiTrie.Node; -import org.quiltmc.enigma.util.multi_trie.CompositeStringMultiTrie; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.arrayContainingInAnyOrder; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertTrue; public class CompositeStringMultiTrieTest { @Test - void testAssociatedPrefixes() { - final CompositeStringMultiTrie trie = CompositeStringMultiTrie.createHashed(); - - Association.BY_PREFIX.values().stream().distinct().forEach(association -> { - trie.put(association.key, association); - }); + void testPut() { + final CompositeStringMultiTrie trie = Association.createAndPopulateTrie(); Association.BY_PREFIX.asMap().forEach((prefix, associations) -> { final Node node = trie.get(prefix); @@ -41,6 +38,42 @@ void testAssociatedPrefixes() { }); } + @Test + void testRemove() { + final CompositeStringMultiTrie trie = Association.createAndPopulateTrie(); + + Association.BY_PREFIX.asMap().forEach((prefix, associations) -> { + for (final Association association : associations) { + assertThat( + "Unexpected [non]removal of \"%s\" with prefix \"%s\"!".formatted(association, prefix), + association.isLeafOf(prefix), + is(trie.remove(prefix, association)) + ); + } + }); + + assertTrue( + trie.isEmpty(), + "Expected trie to be empty, but had contents: " + trie.getRoot().streamValues().toList() + ); + + assertThat( + "Expected root's children to be pruned, but it had children!", + AbstractMapMultiTrieAccessor.getRootChildCount(trie), + is(0) + ); + } + + @Test + void testPutMulti() { + // TODO + } + + @Test + void testRemoveAll() { + // TODO + } + record Association(String key) { static final Association EMPTY = new Association(""); @@ -86,6 +119,16 @@ record Association(String key) { BY_PREFIX = byPrefix.build(); } + static CompositeStringMultiTrie createAndPopulateTrie() { + final CompositeStringMultiTrie trie = CompositeStringMultiTrie.createHashed(); + + Association.BY_PREFIX.values().stream().distinct().forEach(association -> { + trie.put(association.key, association); + }); + + return trie; + } + boolean isLeafOf(String prefix) { return this.key.equals(prefix); } From 11fca56dd052908a796be6489809ad8a80ffa455 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Tue, 11 Nov 2025 13:49:26 -0800 Subject: [PATCH 022/124] implement testPutMulti --- .../AbstractMapMultiTrieAccessor.java | 8 +- .../CompositeStringMultiTrieTest.java | 145 ++++++++++++++---- 2 files changed, 123 insertions(+), 30 deletions(-) diff --git a/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/AbstractMapMultiTrieAccessor.java b/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/AbstractMapMultiTrieAccessor.java index dcedb17bd..b77874781 100644 --- a/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/AbstractMapMultiTrieAccessor.java +++ b/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/AbstractMapMultiTrieAccessor.java @@ -1,12 +1,16 @@ package org.quiltmc.enigma.util.multi_trie; +import com.google.common.collect.BiMap; + import javax.annotation.Nonnull; public class AbstractMapMultiTrieAccessor> extends AbstractMapMultiTrie { - public static int getRootChildCount(AbstractMapMultiTrie trie) { + public static BiMap> getRootChildren( + AbstractMapMultiTrie trie + ) { final AbstractMapMultiTrieAccessor accessor = new AbstractMapMultiTrieAccessor<>(trie); - return accessor.root.children.size(); + return accessor.root.children; } public AbstractMapMultiTrieAccessor(AbstractMapMultiTrie accessed) { diff --git a/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java b/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java index 7e1a312a9..04d22caa5 100644 --- a/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java +++ b/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java @@ -1,16 +1,26 @@ package org.quiltmc.enigma.util.multi_trie; +import com.google.common.collect.BiMap; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMultimap; import org.junit.jupiter.api.Test; import org.quiltmc.enigma.util.multi_trie.MultiTrie.Node; +import java.util.Collection; +import java.util.stream.Stream; + import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.arrayContainingInAnyOrder; -import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.quiltmc.enigma.util.multi_trie.AbstractMapMultiTrieAccessor.getRootChildren; + public class CompositeStringMultiTrieTest { + private static final String VALUES = "values"; + private static final String LEAVES = "leaves"; + private static final String BRANCHES = "branches"; + @Test void testPut() { final CompositeStringMultiTrie trie = Association.createAndPopulateTrie(); @@ -18,22 +28,45 @@ void testPut() { Association.BY_PREFIX.asMap().forEach((prefix, associations) -> { final Node node = trie.get(prefix); - assertThat( - "Unexpected values for prefix \"%s\"!".formatted(prefix), - node.streamValues().toArray(), - arrayContainingInAnyOrder(associations.toArray()) + assertUnorderedContentsForPrefix(prefix, VALUES, associations.stream(), node.streamValues()); + + assertUnorderedContentsForPrefix( + prefix, LEAVES, + associations.stream().filter(association -> association.isLeafOf(prefix)), + node.streamLeaves() + ); + + assertUnorderedContentsForPrefix( + prefix, BRANCHES, + associations.stream().filter(association -> association.isBranchOf(prefix)), + node.streamBranches() + ); + }); + } + + @Test + void testPutMulti() { + final CompositeStringMultiTrie trie = MultiAssociation.createAndPopulateTrie(); + + Association.BY_PREFIX.asMap().forEach((prefix, associations) -> { + final Node node = trie.get(prefix); + + assertUnorderedContentsForPrefix( + prefix, VALUES, + MultiAssociation.streamWith(associations.stream()), + node.streamValues() ); - assertThat( - "Unexpected leaves for prefix \"%s\"!".formatted(prefix), - node.streamLeaves().toArray(), - arrayContainingInAnyOrder(associations.stream().filter(a -> a.isLeafOf(prefix)).toArray()) + assertUnorderedContentsForPrefix( + prefix, LEAVES, + MultiAssociation.streamWith(associations.stream().filter(association -> association.isLeafOf(prefix))), + node.streamLeaves() ); - assertThat( - "Unexpected branches for prefix \"%s\"!".formatted(prefix), - node.streamBranches().toArray(), - arrayContainingInAnyOrder(associations.stream().filter(a -> a.isBranchOf(prefix)).toArray()) + assertUnorderedContentsForPrefix( + prefix, BRANCHES, + MultiAssociation.streamWith(associations.stream().filter(a -> a.isBranchOf(prefix))), + node.streamBranches() ); }); } @@ -44,34 +77,41 @@ void testRemove() { Association.BY_PREFIX.asMap().forEach((prefix, associations) -> { for (final Association association : associations) { - assertThat( - "Unexpected [non]removal of \"%s\" with prefix \"%s\"!".formatted(association, prefix), - association.isLeafOf(prefix), - is(trie.remove(prefix, association)) + final boolean expectation = association.isLeafOf(prefix); + assertEquals( + expectation, + trie.remove(prefix, association), + () -> "Unexpected%s removal of \"%s\" with prefix \"%s\"!" + .formatted(expectation ? "" : " no", association, prefix) ); } }); assertTrue( trie.isEmpty(), - "Expected trie to be empty, but had contents: " + trie.getRoot().streamValues().toList() + () ->"Expected trie to be empty, but had it contents: " + trie.getRoot().streamValues().toList() ); - assertThat( - "Expected root's children to be pruned, but it had children!", - AbstractMapMultiTrieAccessor.getRootChildCount(trie), - is(0) + final BiMap> rootChildren = getRootChildren(trie); + assertTrue( + rootChildren.isEmpty(), + () -> "Expected root's children to be pruned, but it had children: " + rootChildren ); } @Test - void testPutMulti() { + void testRemoveAll() { // TODO } - @Test - void testRemoveAll() { - // TODO + private static void assertUnorderedContentsForPrefix( + String prefix, String arrayName, Stream expected, Stream actual + ) { + assertThat( + "Unexpected %s for prefix \"%s\"!".formatted(arrayName, prefix), + actual.toArray(), + arrayContainingInAnyOrder(expected.toArray()) + ); } record Association(String key) { @@ -122,9 +162,9 @@ record Association(String key) { static CompositeStringMultiTrie createAndPopulateTrie() { final CompositeStringMultiTrie trie = CompositeStringMultiTrie.createHashed(); - Association.BY_PREFIX.values().stream().distinct().forEach(association -> { + for (final Association association : ALL) { trie.put(association.key, association); - }); + } return trie; } @@ -137,4 +177,53 @@ boolean isBranchOf(String prefix) { return this.key.length() > prefix.length() && this.key.startsWith(prefix); } } + + record MultiAssociation(Association association, int id) { + static final int MAX_COUNT = 3; + + static final ImmutableList ALL; + static final ImmutableMultimap BY_ASSOCIATION; + + static { + final ImmutableList.Builder all = ImmutableList.builder(); + + final ImmutableMultimap.Builder byAssociation = ImmutableMultimap.builder(); + + int id = 0; + int count = 1; + for (final Association association : Association.ALL) { + int currentCount = count; + while (currentCount > 0) { + final MultiAssociation multiAssociation = new MultiAssociation(association, id++); + + all.add(multiAssociation); + byAssociation.put(association, multiAssociation); + + currentCount--; + } + + // prevent needless exponential growth + count = (count % MAX_COUNT) + 1; + } + + ALL = all.build(); + BY_ASSOCIATION = byAssociation.build(); + } + + static CompositeStringMultiTrie createAndPopulateTrie() { + final CompositeStringMultiTrie trie = CompositeStringMultiTrie.createHashed(); + + for (final MultiAssociation multiAssociation : ALL) { + trie.put(multiAssociation.association.key, multiAssociation); + } + + return trie; + } + + static Stream streamWith(Stream associations) { + return associations + .map(MultiAssociation.BY_ASSOCIATION::get) + .flatMap(Collection::stream); + } + } } From dd8a4211497092b815b67a0d40f844984add8ea2 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Tue, 11 Nov 2025 14:08:15 -0800 Subject: [PATCH 023/124] add testRemoveMulti --- .../CompositeStringMultiTrieTest.java | 41 +++++++++++++++---- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java b/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java index 04d22caa5..52f33091a 100644 --- a/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java +++ b/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java @@ -77,22 +77,47 @@ void testRemove() { Association.BY_PREFIX.asMap().forEach((prefix, associations) -> { for (final Association association : associations) { - final boolean expectation = association.isLeafOf(prefix); - assertEquals( - expectation, - trie.remove(prefix, association), - () -> "Unexpected%s removal of \"%s\" with prefix \"%s\"!" - .formatted(expectation ? "" : " no", association, prefix) - ); + assertRemovalResult(trie, association.isLeafOf(prefix), prefix, association); } }); + assertEmpty(trie); + } + + @Test + void testRemoveMulti() { + final CompositeStringMultiTrie trie = MultiAssociation.createAndPopulateTrie(); + + Association.BY_PREFIX.asMap().forEach((prefix, associations) -> { + for (final Association association : associations) { + final boolean expectRemoval = association.isLeafOf(prefix); + for (final MultiAssociation multiAssociation : MultiAssociation.BY_ASSOCIATION.get(association)) { + assertRemovalResult(trie, expectRemoval, prefix, multiAssociation); + } + } + }); + + assertEmpty(trie); + } + + private static void assertRemovalResult( + CompositeStringMultiTrie trie, boolean expectRemoval, String prefix, T value + ) { + assertEquals( + expectRemoval, + trie.remove(prefix, value), + () -> "Unexpected %sremoval of \"%s\" with prefix \"%s\"!" + .formatted(expectRemoval ? "non-" : "", value, prefix) + ); + } + + private static void assertEmpty(CompositeStringMultiTrie trie) { assertTrue( trie.isEmpty(), () ->"Expected trie to be empty, but had it contents: " + trie.getRoot().streamValues().toList() ); - final BiMap> rootChildren = getRootChildren(trie); + final BiMap> rootChildren = getRootChildren(trie); assertTrue( rootChildren.isEmpty(), () -> "Expected root's children to be pruned, but it had children: " + rootChildren From c92dc0c2f927e145ec145dfc6ccb31190abde801 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Tue, 11 Nov 2025 14:26:02 -0800 Subject: [PATCH 024/124] implement testRemoveAll --- .../CompositeStringMultiTrieTest.java | 38 ++++++++++++++----- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java b/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java index 52f33091a..d16d6977d 100644 --- a/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java +++ b/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java @@ -7,10 +7,11 @@ import org.quiltmc.enigma.util.multi_trie.MultiTrie.Node; import java.util.Collection; +import java.util.List; import java.util.stream.Stream; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.arrayContainingInAnyOrder; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -100,14 +101,36 @@ void testRemoveMulti() { assertEmpty(trie); } + @Test + void testRemoveAll() { + final CompositeStringMultiTrie trie = MultiAssociation.createAndPopulateTrie(); + + Association.BY_PREFIX.asMap().forEach((prefix, associations) -> { + final List leaves = associations.stream() + .filter(association -> association.isLeafOf(prefix)) + .toList(); + + final boolean expectRemoval = !leaves.isEmpty(); + assertEquals(expectRemoval, trie.removeAll(prefix), () -> { + return expectRemoval + ? "Expected removal of leaves with prefix \"%s\": %s" + .formatted(prefix, MultiAssociation.streamWith(leaves.stream()).toList()) + : "Expected no removal of nodes with prefix \"%s\": %s" + .formatted(prefix, MultiAssociation.streamWith(associations.stream()).toList()); + }); + }); + + assertEmpty(trie); + } + private static void assertRemovalResult( CompositeStringMultiTrie trie, boolean expectRemoval, String prefix, T value ) { assertEquals( expectRemoval, trie.remove(prefix, value), - () -> "Unexpected %sremoval of \"%s\" with prefix \"%s\"!" - .formatted(expectRemoval ? "non-" : "", value, prefix) + () -> "Expected%s removal of \"%s\" with prefix \"%s\"!" + .formatted(expectRemoval ? "" : " no", value, prefix) ); } @@ -124,18 +147,13 @@ private static void assertEmpty(CompositeStringMultiTrie trie) { ); } - @Test - void testRemoveAll() { - // TODO - } - private static void assertUnorderedContentsForPrefix( String prefix, String arrayName, Stream expected, Stream actual ) { assertThat( "Unexpected %s for prefix \"%s\"!".formatted(arrayName, prefix), - actual.toArray(), - arrayContainingInAnyOrder(expected.toArray()) + actual.toList(), + containsInAnyOrder(expected.toArray()) ); } From 281cae7c5a603782b3dddbe1cd387e718a7fe2cb Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Tue, 11 Nov 2025 18:05:31 -0800 Subject: [PATCH 025/124] push the concept of a sequence type down to StringMultiTrie allow mutation via MutableMultiTrie.Node --- .../util/multi_trie/AbstractMapMultiTrie.java | 18 +-- .../AbstractMutableMapMultiTrie.java | 105 ++++-------------- .../multi_trie/CompositeStringMultiTrie.java | 5 +- .../enigma/util/multi_trie/EmptyNode.java | 2 +- .../enigma/util/multi_trie/MultiTrie.java | 18 +-- .../util/multi_trie/MutableMultiTrie.java | 74 ++++-------- .../enigma/util/multi_trie/NodeView.java | 13 ++- .../util/multi_trie/StringMultiTrie.java | 84 +++++++++++--- .../quiltmc/enigma/util/multi_trie/View.java | 14 +-- .../AbstractMapMultiTrieAccessor.java | 21 +--- 10 files changed, 148 insertions(+), 206 deletions(-) diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/AbstractMapMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/AbstractMapMultiTrie.java index 47044d168..ae528038a 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/AbstractMapMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/AbstractMapMultiTrie.java @@ -5,11 +5,10 @@ import org.quiltmc.enigma.util.multi_trie.AbstractMapMultiTrie.Node; import javax.annotation.Nonnull; -import javax.annotation.Nullable; import java.util.Collection; import java.util.stream.Stream; -public abstract class AbstractMapMultiTrie> implements MultiTrie { +public abstract class AbstractMapMultiTrie> implements MultiTrie { protected final N root; protected AbstractMapMultiTrie(N root) { @@ -18,7 +17,7 @@ protected AbstractMapMultiTrie(N root) { @Nonnull @Override - public MultiTrie.Node getRoot() { + public N getRoot() { return this.root; } @@ -27,7 +26,7 @@ public MultiTrie.Node getRoot() { * @param the type of values * @param the type of this node */ - protected static class Node> implements MultiTrie.Node { + protected abstract static class Node> implements MultiTrie.Node { protected final BiMap children; protected final Collection leaves; @@ -52,15 +51,8 @@ public Stream streamValues() { } @Override - @Nonnull - public MultiTrie.Node next(K key) { - final Node next = this.nextImpl(Utils.requireNonNull(key, "key")); - return next == null ? EmptyNode.get() : next; - } - - @Nullable - protected N nextImpl(K key) { - return this.children.get(key); + public N next(K key) { + return this.children.get(Utils.requireNonNull(key, "key")); } } } diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/AbstractMutableMapMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/AbstractMutableMapMultiTrie.java index 05d41e196..ed06fea89 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/AbstractMutableMapMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/AbstractMutableMapMultiTrie.java @@ -2,93 +2,47 @@ import com.google.common.collect.BiMap; import org.checkerframework.dataflow.qual.Pure; -import org.quiltmc.enigma.util.Utils; import org.quiltmc.enigma.util.multi_trie.AbstractMutableMapMultiTrie.Node; import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.util.Collection; -public abstract class AbstractMutableMapMultiTrie> - extends AbstractMapMultiTrie - implements MutableMultiTrie { - private static final String SEQUENCE = "sequence"; - private static final String VALUE = "value"; - - private final View view = new View<>(this); +public abstract class AbstractMutableMapMultiTrie> + extends AbstractMapMultiTrie + implements MutableMultiTrie { + private final View view = new View<>(this); protected AbstractMutableMapMultiTrie(N root) { super(root); } - @Nonnull - @Override - public MultiTrie.Node getRoot() { - return this.root.getView(); - } - - @Nonnull - @Override - public MultiTrie.Node get(S prefix) { - N node = this.root; - for (int i = 0; i < node.getLength(prefix); i++) { - node = node.nextImpl(node.getKey(prefix, i)); - if (node == null) { - return EmptyNode.get(); - } - } - - return node.getView(); - } - - @Override - public void put(S sequence, V value) { - this.root.put(Utils.requireNonNull(sequence, SEQUENCE), Utils.requireNonNull(value, VALUE), 0); - } - - @Override - public boolean remove(S sequence, V value) { - return this.root.remove(Utils.requireNonNull(sequence, SEQUENCE), Utils.requireNonNull(value, VALUE), 0); - } - @Override - public boolean removeAll(S sequence) { - return this.root.removeAll(Utils.requireNonNull(sequence, SEQUENCE), 0); - } - - @Override - public MultiTrie getView() { + public MultiTrie getView() { return this.view; } - protected abstract static class Node> + public abstract static class Node> extends AbstractMapMultiTrie.Node - implements MutableMultiTrie.Node { + implements MutableMultiTrie.Node { @Nullable - protected final Node parent; + protected final N parent; private final NodeView view = new NodeView<>(this); - protected Node(@Nullable Node parent, BiMap children, Collection leaves) { + protected Node(@Nullable N parent, BiMap children, Collection leaves) { super(children, leaves); this.parent = parent; } @Override - public void put(S sequence, V value, int startIndex) { - if (this.getLength(sequence) <= startIndex) { - this.leaves.add(value); - } else { - this.children - .computeIfAbsent(this.getKey(sequence, startIndex), ignored -> this.createChild()) - .put(sequence, value, startIndex + 1); - } + public void put(V value) { + this.leaves.add(value); } @Override - public boolean remove(S sequence, V value, int startIndex) { - final boolean removed = this.removeImpl(sequence, value, startIndex); - if (removed) { + public boolean remove(V value) { + if (this.leaves.remove(value)) { this.pruneIfEmpty(); return true; @@ -98,9 +52,10 @@ public boolean remove(S sequence, V value, int startIndex) { } @Override - public boolean removeAll(S sequence, int startIndex) { - final boolean removed = this.removeAllImpl(sequence, startIndex); - if (removed) { + public boolean removeAll() { + final boolean hasLeaves = !this.leaves.isEmpty(); + if (hasLeaves) { + this.leaves.clear(); this.pruneIfEmpty(); return true; @@ -114,39 +69,17 @@ public MultiTrie.Node getView() { return this.view; } - protected boolean removeImpl(S sequence, V value, int startIndex) { - if (this.getLength(sequence) <= startIndex) { - return this.leaves.remove(value); - } else { - final N next = this.nextImpl(this.getKey(sequence, startIndex)); - return next != null && next.remove(sequence, value, startIndex + 1); - } - } - - protected boolean removeAllImpl(S sequence, int startIndex) { - if (this.getLength(sequence) <= startIndex) { - if (this.leaves.isEmpty()) { - return false; - } else { - this.leaves.clear(); - - return true; - } - } else { - final N next = this.nextImpl(this.getKey(sequence, startIndex)); - return next != null && next.removeAll(sequence, startIndex + 1); - } - } - protected void pruneIfEmpty() { if (this.parent != null && this.isEmpty()) { this.parent.children.inverse().remove(this.getSelf()); + this.parent.pruneIfEmpty(); } } /** * @return this node */ + @Nonnull @Pure protected abstract N getSelf(); diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java index 5676a7ae8..8b298fdc1 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java @@ -35,9 +35,11 @@ private CompositeStringMultiTrie( super(createNode(null, childrenFactory, leavesFactory)); } - protected static final class Node extends StringMultiTrie.Node> { + public static final class Node extends AbstractMutableMapMultiTrie.Node> { private final UnaryOperator> childFactory; + // private final View view = new View(); + private Node( @Nullable Node parent, BiMap> children, Collection leaves, UnaryOperator> childFactory @@ -47,6 +49,7 @@ private Node( this.childFactory = childFactory; } + @Nonnull @Override protected Node getSelf() { return this; diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/EmptyNode.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/EmptyNode.java index 41d0326c9..a5f6cd01a 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/EmptyNode.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/EmptyNode.java @@ -6,7 +6,7 @@ /** * An empty, immutable, singleton {@link MultiTrie.Node} implementation. * - *

{@link MultiTrie.Node#next(Object)} implementations may return {@linkplain #get() the empty node} + *

{@link MultiTrie.Node#nextOrEmpty(Object)} implementations may return {@linkplain #get() the empty node} * when nodes have no branches. * * @implNote not intended to be stored in tries diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MultiTrie.java index 2ea3f332b..6b0bc593b 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MultiTrie.java @@ -1,6 +1,7 @@ package org.quiltmc.enigma.util.multi_trie; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.util.stream.Stream; /** @@ -8,23 +9,18 @@ * *

Values can be looked up by a prefix of their key sequence; all values associated with a sequence beginning with * the prefix will be returned.
- * The prefix can be passed either all at once to {@link #get}, - * or key-by-key to {@link Node#next} starting with {@link #getRoot}. + * The prefix is passed key-by-key to {@link Node#next} starting with {@link #getRoot}. * * @implSpec {@code S} sequence types should represent an ordered sequence of keys of type {@code K}; * sequences that represent the same sequence of keys should be equivalent * * @param the type of keys - * @param the type of sequences * @param the type of values */ -public interface MultiTrie { +public interface MultiTrie { @Nonnull Node getRoot(); - @Nonnull - Node get(S prefix); - default long getSize() { return this.getRoot().getSize(); } @@ -66,7 +62,13 @@ default boolean isEmpty() { return this.getSize() == 0; } - @Nonnull + @Nullable Node next(K key); + + @Nonnull + default Node nextOrEmpty(K key) { + final Node next = this.next(key); + return next == null ? EmptyNode.get() : next; + } } } diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMultiTrie.java index 372d2f84f..cb5933c3a 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMultiTrie.java @@ -1,62 +1,45 @@ package org.quiltmc.enigma.util.multi_trie; -import org.checkerframework.dataflow.qual.Pure; +import org.quiltmc.enigma.util.multi_trie.MutableMultiTrie.Node; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; /** * A multi-trie that allows modification which can also provide unmodifiable views of its contents. * * @param the type of keys - * @param the type of sequences; should generally support constant-time random key access for fast - * {@link Node#getKey(Object, int)} implementations * @param the type of values */ -public interface MutableMultiTrie extends MultiTrie { - /** - * Associates the passed {@code value} with the passed {@code sequence} of keys. - */ - void put(S sequence, V value); - - /** - * Removes any association between the passed {@code sequence} of keys and the passed {@code value}. - * - *

Note: this removes {@linkplain Node#streamLeaves() leaves}, - * not {@linkplain Node#streamBranches() branches} - * - * @return {@code true} if the value was dissociated, or {@code false} otherwise - */ - boolean remove(S sequence, V value); - - /** - * Removes all associations with the passed {@code sequence} of keys. - * - *

Note: this removes {@linkplain Node#streamLeaves() leaves}, - * not {@linkplain Node#streamBranches() branches} - * - * @return {@code true} if any values were dissociated, or {@code false} otherwise - */ - boolean removeAll(S sequence); +public interface MutableMultiTrie> extends MultiTrie { + @Nonnull + @Override + N getRoot(); /** * @return a live, unmodifiable view of this trie */ - MultiTrie getView(); + MultiTrie getView(); /** - * @implSpec mutable nodes should not be returned from public methods, return a - * {@linkplain #getView() view} instead; users should never see node mutation methods + * A mutable node representing values associated with a {@link MutableMultiTrie}. * * @implNote most implementations should remove themselves from any * backing data structures when the node becomes empty */ - interface Node extends MultiTrie.Node { + interface Node extends MultiTrie.Node { + @Override + @Nullable + Node next(K key); + /** - * @param value the value to associate - * @param sequence the full sequence the value will be associated with in this node's trie - * @param startIndex the index in the passed {@code sequence} at which this node's suffix starts; + * @param value the value to associate + * @param sequence the full sequence the value will be associated with in this node's trie + * @param startIndex the index in the passed {@code sequence} at which this node's suffix starts; * may be greater than or equal to the passed {@code sequence}'s * {@linkplain #getLength(Object) length}, indicating that the value is a leaf */ - void put(S sequence, V value, int startIndex); + void put(V value); /** * @param value the value to dissociate @@ -67,7 +50,7 @@ interface Node extends MultiTrie.Node { * * @return {@code true} if a value was dissociated, or {@code false} otherwise */ - boolean remove(S sequence, V value, int startIndex); + boolean remove(V value); /** * @param sequence the full sequence whose values are to be dissociated @@ -77,26 +60,11 @@ interface Node extends MultiTrie.Node { * * @return {@code true} if any values were dissociated, or {@code false} otherwise */ - boolean removeAll(S sequence, int startIndex); + boolean removeAll(); /** * @return a live, unmodifiable view of this node */ MultiTrie.Node getView(); - - /** - * @return the number of keys in the passed {@code sequence} - */ - @Pure - int getLength(S sequence); - - /** - * @return the key at the passed {@code index} within the passed {@code sequence} - * - * @implSpec should only be passed valid {@code index}es; i.e. non-negative {@code index}es which are less than - * the {@linkplain #getLength(Object) length} of the passed {@code sequence} - */ - @Pure - K getKey(S sequence, int index); } } diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/NodeView.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/NodeView.java index d482aa550..3303d8fc6 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/NodeView.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/NodeView.java @@ -4,9 +4,9 @@ import java.util.stream.Stream; public class NodeView implements MultiTrie.Node { - private final MutableMultiTrie.Node viewed; + private final MutableMultiTrie.Node viewed; - public NodeView(MutableMultiTrie.Node viewed) { + public NodeView(MutableMultiTrie.Node viewed) { this.viewed = viewed; } @@ -28,6 +28,13 @@ public Stream streamValues() { @Nonnull @Override public MultiTrie.Node next(K key) { - return this.viewed.next(key); + final MutableMultiTrie.Node next = this.viewed.next(key); + return next == null ? EmptyNode.get() : next.getView(); + } + + @Nonnull + @Override + public MultiTrie.Node nextOrEmpty(K key) { + return this.next(key); } } diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java index a26a0492d..d741621eb 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java @@ -1,31 +1,83 @@ package org.quiltmc.enigma.util.multi_trie; -import com.google.common.collect.BiMap; -import org.quiltmc.enigma.util.multi_trie.StringMultiTrie.Node; +import org.quiltmc.enigma.util.Utils; -import javax.annotation.Nullable; -import java.util.Collection; +import javax.annotation.Nonnull; + +public abstract class StringMultiTrie> + extends AbstractMutableMapMultiTrie { + private static final String PREFIX = "prefix"; + private static final String STRING = "string"; + private static final String VALUE = "value"; -public class StringMultiTrie> - extends AbstractMutableMapMultiTrie { protected StringMultiTrie(N root) { super(root); } - protected abstract static class Node> - extends AbstractMutableMapMultiTrie.Node { - protected Node(@Nullable Node parent, BiMap children, Collection leaves) { - super(parent, children, leaves); + public N get(String prefix) { + Utils.requireNonNull(prefix, PREFIX); + + N node = this.root; + for (int i = 0; i < prefix.length(); i++) { + node = node.next(prefix.charAt(i)); + if (node == null) { + return null; + } } - @Override - public int getLength(String sequence) { - return sequence.length(); + return node; + } + + @Nonnull + public MultiTrie.Node getView(String prefix) { + final N node = this.get(prefix); + return node == null ? EmptyNode.get() : node.getView(); + } + + @Nonnull + public N put(String string, V value) { + Utils.requireNonNull(string, STRING); + Utils.requireNonNull(value, VALUE); + + N node = this.root; + for (int i = 0; i < string.length(); i++) { + final N parent = node; + node = node.children.computeIfAbsent(string.charAt(i), ignored -> parent.createChild()); } - @Override - public Character getKey(String sequence, int index) { - return sequence.charAt(index); + node.put(value); + + return node; + } + + public boolean remove(String string, V value) { + Utils.requireNonNull(string, STRING); + Utils.requireNonNull(value, VALUE); + + N node = this.root; + for (int i = 0; i < string.length(); i++) { + node = node.next(string.charAt(i)); + + if (node == null) { + return false; + } } + + return node.remove(value); + } + + public boolean removeAll(String string) { + Utils.requireNonNull(string, STRING); + + N node = this.root; + for (int i = 0; i < string.length(); i++) { + node = node.next(string.charAt(i)); + + if (node == null) { + return false; + } + } + + return node.removeAll(); } } diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/View.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/View.java index 937cbd226..f36fe223a 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/View.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/View.java @@ -8,22 +8,16 @@ * A live, unmodifiable view of a {@link MutableMultiTrie}, * for use in {@link MutableMultiTrie#getView()} implementations. */ -public final class View implements MultiTrie { - private final MutableMultiTrie viewed; +public final class View implements MultiTrie { + private final MutableMultiTrie> viewed; - public View(MutableMultiTrie viewed) { + public View(MutableMultiTrie> viewed) { this.viewed = Utils.requireNonNull(viewed, "viewed"); } @Nonnull @Override public Node getRoot() { - return this.viewed.getRoot(); - } - - @Nonnull - @Override - public Node get(S prefix) { - return this.viewed.get(prefix); + return this.viewed.getRoot().getView(); } } diff --git a/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/AbstractMapMultiTrieAccessor.java b/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/AbstractMapMultiTrieAccessor.java index b77874781..afa58dd5f 100644 --- a/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/AbstractMapMultiTrieAccessor.java +++ b/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/AbstractMapMultiTrieAccessor.java @@ -2,24 +2,15 @@ import com.google.common.collect.BiMap; -import javax.annotation.Nonnull; - -public class AbstractMapMultiTrieAccessor> - extends AbstractMapMultiTrie { - public static BiMap> getRootChildren( - AbstractMapMultiTrie trie +public class AbstractMapMultiTrieAccessor> + extends AbstractMapMultiTrie { + public static BiMap> getRootChildren( + AbstractMapMultiTrie> trie ) { - final AbstractMapMultiTrieAccessor accessor = new AbstractMapMultiTrieAccessor<>(trie); - return accessor.root.children; + return new AbstractMapMultiTrieAccessor<>(trie).root.children; } - public AbstractMapMultiTrieAccessor(AbstractMapMultiTrie accessed) { + public AbstractMapMultiTrieAccessor(AbstractMapMultiTrie accessed) { super(accessed.root); } - - @Nonnull - @Override - public MultiTrie.Node get(S prefix) { - throw new UnsupportedOperationException(); - } } From fd37f15a19ce66fe4c4d010b9c14f7b400a8a92c Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Tue, 11 Nov 2025 18:42:15 -0800 Subject: [PATCH 026/124] extract View interfaces --- .../AbstractMutableMapMultiTrie.java | 8 +- .../enigma/util/multi_trie/EmptyNode.java | 4 +- .../util/multi_trie/MutableMultiTrie.java | 89 +++++++++++++++---- .../enigma/util/multi_trie/NodeView.java | 40 --------- .../quiltmc/enigma/util/multi_trie/View.java | 23 ----- 5 files changed, 77 insertions(+), 87 deletions(-) delete mode 100644 enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/NodeView.java delete mode 100644 enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/View.java diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/AbstractMutableMapMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/AbstractMutableMapMultiTrie.java index ed06fea89..041a7f17f 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/AbstractMutableMapMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/AbstractMutableMapMultiTrie.java @@ -11,14 +11,14 @@ public abstract class AbstractMutableMapMultiTrie> extends AbstractMapMultiTrie implements MutableMultiTrie { - private final View view = new View<>(this); + private final MutableMultiTrie.View view = new MutableMultiTrie.View.Impl<>(this); protected AbstractMutableMapMultiTrie(N root) { super(root); } @Override - public MultiTrie getView() { + public View getView() { return this.view; } @@ -28,7 +28,7 @@ public abstract static class Node> @Nullable protected final N parent; - private final NodeView view = new NodeView<>(this); + private final View view = new View.Impl<>(this); protected Node(@Nullable N parent, BiMap children, Collection leaves) { super(children, leaves); @@ -65,7 +65,7 @@ public boolean removeAll() { } @Override - public MultiTrie.Node getView() { + public View getView() { return this.view; } diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/EmptyNode.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/EmptyNode.java index a5f6cd01a..af5f211d5 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/EmptyNode.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/EmptyNode.java @@ -11,7 +11,7 @@ * * @implNote not intended to be stored in tries */ -public final class EmptyNode implements MultiTrie.Node { +public final class EmptyNode implements MutableMultiTrie.Node.View { private static final EmptyNode INSTANCE = new EmptyNode<>(); @SuppressWarnings("unchecked") @@ -38,7 +38,7 @@ public Stream streamValues() { @Override @Nonnull - public MultiTrie.Node next(K key) { + public EmptyNode next(K key) { return this; } } diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMultiTrie.java index cb5933c3a..5b4d313e7 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMultiTrie.java @@ -1,9 +1,11 @@ package org.quiltmc.enigma.util.multi_trie; +import org.quiltmc.enigma.util.Utils; import org.quiltmc.enigma.util.multi_trie.MutableMultiTrie.Node; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import java.util.stream.Stream; /** * A multi-trie that allows modification which can also provide unmodifiable views of its contents. @@ -19,7 +21,7 @@ public interface MutableMultiTrie> extends MultiTrie< /** * @return a live, unmodifiable view of this trie */ - MultiTrie getView(); + View getView(); /** * A mutable node representing values associated with a {@link MutableMultiTrie}. @@ -33,38 +35,89 @@ interface Node extends MultiTrie.Node { Node next(K key); /** - * @param value the value to associate - * @param sequence the full sequence the value will be associated with in this node's trie - * @param startIndex the index in the passed {@code sequence} at which this node's suffix starts; - * may be greater than or equal to the passed {@code sequence}'s - * {@linkplain #getLength(Object) length}, indicating that the value is a leaf + * @param value a value to add to this node's leaves, associating it with the sequence leading to this node. */ void put(V value); /** - * @param value the value to dissociate - * @param sequence the full sequence the value may be associated with in this node's trie - * @param startIndex the index in the passed {@code sequence} at which this node's suffix starts; - * may be greater than or equal to the passed {@code sequence}'s - * {@linkplain #getLength(Object) length}, indicating that the value is a leaf + * @param value a value to remove from this node's leaves * - * @return {@code true} if a value was dissociated, or {@code false} otherwise + * @return {@code true} if a value was removed, or {@code false} otherwise */ boolean remove(V value); /** - * @param sequence the full sequence whose values are to be dissociated - * @param startIndex the index in the passed {@code sequence} at which this node's suffix starts; - * may be greater than or equal to the passed {@code sequence}'s - * {@linkplain #getLength(Object) length}, indicating that the value is a leaf + * Removes all leaves from this node. * - * @return {@code true} if any values were dissociated, or {@code false} otherwise + * @return {@code true} if any values were removed, or {@code false} otherwise */ boolean removeAll(); /** * @return a live, unmodifiable view of this node */ - MultiTrie.Node getView(); + View getView(); + + interface View extends MultiTrie.Node { + @Override + @Nonnull + View next(K key); + + @Override + @Nonnull + default View nextOrEmpty(K key) { + return this.next(key); + } + + class Impl implements View { + protected final Node viewed; + + public Impl(Node viewed) { + this.viewed = viewed; + } + + @Override + public Stream streamLeaves() { + return this.viewed.streamLeaves(); + } + + @Override + public Stream streamBranches() { + return this.viewed.streamBranches(); + } + + @Override + public Stream streamValues() { + return this.viewed.streamValues(); + } + + @Nonnull + @Override + public View next(K key) { + final Node next = this.viewed.next(key); + return next == null ? EmptyNode.get() : next.getView(); + } + } + } + } + + interface View extends MultiTrie { + @Nonnull + @Override + MutableMultiTrie.Node.View getRoot(); + + class Impl implements View { + protected final MutableMultiTrie> viewed; + + public Impl(MutableMultiTrie> viewed) { + this.viewed = Utils.requireNonNull(viewed, "viewed"); + } + + @Nonnull + @Override + public MutableMultiTrie.Node.View getRoot() { + return this.viewed.getRoot().getView(); + } + } } } diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/NodeView.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/NodeView.java deleted file mode 100644 index 3303d8fc6..000000000 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/NodeView.java +++ /dev/null @@ -1,40 +0,0 @@ -package org.quiltmc.enigma.util.multi_trie; - -import javax.annotation.Nonnull; -import java.util.stream.Stream; - -public class NodeView implements MultiTrie.Node { - private final MutableMultiTrie.Node viewed; - - public NodeView(MutableMultiTrie.Node viewed) { - this.viewed = viewed; - } - - @Override - public Stream streamLeaves() { - return this.viewed.streamLeaves(); - } - - @Override - public Stream streamBranches() { - return this.viewed.streamBranches(); - } - - @Override - public Stream streamValues() { - return this.viewed.streamValues(); - } - - @Nonnull - @Override - public MultiTrie.Node next(K key) { - final MutableMultiTrie.Node next = this.viewed.next(key); - return next == null ? EmptyNode.get() : next.getView(); - } - - @Nonnull - @Override - public MultiTrie.Node nextOrEmpty(K key) { - return this.next(key); - } -} diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/View.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/View.java deleted file mode 100644 index f36fe223a..000000000 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/View.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.quiltmc.enigma.util.multi_trie; - -import org.quiltmc.enigma.util.Utils; - -import javax.annotation.Nonnull; - -/** - * A live, unmodifiable view of a {@link MutableMultiTrie}, - * for use in {@link MutableMultiTrie#getView()} implementations. - */ -public final class View implements MultiTrie { - private final MutableMultiTrie> viewed; - - public View(MutableMultiTrie> viewed) { - this.viewed = Utils.requireNonNull(viewed, "viewed"); - } - - @Nonnull - @Override - public Node getRoot() { - return this.viewed.getRoot().getView(); - } -} From dabcb79c461b354e2035abb4cd62123c37d20f13 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 12 Nov 2025 20:15:28 -0800 Subject: [PATCH 027/124] refactor so that mutable node's types are visible eliminate public nullability eliminate view interfaces add CompositeBiMap --- .../quiltmc/enigma/util/CompositeBiMap.java | 291 ++++++++++++++++++ .../util/multi_trie/AbstractMapMultiTrie.java | 58 ---- .../AbstractMutableMapMultiTrie.java | 93 ------ .../multi_trie/CompositeStringMultiTrie.java | 76 ++++- .../enigma/util/multi_trie/EmptyNode.java | 44 --- .../enigma/util/multi_trie/MapNode.java | 30 ++ .../enigma/util/multi_trie/MultiTrie.java | 9 +- .../util/multi_trie/MutableMapNode.java | 119 +++++++ .../util/multi_trie/MutableMultiTrie.java | 71 +---- .../enigma/util/multi_trie/NodeView.java | 33 ++ .../util/multi_trie/StringMultiTrie.java | 65 ++-- .../AbstractMapMultiTrieAccessor.java | 16 - .../CompositeStringMultiTrieTest.java | 7 +- 13 files changed, 592 insertions(+), 320 deletions(-) create mode 100644 enigma/src/main/java/org/quiltmc/enigma/util/CompositeBiMap.java delete mode 100644 enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/AbstractMapMultiTrie.java delete mode 100644 enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/AbstractMutableMapMultiTrie.java delete mode 100644 enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/EmptyNode.java create mode 100644 enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MapNode.java create mode 100644 enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMapNode.java create mode 100644 enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/NodeView.java delete mode 100644 enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/AbstractMapMultiTrieAccessor.java diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/CompositeBiMap.java b/enigma/src/main/java/org/quiltmc/enigma/util/CompositeBiMap.java new file mode 100644 index 000000000..cf796118f --- /dev/null +++ b/enigma/src/main/java/org/quiltmc/enigma/util/CompositeBiMap.java @@ -0,0 +1,291 @@ +package org.quiltmc.enigma.util; + +import com.google.common.collect.BiMap; +import com.google.common.collect.MapMaker; + +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; + +public class CompositeBiMap implements BiMap { + public static BiMap ofWeakValues() { + return of(new MapMaker().weakValues().makeMap(), new MapMaker().weakKeys().makeMap()); + } + + public static BiMap of(Map forward, Map reverse) { + return new CompositeBiMap<>(forward, reverse); + } + + private final Map forward; + private final Map reverse; + + CompositeBiMap inverse; + + private CompositeBiMap(Map forward, Map reverse) { + this.forward = forward; + this.reverse = reverse; + } + + @Override + public int size() { + return this.forward.size(); + } + + @Override + public boolean isEmpty() { + return this.forward.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + return this.forward.containsKey(key); + } + + @SuppressWarnings("SuspiciousMethodCalls") + @Override + public boolean containsValue(Object value) { + return this.reverse.containsKey(value); + } + + @Override + public V get(Object key) { + return this.forward.get(key); + } + + /** + * @throws IllegalArgumentException see {@link BiMap#put(Object, Object)} + * + * @see #put(Object, Object) + */ + @CheckForNull + @Override + public V put(K key, V value) { + if (this.containsValue(value)) { + throw new IllegalArgumentException( + "Tried to put duplicate value %s, already associated with key %s!" + .formatted(value, this.reverse.get(value)) + ); + } else { + return this.forcePut(key, value); + } + } + + @Override + public V remove(Object key) { + final V removed = this.forward.remove(key); + if (removed == null) { + return null; + } else { + this.reverse.remove(removed); + return removed; + } + } + + @CheckForNull + @Override + public V forcePut(K key, V value) { + this.reverse.put(value, key); + return this.forward.put(key, value); + } + + @Override + public void putAll(Map map) { + map.forEach(this::put); + } + + @Override + public void clear() { + this.forward.clear(); + this.reverse.clear(); + } + + @SuppressWarnings("SuspiciousMethodCalls") + @Override + @Nonnull + public Set keySet() { + return new LiveSet<>(Map.Entry::getKey, "key", this.forward::keySet, this.forward::containsKey); + } + + @SuppressWarnings("SuspiciousMethodCalls") + @Override + @Nonnull + public Set values() { + return new LiveSet<>(Map.Entry::getValue, "value", this.reverse::keySet, this.reverse::containsKey); + } + + @SuppressWarnings("SuspiciousMethodCalls") + @Override + @Nonnull + public Set> entrySet() { + return new LiveSet<>( + Function.identity(), "entry", this.forward::entrySet, + o -> o instanceof Map.Entry e + && this.forward.containsKey(e.getKey()) + && this.reverse.containsKey(e.getValue()) + ); + } + + @Override + @Nonnull + public BiMap inverse() { + if (this.inverse == null) { + this.inverse = new Inverse<>(this); + } + + return this.inverse; + } + + private static class Inverse extends CompositeBiMap { + Inverse(CompositeBiMap original) { + super(original.reverse, original.forward); + this.inverse = original; + } + + @Override + @Nonnull + public BiMap inverse() { + return this.inverse; + } + } + + private class LiveSet implements Set { + private static UnsupportedOperationException createAddException(String elementName) { + return new UnsupportedOperationException("Cannot add to map via " + elementName + " set!"); + } + + final Function, E> elementFromEntry; + final Supplier> getDelegateSet; + final Predicate containsElement; + + final String elementName; + + LiveSet( + Function, E> elementFromEntry, String elementName, + Supplier> getDelegateSet, Predicate containsElement + ) { + this.elementFromEntry = elementFromEntry; + this.elementName = elementName; + this.getDelegateSet = getDelegateSet; + this.containsElement = containsElement; + } + + @Override + public int size() { + return CompositeBiMap.this.size(); + } + + @Override + public boolean isEmpty() { + return CompositeBiMap.this.isEmpty(); + } + + @Override + public boolean contains(Object o) { + return this.containsElement.test(o); + } + + @Override + @Nonnull + public Iterator iterator() { + return new LiveIterator(); + } + + @Override + @Nonnull + public Object[] toArray() { + return this.getDelegateSet.get().toArray(); + } + + @Override + @Nonnull + public T[] toArray(@Nonnull T[] array) { + return this.getDelegateSet.get().toArray(array); + } + + @Override + public boolean add(E element) { + throw createAddException(this.elementName); + } + + @Override + public boolean remove(Object o) { + return CompositeBiMap.this.remove(o) != null; + } + + @Override + public boolean containsAll(@Nonnull Collection collection) { + for (final Object o : collection) { + if (!this.containsElement.test(o)) { + return false; + } + } + + return true; + } + + @Override + public boolean addAll(@Nonnull Collection collection) { + throw createAddException(this.elementName); + } + + @Override + public boolean retainAll(@Nonnull Collection collection) { + return this.removeAllMatching(key -> !collection.contains(key)); + } + + @Override + public boolean removeAll(Collection collection) { + return this.removeAllMatching(collection::contains); + } + + private boolean removeAllMatching(Predicate predicate) { + boolean removed = false; + final Iterator> itr = CompositeBiMap.this.forward.entrySet().iterator(); + while (itr.hasNext()) { + final Entry entry = itr.next(); + if (predicate.test(this.elementFromEntry.apply(entry))) { + removed = true; + itr.remove(); + CompositeBiMap.this.reverse.remove(entry.getValue()); + } + } + + return removed; + } + + @Override + public void clear() { + CompositeBiMap.this.clear(); + } + + private final class LiveIterator implements Iterator { + final Iterator> delegate = CompositeBiMap.this.forward.entrySet().iterator(); + + Entry current; + + @Override + public boolean hasNext() { + return this.delegate.hasNext(); + } + + @Override + public E next() { + this.current = this.delegate.next(); + return LiveSet.this.elementFromEntry.apply(this.current); + } + + @Override + public void remove() { + this.delegate.remove(); + + CompositeBiMap.this.reverse.remove(this.current.getValue()); + } + } + } +} diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/AbstractMapMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/AbstractMapMultiTrie.java deleted file mode 100644 index ae528038a..000000000 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/AbstractMapMultiTrie.java +++ /dev/null @@ -1,58 +0,0 @@ -package org.quiltmc.enigma.util.multi_trie; - -import com.google.common.collect.BiMap; -import org.quiltmc.enigma.util.Utils; -import org.quiltmc.enigma.util.multi_trie.AbstractMapMultiTrie.Node; - -import javax.annotation.Nonnull; -import java.util.Collection; -import java.util.stream.Stream; - -public abstract class AbstractMapMultiTrie> implements MultiTrie { - protected final N root; - - protected AbstractMapMultiTrie(N root) { - this.root = Utils.requireNonNull(root, "root"); - } - - @Nonnull - @Override - public N getRoot() { - return this.root; - } - - /** - * @param the type of keys - * @param the type of values - * @param the type of this node - */ - protected abstract static class Node> implements MultiTrie.Node { - protected final BiMap children; - protected final Collection leaves; - - protected Node(BiMap children, Collection leaves) { - this.children = Utils.requireNonNull(children, "children"); - this.leaves = Utils.requireNonNull(leaves, "leaves"); - } - - @Override - public Stream streamLeaves() { - return this.leaves.stream(); - } - - @Override - public Stream streamBranches() { - return this.children.values().stream().flatMap(Node::streamValues); - } - - @Override - public Stream streamValues() { - return Stream.concat(this.streamLeaves(), this.streamBranches()); - } - - @Override - public N next(K key) { - return this.children.get(Utils.requireNonNull(key, "key")); - } - } -} diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/AbstractMutableMapMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/AbstractMutableMapMultiTrie.java deleted file mode 100644 index 041a7f17f..000000000 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/AbstractMutableMapMultiTrie.java +++ /dev/null @@ -1,93 +0,0 @@ -package org.quiltmc.enigma.util.multi_trie; - -import com.google.common.collect.BiMap; -import org.checkerframework.dataflow.qual.Pure; -import org.quiltmc.enigma.util.multi_trie.AbstractMutableMapMultiTrie.Node; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.util.Collection; - -public abstract class AbstractMutableMapMultiTrie> - extends AbstractMapMultiTrie - implements MutableMultiTrie { - private final MutableMultiTrie.View view = new MutableMultiTrie.View.Impl<>(this); - - protected AbstractMutableMapMultiTrie(N root) { - super(root); - } - - @Override - public View getView() { - return this.view; - } - - public abstract static class Node> - extends AbstractMapMultiTrie.Node - implements MutableMultiTrie.Node { - @Nullable - protected final N parent; - - private final View view = new View.Impl<>(this); - - protected Node(@Nullable N parent, BiMap children, Collection leaves) { - super(children, leaves); - this.parent = parent; - } - - @Override - public void put(V value) { - this.leaves.add(value); - } - - @Override - public boolean remove(V value) { - if (this.leaves.remove(value)) { - this.pruneIfEmpty(); - - return true; - } else { - return false; - } - } - - @Override - public boolean removeAll() { - final boolean hasLeaves = !this.leaves.isEmpty(); - if (hasLeaves) { - this.leaves.clear(); - this.pruneIfEmpty(); - - return true; - } else { - return false; - } - } - - @Override - public View getView() { - return this.view; - } - - protected void pruneIfEmpty() { - if (this.parent != null && this.isEmpty()) { - this.parent.children.inverse().remove(this.getSelf()); - this.parent.pruneIfEmpty(); - } - } - - /** - * @return this node - */ - @Nonnull - @Pure - protected abstract N getSelf(); - - /** - * @return a new, empty child node instance - */ - @Nonnull - @Pure - protected abstract N createChild(); - } -} diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java index 8b298fdc1..07736119e 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java @@ -8,12 +8,23 @@ import javax.annotation.Nullable; import java.util.Collection; import java.util.HashSet; +import java.util.Optional; import java.util.function.Supplier; import java.util.function.UnaryOperator; public final class CompositeStringMultiTrie extends StringMultiTrie> { + private final Node root; + private final View view = new View(); + public static CompositeStringMultiTrie createHashed() { - return new CompositeStringMultiTrie<>(HashBiMap::create, HashSet::new); + return of(HashBiMap::create, HashSet::new); + } + + public static CompositeStringMultiTrie of( + Supplier>> childrenFactory, + Supplier> leavesFactory + ) { + return new CompositeStringMultiTrie<>(childrenFactory, leavesFactory); } private static Node createNode( @@ -22,8 +33,7 @@ private static Node createNode( Supplier> leavesFactory ) { return new Node<>( - parent, - childrenFactory.get(), leavesFactory.get(), + parent, childrenFactory.get(), leavesFactory.get(), self -> createNode(self, childrenFactory, leavesFactory) ); } @@ -32,20 +42,40 @@ private CompositeStringMultiTrie( Supplier>> childrenFactory, Supplier> leavesFactory ) { - super(createNode(null, childrenFactory, leavesFactory)); + this.root = createNode(null, childrenFactory, leavesFactory); + } + + @Nonnull + @Override + public Node getRoot() { + return this.root; } - public static final class Node extends AbstractMutableMapMultiTrie.Node> { + @Override + @Nonnull + public StringMultiTrie.View> getView() { + return this.view; + } + + public static final class Node extends MutableMapNode> { + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + private final Optional> parent; + + private final BiMap> children; + private final Collection leaves; + private final UnaryOperator> childFactory; - // private final View view = new View(); + private final NodeView view = new NodeView<>(this); private Node( - @Nullable Node parent, BiMap> children, Collection leaves, + @Nullable Node parent, + BiMap> children, Collection leaves, UnaryOperator> childFactory ) { - super(parent, children, leaves); - + this.parent = Optional.ofNullable(parent); + this.children = children; + this.leaves = leaves; this.childFactory = childFactory; } @@ -55,10 +85,38 @@ protected Node getSelf() { return this; } + @Override + protected Optional> getParent() { + return this.parent; + } + @Nonnull @Override protected Node createChild() { return this.childFactory.apply(this); } + + @Override + protected Collection getLeaves() { + return this.leaves; + } + + @Override + @Nonnull + protected BiMap> getChildren() { + return this.children; + } + + @Override + public MultiTrie.Node getView() { + return this.view; + } + } + + private class View extends StringMultiTrie.View> { + @Override + protected StringMultiTrie> getViewed() { + return CompositeStringMultiTrie.this; + } } } diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/EmptyNode.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/EmptyNode.java deleted file mode 100644 index af5f211d5..000000000 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/EmptyNode.java +++ /dev/null @@ -1,44 +0,0 @@ -package org.quiltmc.enigma.util.multi_trie; - -import javax.annotation.Nonnull; -import java.util.stream.Stream; - -/** - * An empty, immutable, singleton {@link MultiTrie.Node} implementation. - * - *

{@link MultiTrie.Node#nextOrEmpty(Object)} implementations may return {@linkplain #get() the empty node} - * when nodes have no branches. - * - * @implNote not intended to be stored in tries - */ -public final class EmptyNode implements MutableMultiTrie.Node.View { - private static final EmptyNode INSTANCE = new EmptyNode<>(); - - @SuppressWarnings("unchecked") - public static EmptyNode get() { - return (EmptyNode) INSTANCE; - } - - private EmptyNode() { } - - @Override - public Stream streamLeaves() { - return Stream.empty(); - } - - @Override - public Stream streamBranches() { - return Stream.empty(); - } - - @Override - public Stream streamValues() { - return Stream.empty(); - } - - @Override - @Nonnull - public EmptyNode next(K key) { - return this; - } -} diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MapNode.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MapNode.java new file mode 100644 index 000000000..4903eb380 --- /dev/null +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MapNode.java @@ -0,0 +1,30 @@ +package org.quiltmc.enigma.util.multi_trie; + +import org.checkerframework.dataflow.qual.Pure; + +import javax.annotation.Nonnull; +import java.util.Map; +import java.util.stream.Stream; + +/** + * A {@link MultiTrie.Node} that stores child nodes in a {@link Map}. + * + * @param the type of keys + * @param the type of values + * @param the type of this node + */ +public abstract class MapNode> implements MultiTrie.Node { + @Override + public Stream streamBranches() { + return this.getChildren().values().stream().flatMap(MapNode::streamValues); + } + + @Override + public Stream streamValues() { + return Stream.concat(this.streamLeaves(), this.streamBranches()); + } + + @Nonnull + @Pure + protected abstract Map getChildren(); +} diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MultiTrie.java index 6b0bc593b..58bc8b4ad 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MultiTrie.java @@ -1,7 +1,6 @@ package org.quiltmc.enigma.util.multi_trie; import javax.annotation.Nonnull; -import javax.annotation.Nullable; import java.util.stream.Stream; /** @@ -62,13 +61,7 @@ default boolean isEmpty() { return this.getSize() == 0; } - @Nullable - Node next(K key); - @Nonnull - default Node nextOrEmpty(K key) { - final Node next = this.next(key); - return next == null ? EmptyNode.get() : next; - } + Node next(K key); } } diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMapNode.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMapNode.java new file mode 100644 index 000000000..c12fd51ea --- /dev/null +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMapNode.java @@ -0,0 +1,119 @@ +package org.quiltmc.enigma.util.multi_trie; + +import com.google.common.collect.BiMap; +import org.checkerframework.dataflow.qual.Pure; +import org.quiltmc.enigma.util.CompositeBiMap; +import org.quiltmc.enigma.util.Utils; + +import javax.annotation.Nonnull; +import java.util.Collection; +import java.util.Optional; +import java.util.stream.Stream; + +/** + * A {@link MutableMultiTrie.Node} that stores child nodes in a {@link BiMap}. + * + * @implNote A {@link BiMap} is used to facilitate pruning of empty nodes; child nodes can remove themselves from their + * parents without knowing their key. + * + * @param the type of keys + * @param the type of values + * @param the type of this node + */ +public abstract class MutableMapNode> + extends MapNode + implements MutableMultiTrie.Node { + @Override + public Stream streamLeaves() { + return this.getLeaves().stream(); + } + + /** + * Orphans are empty nodes. + * + *

They may be moved to {@link #getChildren()} when they become non-empty. + * + * @implNote Using a map with weak value references prevents memory leaks when users look up a sequence with no + * values and don't put any value in it. + */ + final BiMap orphans = CompositeBiMap.ofWeakValues(); + + @Override + @Nonnull + public N next(K key) { + final N next = this.nextImpl(Utils.requireNonNull(key, "key")); + return next == null ? this.orphans.computeIfAbsent(key, ignored -> this.createChild()) : next; + } + + protected N nextImpl(K key) { + return this.getChildren().get(key); + } + + @Override + public void put(V value) { + this.getLeaves().add(value); + this.getParent().ifPresent(parent -> { + final N self = this.getSelf(); + final K key = parent.orphans.inverse().remove(self); + if (key != null) { + parent.getChildren().put(key, self); + } + }); + } + + @Override + public boolean remove(V value) { + if (this.getLeaves().remove(value)) { + this.pruneIfEmpty(); + + return true; + } else { + return false; + } + } + + @Override + public boolean removeAll() { + final boolean hasLeaves = !this.getLeaves().isEmpty(); + if (hasLeaves) { + this.getLeaves().clear(); + this.pruneIfEmpty(); + + return true; + } else { + return false; + } + } + + protected void pruneIfEmpty() { + this.getParent().ifPresent(parent -> { + if (this.isEmpty()) { + parent.getChildren().inverse().remove(this.getSelf()); + parent.pruneIfEmpty(); + } + }); + } + + @Override + @Nonnull + protected abstract BiMap getChildren(); + + /** + * @return this node + */ + @Nonnull + @Pure + protected abstract N getSelf(); + + @Pure + protected abstract Optional getParent(); + + /** + * @return a new, empty child node instance + */ + @Nonnull + @Pure + protected abstract N createChild(); + + protected abstract Collection getLeaves(); +} diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMultiTrie.java index 5b4d313e7..da99fa273 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMultiTrie.java @@ -1,11 +1,8 @@ package org.quiltmc.enigma.util.multi_trie; -import org.quiltmc.enigma.util.Utils; import org.quiltmc.enigma.util.multi_trie.MutableMultiTrie.Node; import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.util.stream.Stream; /** * A multi-trie that allows modification which can also provide unmodifiable views of its contents. @@ -21,7 +18,7 @@ public interface MutableMultiTrie> extends MultiTrie< /** * @return a live, unmodifiable view of this trie */ - View getView(); + MultiTrie getView(); /** * A mutable node representing values associated with a {@link MutableMultiTrie}. @@ -31,7 +28,7 @@ public interface MutableMultiTrie> extends MultiTrie< */ interface Node extends MultiTrie.Node { @Override - @Nullable + @Nonnull Node next(K key); /** @@ -56,68 +53,6 @@ interface Node extends MultiTrie.Node { /** * @return a live, unmodifiable view of this node */ - View getView(); - - interface View extends MultiTrie.Node { - @Override - @Nonnull - View next(K key); - - @Override - @Nonnull - default View nextOrEmpty(K key) { - return this.next(key); - } - - class Impl implements View { - protected final Node viewed; - - public Impl(Node viewed) { - this.viewed = viewed; - } - - @Override - public Stream streamLeaves() { - return this.viewed.streamLeaves(); - } - - @Override - public Stream streamBranches() { - return this.viewed.streamBranches(); - } - - @Override - public Stream streamValues() { - return this.viewed.streamValues(); - } - - @Nonnull - @Override - public View next(K key) { - final Node next = this.viewed.next(key); - return next == null ? EmptyNode.get() : next.getView(); - } - } - } - } - - interface View extends MultiTrie { - @Nonnull - @Override - MutableMultiTrie.Node.View getRoot(); - - class Impl implements View { - protected final MutableMultiTrie> viewed; - - public Impl(MutableMultiTrie> viewed) { - this.viewed = Utils.requireNonNull(viewed, "viewed"); - } - - @Nonnull - @Override - public MutableMultiTrie.Node.View getRoot() { - return this.viewed.getRoot().getView(); - } - } + MultiTrie.Node getView(); } } diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/NodeView.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/NodeView.java new file mode 100644 index 000000000..ac7a5b9aa --- /dev/null +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/NodeView.java @@ -0,0 +1,33 @@ +package org.quiltmc.enigma.util.multi_trie; + +import javax.annotation.Nonnull; +import java.util.stream.Stream; + +public final class NodeView implements MultiTrie.Node { + private final MutableMultiTrie.Node viewed; + + public NodeView(MutableMultiTrie.Node viewed) { + this.viewed = viewed; + } + + @Override + public Stream streamLeaves() { + return this.viewed.streamLeaves(); + } + + @Override + public Stream streamBranches() { + return this.viewed.streamBranches(); + } + + @Override + public Stream streamValues() { + return this.viewed.streamValues(); + } + + @Nonnull + @Override + public MultiTrie.Node next(K key) { + return this.viewed.next(key).getView(); + } +} diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java index d741621eb..2bf16ccdf 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java @@ -4,45 +4,54 @@ import javax.annotation.Nonnull; -public abstract class StringMultiTrie> - extends AbstractMutableMapMultiTrie { +/** + * A {@link MutableMultiTrie} that associates sequences of characters with values of type {@code V}. + * + *

Adds convenience methods for accessing contents by passing a {@link String} instead of passing individual + * characters to nodes: + *

    + *
  • {@link #get(String)} + *
  • {@link #put(String, Object)} + *
  • {@link #remove(String, Object)} + *
  • {@link #removeAll(String)} + *
+ * + *

{@linkplain #getView() Views} also provide a {@link #get(String)} method. + * + * @param the type of values + * @param the type of nodes + */ +public abstract class StringMultiTrie> + implements MutableMultiTrie { private static final String PREFIX = "prefix"; private static final String STRING = "string"; private static final String VALUE = "value"; - protected StringMultiTrie(N root) { - super(root); - } + @Override + @Nonnull + public abstract View getView(); + @Nonnull public N get(String prefix) { Utils.requireNonNull(prefix, PREFIX); - N node = this.root; + N node = this.getRoot(); for (int i = 0; i < prefix.length(); i++) { node = node.next(prefix.charAt(i)); - if (node == null) { - return null; - } } return node; } - @Nonnull - public MultiTrie.Node getView(String prefix) { - final N node = this.get(prefix); - return node == null ? EmptyNode.get() : node.getView(); - } - @Nonnull public N put(String string, V value) { Utils.requireNonNull(string, STRING); Utils.requireNonNull(value, VALUE); - N node = this.root; + N node = this.getRoot(); for (int i = 0; i < string.length(); i++) { final N parent = node; - node = node.children.computeIfAbsent(string.charAt(i), ignored -> parent.createChild()); + node = node.getChildren().computeIfAbsent(string.charAt(i), ignored -> parent.createChild()); } node.put(value); @@ -54,9 +63,9 @@ public boolean remove(String string, V value) { Utils.requireNonNull(string, STRING); Utils.requireNonNull(value, VALUE); - N node = this.root; + N node = this.getRoot(); for (int i = 0; i < string.length(); i++) { - node = node.next(string.charAt(i)); + node = node.nextImpl(string.charAt(i)); if (node == null) { return false; @@ -69,9 +78,9 @@ public boolean remove(String string, V value) { public boolean removeAll(String string) { Utils.requireNonNull(string, STRING); - N node = this.root; + N node = this.getRoot(); for (int i = 0; i < string.length(); i++) { - node = node.next(string.charAt(i)); + node = node.nextImpl(string.charAt(i)); if (node == null) { return false; @@ -80,4 +89,18 @@ public boolean removeAll(String string) { return node.removeAll(); } + + public abstract static class View> implements MultiTrie { + @Nonnull + @Override + public Node getRoot() { + return this.getViewed().getRoot().getView(); + } + + public MultiTrie.Node get(String prefix) { + return this.getViewed().get(prefix).getView(); + } + + protected abstract StringMultiTrie getViewed(); + } } diff --git a/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/AbstractMapMultiTrieAccessor.java b/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/AbstractMapMultiTrieAccessor.java deleted file mode 100644 index afa58dd5f..000000000 --- a/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/AbstractMapMultiTrieAccessor.java +++ /dev/null @@ -1,16 +0,0 @@ -package org.quiltmc.enigma.util.multi_trie; - -import com.google.common.collect.BiMap; - -public class AbstractMapMultiTrieAccessor> - extends AbstractMapMultiTrie { - public static BiMap> getRootChildren( - AbstractMapMultiTrie> trie - ) { - return new AbstractMapMultiTrieAccessor<>(trie).root.children; - } - - public AbstractMapMultiTrieAccessor(AbstractMapMultiTrie accessed) { - super(accessed.root); - } -} diff --git a/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java b/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java index d16d6977d..dacf55bf2 100644 --- a/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java +++ b/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java @@ -15,8 +15,9 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.quiltmc.enigma.util.multi_trie.AbstractMapMultiTrieAccessor.getRootChildren; - +/** + * TODO key-by-key access tests + */ public class CompositeStringMultiTrieTest { private static final String VALUES = "values"; private static final String LEAVES = "leaves"; @@ -140,7 +141,7 @@ private static void assertEmpty(CompositeStringMultiTrie trie) { () ->"Expected trie to be empty, but had it contents: " + trie.getRoot().streamValues().toList() ); - final BiMap> rootChildren = getRootChildren(trie); + final BiMap> rootChildren = trie.getRoot().getChildren(); assertTrue( rootChildren.isEmpty(), () -> "Expected root's children to be pruned, but it had children: " + rootChildren From 7cd9b94409e12400004cc02d4d1713fa19b09eb6 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 12 Nov 2025 20:23:20 -0800 Subject: [PATCH 028/124] rename createAddException -> addExceptionOf --- .../main/java/org/quiltmc/enigma/util/CompositeBiMap.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/CompositeBiMap.java b/enigma/src/main/java/org/quiltmc/enigma/util/CompositeBiMap.java index cf796118f..d02a38afb 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/CompositeBiMap.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/CompositeBiMap.java @@ -155,7 +155,7 @@ public BiMap inverse() { } private class LiveSet implements Set { - private static UnsupportedOperationException createAddException(String elementName) { + private static UnsupportedOperationException addExceptionOf(String elementName) { return new UnsupportedOperationException("Cannot add to map via " + elementName + " set!"); } @@ -210,7 +210,7 @@ public T[] toArray(@Nonnull T[] array) { @Override public boolean add(E element) { - throw createAddException(this.elementName); + throw addExceptionOf(this.elementName); } @Override @@ -231,7 +231,7 @@ public boolean containsAll(@Nonnull Collection collection) { @Override public boolean addAll(@Nonnull Collection collection) { - throw createAddException(this.elementName); + throw addExceptionOf(this.elementName); } @Override From 02cb9cc80d0bfd8dcdb7ec36cd3bd141fee1fa28 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 12 Nov 2025 20:28:20 -0800 Subject: [PATCH 029/124] fix CompositeBiMap::forcePut --- .../src/main/java/org/quiltmc/enigma/util/CompositeBiMap.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/CompositeBiMap.java b/enigma/src/main/java/org/quiltmc/enigma/util/CompositeBiMap.java index d02a38afb..6569f90f4 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/CompositeBiMap.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/CompositeBiMap.java @@ -72,7 +72,8 @@ public V put(K key, V value) { .formatted(value, this.reverse.get(value)) ); } else { - return this.forcePut(key, value); + this.reverse.put(value, key); + return this.forward.put(key, value); } } @@ -90,6 +91,7 @@ public V remove(Object key) { @CheckForNull @Override public V forcePut(K key, V value) { + this.reverse.remove(value); this.reverse.put(value, key); return this.forward.put(key, value); } From be1b6e8cc6e87cf2d4638caa31b75e20d248f5b6 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Thu, 13 Nov 2025 08:19:59 -0800 Subject: [PATCH 030/124] make nodes key-aware instead of using BiMaps :[ --- .../multi_trie/CompositeStringMultiTrie.java | 59 +++++++++++-------- .../util/multi_trie/MutableMapNode.java | 52 ++++++++-------- .../util/multi_trie/StringMultiTrie.java | 3 +- .../CompositeStringMultiTrieTest.java | 8 +-- 4 files changed, 65 insertions(+), 57 deletions(-) diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java index 07736119e..ab29756b4 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java @@ -1,48 +1,57 @@ package org.quiltmc.enigma.util.multi_trie; -import com.google.common.collect.BiMap; -import com.google.common.collect.HashBiMap; import org.quiltmc.enigma.util.multi_trie.CompositeStringMultiTrie.Node; import javax.annotation.Nonnull; -import javax.annotation.Nullable; import java.util.Collection; +import java.util.HashMap; import java.util.HashSet; +import java.util.Map; import java.util.Optional; +import java.util.function.Function; import java.util.function.Supplier; -import java.util.function.UnaryOperator; public final class CompositeStringMultiTrie extends StringMultiTrie> { private final Node root; private final View view = new View(); public static CompositeStringMultiTrie createHashed() { - return of(HashBiMap::create, HashSet::new); + return of(HashMap::new, HashSet::new); } public static CompositeStringMultiTrie of( - Supplier>> childrenFactory, + Supplier>> childrenFactory, Supplier> leavesFactory ) { return new CompositeStringMultiTrie<>(childrenFactory, leavesFactory); } + private static Node createRoot( + Supplier>> childrenFactory, + Supplier> leavesFactory + ) { + return new Node<>( + Optional.empty(), childrenFactory.get(), leavesFactory.get(), + selfAccess -> createNode(selfAccess, childrenFactory, leavesFactory) + ); + } + private static Node createNode( - @Nullable Node parent, - Supplier>> childrenFactory, + MutableMapNode.ParentAccess, Character> parentAccess, + Supplier>> childrenFactory, Supplier> leavesFactory ) { return new Node<>( - parent, childrenFactory.get(), leavesFactory.get(), - self -> createNode(self, childrenFactory, leavesFactory) + Optional.of(parentAccess), childrenFactory.get(), leavesFactory.get(), + selfAccess -> createNode(selfAccess, childrenFactory, leavesFactory) ); } private CompositeStringMultiTrie( - Supplier>> childrenFactory, + Supplier>> childrenFactory, Supplier> leavesFactory ) { - this.root = createNode(null, childrenFactory, leavesFactory); + this.root = createRoot(childrenFactory, leavesFactory); } @Nonnull @@ -57,23 +66,23 @@ public StringMultiTrie.View> getView() { return this.view; } + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") public static final class Node extends MutableMapNode> { - @SuppressWarnings("OptionalUsedAsFieldOrParameterType") - private final Optional> parent; + private final Optional, Character>> parentAccess; - private final BiMap> children; + private final Map> children; private final Collection leaves; - private final UnaryOperator> childFactory; + private final Function, Character>, Node> childFactory; private final NodeView view = new NodeView<>(this); private Node( - @Nullable Node parent, - BiMap> children, Collection leaves, - UnaryOperator> childFactory + Optional, Character>> parentAccess, + Map> children, Collection leaves, + Function, Character>, Node> childFactory ) { - this.parent = Optional.ofNullable(parent); + this.parentAccess = parentAccess; this.children = children; this.leaves = leaves; this.childFactory = childFactory; @@ -86,14 +95,14 @@ protected Node getSelf() { } @Override - protected Optional> getParent() { - return this.parent; + protected Optional, Character>> getParentAccess() { + return this.parentAccess; } @Nonnull @Override - protected Node createChild() { - return this.childFactory.apply(this); + protected Node createChildImpl(ParentAccess, Character> parentAccess) { + return this.childFactory.apply(parentAccess); } @Override @@ -103,7 +112,7 @@ protected Collection getLeaves() { @Override @Nonnull - protected BiMap> getChildren() { + protected Map> getChildren() { return this.children; } diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMapNode.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMapNode.java index c12fd51ea..4bf74deb0 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMapNode.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMapNode.java @@ -1,20 +1,19 @@ package org.quiltmc.enigma.util.multi_trie; -import com.google.common.collect.BiMap; +import com.google.common.collect.MapMaker; import org.checkerframework.dataflow.qual.Pure; -import org.quiltmc.enigma.util.CompositeBiMap; import org.quiltmc.enigma.util.Utils; import javax.annotation.Nonnull; import java.util.Collection; +import java.util.Map; import java.util.Optional; import java.util.stream.Stream; /** - * A {@link MutableMultiTrie.Node} that stores child nodes in a {@link BiMap}. + * A {@link MutableMultiTrie.Node} that stores child nodes in a {@link Map}. * - * @implNote A {@link BiMap} is used to facilitate pruning of empty nodes; child nodes can remove themselves from their - * parents without knowing their key. + *

Nodes are aware of their keys so that they can manage their presence in maps. * * @param the type of keys * @param the type of values @@ -23,11 +22,6 @@ public abstract class MutableMapNode> extends MapNode implements MutableMultiTrie.Node { - @Override - public Stream streamLeaves() { - return this.getLeaves().stream(); - } - /** * Orphans are empty nodes. * @@ -36,13 +30,18 @@ public Stream streamLeaves() { * @implNote Using a map with weak value references prevents memory leaks when users look up a sequence with no * values and don't put any value in it. */ - final BiMap orphans = CompositeBiMap.ofWeakValues(); + final Map orphans = new MapMaker().weakValues().makeMap(); + + @Override + public Stream streamLeaves() { + return this.getLeaves().stream(); + } @Override @Nonnull public N next(K key) { final N next = this.nextImpl(Utils.requireNonNull(key, "key")); - return next == null ? this.orphans.computeIfAbsent(key, ignored -> this.createChild()) : next; + return next == null ? this.orphans.computeIfAbsent(key, ignored -> this.createChild(key)) : next; } protected N nextImpl(K key) { @@ -52,11 +51,10 @@ protected N nextImpl(K key) { @Override public void put(V value) { this.getLeaves().add(value); - this.getParent().ifPresent(parent -> { - final N self = this.getSelf(); - final K key = parent.orphans.inverse().remove(self); - if (key != null) { - parent.getChildren().put(key, self); + this.getParentAccess().ifPresent(access -> { + final boolean wasOrphan = access.parent.orphans.remove(access.key) != null; + if (wasOrphan) { + access.parent.getChildren().put(access.key, this.getSelf()); } }); } @@ -86,18 +84,14 @@ public boolean removeAll() { } protected void pruneIfEmpty() { - this.getParent().ifPresent(parent -> { + this.getParentAccess().ifPresent(access -> { if (this.isEmpty()) { - parent.getChildren().inverse().remove(this.getSelf()); - parent.pruneIfEmpty(); + access.parent.getChildren().remove(access.key); + access.parent.pruneIfEmpty(); } }); } - @Override - @Nonnull - protected abstract BiMap getChildren(); - /** * @return this node */ @@ -106,14 +100,20 @@ protected void pruneIfEmpty() { protected abstract N getSelf(); @Pure - protected abstract Optional getParent(); + protected abstract Optional> getParentAccess(); /** * @return a new, empty child node instance */ @Nonnull @Pure - protected abstract N createChild(); + protected final N createChild(K key) { + return this.createChildImpl(new ParentAccess<>(this.getSelf(), key)); + } + + protected abstract N createChildImpl(ParentAccess parentAccess); protected abstract Collection getLeaves(); + + protected record ParentAccess(N parent, K key) { } } diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java index 2bf16ccdf..36d86465f 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java @@ -51,7 +51,8 @@ public N put(String string, V value) { N node = this.getRoot(); for (int i = 0; i < string.length(); i++) { final N parent = node; - node = node.getChildren().computeIfAbsent(string.charAt(i), ignored -> parent.createChild()); + final char key = string.charAt(i); + node = node.getChildren().computeIfAbsent(key, ignored -> parent.createChild(key)); } node.put(value); diff --git a/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java b/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java index dacf55bf2..ce562018d 100644 --- a/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java +++ b/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java @@ -1,6 +1,5 @@ package org.quiltmc.enigma.util.multi_trie; -import com.google.common.collect.BiMap; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMultimap; import org.junit.jupiter.api.Test; @@ -8,6 +7,7 @@ import java.util.Collection; import java.util.List; +import java.util.Map; import java.util.stream.Stream; import static org.hamcrest.MatcherAssert.assertThat; @@ -15,9 +15,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; -/** - * TODO key-by-key access tests - */ +// TODO key-by-key access tests public class CompositeStringMultiTrieTest { private static final String VALUES = "values"; private static final String LEAVES = "leaves"; @@ -141,7 +139,7 @@ private static void assertEmpty(CompositeStringMultiTrie trie) { () ->"Expected trie to be empty, but had it contents: " + trie.getRoot().streamValues().toList() ); - final BiMap> rootChildren = trie.getRoot().getChildren(); + final Map> rootChildren = trie.getRoot().getChildren(); assertTrue( rootChildren.isEmpty(), () -> "Expected root's children to be pruned, but it had children: " + rootChildren From 2a5fd0568d4cb773dd4c025b9767067d5e45391e Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Thu, 13 Nov 2025 16:06:50 -0800 Subject: [PATCH 031/124] rename MultiTrie.Node::streamBranches -> streamStems introduce separate branch node type extending MutableMapNode eliminate parent nullability/optionality --- .../multi_trie/CompositeStringMultiTrie.java | 150 +++++++++++------- .../enigma/util/multi_trie/MapNode.java | 10 +- .../enigma/util/multi_trie/MultiTrie.java | 5 +- .../util/multi_trie/MutableMapNode.java | 132 +++++++++------ .../util/multi_trie/MutableMultiTrie.java | 14 +- .../enigma/util/multi_trie/NodeView.java | 4 +- .../util/multi_trie/StringMultiTrie.java | 41 +++-- .../CompositeStringMultiTrieTest.java | 6 +- 8 files changed, 209 insertions(+), 153 deletions(-) diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java index ab29756b4..ed7e9e82d 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java @@ -1,18 +1,13 @@ package org.quiltmc.enigma.util.multi_trie; -import org.quiltmc.enigma.util.multi_trie.CompositeStringMultiTrie.Node; - -import javax.annotation.Nonnull; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Map; -import java.util.Optional; -import java.util.function.Function; import java.util.function.Supplier; -public final class CompositeStringMultiTrie extends StringMultiTrie> { - private final Node root; +public final class CompositeStringMultiTrie extends StringMultiTrie> { + private final Root root; private final View view = new View(); public static CompositeStringMultiTrie createHashed() { @@ -20,89 +15,119 @@ public static CompositeStringMultiTrie createHashed() { } public static CompositeStringMultiTrie of( - Supplier>> childrenFactory, - Supplier> leavesFactory - ) { - return new CompositeStringMultiTrie<>(childrenFactory, leavesFactory); - } - - private static Node createRoot( - Supplier>> childrenFactory, + Supplier>> branchesFactory, Supplier> leavesFactory ) { - return new Node<>( - Optional.empty(), childrenFactory.get(), leavesFactory.get(), - selfAccess -> createNode(selfAccess, childrenFactory, leavesFactory) - ); + return new CompositeStringMultiTrie<>(branchesFactory, leavesFactory); } - private static Node createNode( - MutableMapNode.ParentAccess, Character> parentAccess, - Supplier>> childrenFactory, + private static Root createRoot( + Supplier>> branchesFactory, Supplier> leavesFactory ) { - return new Node<>( - Optional.of(parentAccess), childrenFactory.get(), leavesFactory.get(), - selfAccess -> createNode(selfAccess, childrenFactory, leavesFactory) + return new Root<>( + branchesFactory.get(), leavesFactory.get(), + new Branch.Factory<>(leavesFactory, branchesFactory) ); } private CompositeStringMultiTrie( - Supplier>> childrenFactory, + Supplier>> childrenFactory, Supplier> leavesFactory ) { this.root = createRoot(childrenFactory, leavesFactory); } - @Nonnull @Override - public Node getRoot() { + public MutableMapNode> getRoot() { return this.root; } @Override - @Nonnull - public StringMultiTrie.View> getView() { + public StringMultiTrie.View getView() { return this.view; } - @SuppressWarnings("OptionalUsedAsFieldOrParameterType") - public static final class Node extends MutableMapNode> { - private final Optional, Character>> parentAccess; - - private final Map> children; + private static final class Root extends MutableMapNode> { private final Collection leaves; + private final Map> branches; - private final Function, Character>, Node> childFactory; + private final CompositeStringMultiTrie.Branch.Factory branchFactory; private final NodeView view = new NodeView<>(this); - private Node( - Optional, Character>> parentAccess, - Map> children, Collection leaves, - Function, Character>, Node> childFactory + private Root( + Map> branches, Collection leaves, + CompositeStringMultiTrie.Branch.Factory branchFactory ) { - this.parentAccess = parentAccess; - this.children = children; this.leaves = leaves; - this.childFactory = childFactory; + this.branches = branches; + this.branchFactory = branchFactory; } - @Nonnull @Override - protected Node getSelf() { - return this; + protected CompositeStringMultiTrie.Branch createBranch(Character key) { + return this.branchFactory.create(key, this); + } + + @Override + protected Collection getLeaves() { + return this.leaves; + } + + @Override + protected Map> getBranches() { + return this.branches; + } + + @Override + public MultiTrie.Node getView() { + return this.view; + } + } + + public static final class Branch extends MutableMapNode.Branch> { + private final MutableMapNode> parent; + private final Character key; + + private final Collection leaves; + private final Map> branches; + + private final CompositeStringMultiTrie.Branch.Factory branchFactory; + + private final MultiTrie.Node view = new NodeView<>(this); + + private Branch( + MutableMapNode> parent, char key, + Collection leaves, Map> branches, + CompositeStringMultiTrie.Branch.Factory branchFactory + ) { + this.parent = parent; + this.key = key; + + this.leaves = leaves; + this.branches = branches; + this.branchFactory = branchFactory; } @Override - protected Optional, Character>> getParentAccess() { - return this.parentAccess; + protected MutableMapNode> getParent() { + return this.parent; } - @Nonnull @Override - protected Node createChildImpl(ParentAccess, Character> parentAccess) { - return this.childFactory.apply(parentAccess); + protected Character getKey() { + return this.key; + } + + @Override + protected CompositeStringMultiTrie.Branch getSelf() { + return this; + } + + @Override + protected CompositeStringMultiTrie.Branch createBranch(Character key) { + return this.branchFactory.create(key, this); } @Override @@ -111,20 +136,33 @@ protected Collection getLeaves() { } @Override - @Nonnull - protected Map> getChildren() { - return this.children; + protected Map> getBranches() { + return this.branches; } @Override public MultiTrie.Node getView() { return this.view; } + + private record Factory( + Supplier> leavesFactory, + Supplier>> branchesFactory + ) { + CompositeStringMultiTrie.Branch create( + char key, MutableMapNode> parent + ) { + return new CompositeStringMultiTrie.Branch<>( + parent, key, this.leavesFactory.get(), this.branchesFactory.get(), + new CompositeStringMultiTrie.Branch.Factory<>(this.leavesFactory, this.branchesFactory) + ); + } + } } - private class View extends StringMultiTrie.View> { + private class View extends StringMultiTrie.View { @Override - protected StringMultiTrie> getViewed() { + protected StringMultiTrie getViewed() { return CompositeStringMultiTrie.this; } } diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MapNode.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MapNode.java index 4903eb380..1e4a63c87 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MapNode.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MapNode.java @@ -2,7 +2,6 @@ import org.checkerframework.dataflow.qual.Pure; -import javax.annotation.Nonnull; import java.util.Map; import java.util.stream.Stream; @@ -15,16 +14,15 @@ */ public abstract class MapNode> implements MultiTrie.Node { @Override - public Stream streamBranches() { - return this.getChildren().values().stream().flatMap(MapNode::streamValues); + public Stream streamStems() { + return this.getBranches().values().stream().flatMap(MapNode::streamValues); } @Override public Stream streamValues() { - return Stream.concat(this.streamLeaves(), this.streamBranches()); + return Stream.concat(this.streamLeaves(), this.streamStems()); } - @Nonnull @Pure - protected abstract Map getChildren(); + protected abstract Map getBranches(); } diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MultiTrie.java index 58bc8b4ad..eb4249b19 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MultiTrie.java @@ -1,6 +1,5 @@ package org.quiltmc.enigma.util.multi_trie; -import javax.annotation.Nonnull; import java.util.stream.Stream; /** @@ -17,7 +16,6 @@ * @param the type of values */ public interface MultiTrie { - @Nonnull Node getRoot(); default long getSize() { @@ -46,7 +44,7 @@ interface Node { * i.e. the prefix this node is associated with is not * the whole sequence the values are associated with */ - Stream streamBranches(); + Stream streamStems(); /** * @return a {@link Stream} containing all values associated with the prefix this node is associated with @@ -61,7 +59,6 @@ default boolean isEmpty() { return this.getSize() == 0; } - @Nonnull Node next(K key); } } diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMapNode.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMapNode.java index 4bf74deb0..6c2726c2a 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMapNode.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMapNode.java @@ -4,10 +4,9 @@ import org.checkerframework.dataflow.qual.Pure; import org.quiltmc.enigma.util.Utils; -import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.util.Collection; import java.util.Map; -import java.util.Optional; import java.util.stream.Stream; /** @@ -17,20 +16,20 @@ * * @param the type of keys * @param the type of values - * @param the type of this node + * @param the type of branch nodes */ -public abstract class MutableMapNode> - extends MapNode +public abstract class MutableMapNode> + extends MapNode implements MutableMultiTrie.Node { /** * Orphans are empty nodes. * - *

They may be moved to {@link #getChildren()} when they become non-empty. + *

They may be moved to {@link #getBranches()} when they become non-empty. * * @implNote Using a map with weak value references prevents memory leaks when users look up a sequence with no * values and don't put any value in it. */ - final Map orphans = new MapMaker().weakValues().makeMap(); + final Map orphans = new MapMaker().weakValues().makeMap(); @Override public Stream streamLeaves() { @@ -38,82 +37,115 @@ public Stream streamLeaves() { } @Override - @Nonnull - public N next(K key) { - final N next = this.nextImpl(Utils.requireNonNull(key, "key")); - return next == null ? this.orphans.computeIfAbsent(key, ignored -> this.createChild(key)) : next; + public B next(K key) { + final B next = this.nextImpl(Utils.requireNonNull(key, "key")); + return next == null ? this.orphans.computeIfAbsent(key, ignored -> this.createBranch(key)) : next; } - protected N nextImpl(K key) { - return this.getChildren().get(key); + @Nullable + protected B nextImpl(K key) { + return this.getBranches().get(key); } @Override public void put(V value) { this.getLeaves().add(value); - this.getParentAccess().ifPresent(access -> { - final boolean wasOrphan = access.parent.orphans.remove(access.key) != null; - if (wasOrphan) { - access.parent.getChildren().put(access.key, this.getSelf()); - } - }); } @Override - public boolean remove(V value) { - if (this.getLeaves().remove(value)) { - this.pruneIfEmpty(); - - return true; - } else { - return false; - } + public boolean removeLeaf(V value) { + return this.getLeaves().remove(value); } @Override - public boolean removeAll() { + public boolean clearLeaves() { final boolean hasLeaves = !this.getLeaves().isEmpty(); if (hasLeaves) { this.getLeaves().clear(); - this.pruneIfEmpty(); - return true; } else { return false; } } - protected void pruneIfEmpty() { - this.getParentAccess().ifPresent(access -> { - if (this.isEmpty()) { - access.parent.getChildren().remove(access.key); - access.parent.pruneIfEmpty(); - } - }); + protected boolean pruneIfEmpty(K key) { + if (this.getBranches().get(key).isEmpty()) { + this.getBranches().remove(key); + + return true; + } else { + return false; + } } /** - * @return this node + * @return a new, empty branch node instance */ - @Nonnull @Pure - protected abstract N getSelf(); + protected abstract B createBranch(K key); @Pure - protected abstract Optional> getParentAccess(); + protected abstract Collection getLeaves(); /** - * @return a new, empty child node instance + * + * @param the type of keys + * @param the type of values + * @param the type of this node */ - @Nonnull - @Pure - protected final N createChild(K key) { - return this.createChildImpl(new ParentAccess<>(this.getSelf(), key)); - } + protected abstract static class Branch> extends MutableMapNode { + @Pure + protected abstract MutableMapNode getParent(); + + @Pure + protected abstract K getKey(); + + /** + * @return this branch + */ + @Pure + protected abstract B getSelf(); + + @Override + public void put(V value) { + super.put(value); + final boolean wasOrphan = this.getParent().orphans.remove(this.getKey()) != null; + if (wasOrphan) { + this.getParent().getBranches().put(this.getKey(), this.getSelf()); + } + } - protected abstract N createChildImpl(ParentAccess parentAccess); + @Override + public boolean removeLeaf(V value) { + if (this.getLeaves().remove(value)) { + this.getParent().pruneIfEmpty(this.getKey()); - protected abstract Collection getLeaves(); + return true; + } else { + return false; + } + } - protected record ParentAccess(N parent, K key) { } + @Override + public boolean clearLeaves() { + if (super.clearLeaves()) { + this.getParent().pruneIfEmpty(this.getKey()); + + return true; + } else { + return false; + } + } + + @Override + protected boolean pruneIfEmpty(K key) { + if (super.pruneIfEmpty(key)) { + this.getParent().pruneIfEmpty(this.getKey()); + + return true; + } else { + return false; + } + } + } } diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMultiTrie.java index da99fa273..f51bd697f 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMultiTrie.java @@ -1,19 +1,14 @@ package org.quiltmc.enigma.util.multi_trie; -import org.quiltmc.enigma.util.multi_trie.MutableMultiTrie.Node; - -import javax.annotation.Nonnull; - /** * A multi-trie that allows modification which can also provide unmodifiable views of its contents. * * @param the type of keys * @param the type of values */ -public interface MutableMultiTrie> extends MultiTrie { - @Nonnull +public interface MutableMultiTrie extends MultiTrie { @Override - N getRoot(); + MutableMultiTrie.Node getRoot(); /** * @return a live, unmodifiable view of this trie @@ -28,7 +23,6 @@ public interface MutableMultiTrie> extends MultiTrie< */ interface Node extends MultiTrie.Node { @Override - @Nonnull Node next(K key); /** @@ -41,14 +35,14 @@ interface Node extends MultiTrie.Node { * * @return {@code true} if a value was removed, or {@code false} otherwise */ - boolean remove(V value); + boolean removeLeaf(V value); /** * Removes all leaves from this node. * * @return {@code true} if any values were removed, or {@code false} otherwise */ - boolean removeAll(); + boolean clearLeaves(); /** * @return a live, unmodifiable view of this node diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/NodeView.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/NodeView.java index ac7a5b9aa..2d898e043 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/NodeView.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/NodeView.java @@ -16,8 +16,8 @@ public Stream streamLeaves() { } @Override - public Stream streamBranches() { - return this.viewed.streamBranches(); + public Stream streamStems() { + return this.viewed.streamStems(); } @Override diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java index 36d86465f..102db5cc0 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java @@ -2,8 +2,6 @@ import org.quiltmc.enigma.util.Utils; -import javax.annotation.Nonnull; - /** * A {@link MutableMultiTrie} that associates sequences of characters with values of type {@code V}. * @@ -19,23 +17,24 @@ *

{@linkplain #getView() Views} also provide a {@link #get(String)} method. * * @param the type of values - * @param the type of nodes + * @param the type of branch nodes */ -public abstract class StringMultiTrie> - implements MutableMultiTrie { +public abstract class StringMultiTrie> + implements MutableMultiTrie { private static final String PREFIX = "prefix"; private static final String STRING = "string"; private static final String VALUE = "value"; @Override - @Nonnull - public abstract View getView(); + public abstract MutableMapNode getRoot(); + + @Override + public abstract View getView(); - @Nonnull - public N get(String prefix) { + public MutableMapNode get(String prefix) { Utils.requireNonNull(prefix, PREFIX); - N node = this.getRoot(); + MutableMapNode node = this.getRoot(); for (int i = 0; i < prefix.length(); i++) { node = node.next(prefix.charAt(i)); } @@ -43,16 +42,15 @@ public N get(String prefix) { return node; } - @Nonnull - public N put(String string, V value) { + public MutableMapNode put(String string, V value) { Utils.requireNonNull(string, STRING); Utils.requireNonNull(value, VALUE); - N node = this.getRoot(); + MutableMapNode node = this.getRoot(); for (int i = 0; i < string.length(); i++) { - final N parent = node; + final MutableMapNode parent = node; final char key = string.charAt(i); - node = node.getChildren().computeIfAbsent(key, ignored -> parent.createChild(key)); + node = node.getBranches().computeIfAbsent(key, ignored -> parent.createBranch(key)); } node.put(value); @@ -64,7 +62,7 @@ public boolean remove(String string, V value) { Utils.requireNonNull(string, STRING); Utils.requireNonNull(value, VALUE); - N node = this.getRoot(); + MutableMapNode node = this.getRoot(); for (int i = 0; i < string.length(); i++) { node = node.nextImpl(string.charAt(i)); @@ -73,13 +71,13 @@ public boolean remove(String string, V value) { } } - return node.remove(value); + return node.removeLeaf(value); } public boolean removeAll(String string) { Utils.requireNonNull(string, STRING); - N node = this.getRoot(); + MutableMapNode node = this.getRoot(); for (int i = 0; i < string.length(); i++) { node = node.nextImpl(string.charAt(i)); @@ -88,11 +86,10 @@ public boolean removeAll(String string) { } } - return node.removeAll(); + return node.clearLeaves(); } - public abstract static class View> implements MultiTrie { - @Nonnull + public abstract static class View implements MultiTrie { @Override public Node getRoot() { return this.getViewed().getRoot().getView(); @@ -102,6 +99,6 @@ public MultiTrie.Node get(String prefix) { return this.getViewed().get(prefix).getView(); } - protected abstract StringMultiTrie getViewed(); + protected abstract StringMultiTrie getViewed(); } } diff --git a/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java b/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java index ce562018d..92d6fc5de 100644 --- a/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java +++ b/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java @@ -39,7 +39,7 @@ void testPut() { assertUnorderedContentsForPrefix( prefix, BRANCHES, associations.stream().filter(association -> association.isBranchOf(prefix)), - node.streamBranches() + node.streamStems() ); }); } @@ -66,7 +66,7 @@ void testPutMulti() { assertUnorderedContentsForPrefix( prefix, BRANCHES, MultiAssociation.streamWith(associations.stream().filter(a -> a.isBranchOf(prefix))), - node.streamBranches() + node.streamStems() ); }); } @@ -139,7 +139,7 @@ private static void assertEmpty(CompositeStringMultiTrie trie) { () ->"Expected trie to be empty, but had it contents: " + trie.getRoot().streamValues().toList() ); - final Map> rootChildren = trie.getRoot().getChildren(); + final Map> rootChildren = trie.getRoot().getBranches(); assertTrue( rootChildren.isEmpty(), () -> "Expected root's children to be pruned, but it had children: " + rootChildren From 091ab34f9c0b19abf9fe40d674e2ddd2b3b608d9 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Thu, 13 Nov 2025 16:13:28 -0800 Subject: [PATCH 032/124] improve javadoc minor improvements --- .../enigma/util/multi_trie/MapNode.java | 8 +-- .../enigma/util/multi_trie/MultiTrie.java | 28 +++++++++-- .../util/multi_trie/MutableMapNode.java | 49 +++++++++++++------ .../util/multi_trie/StringMultiTrie.java | 4 +- 4 files changed, 63 insertions(+), 26 deletions(-) diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MapNode.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MapNode.java index 1e4a63c87..d8e6e5cbd 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MapNode.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MapNode.java @@ -6,13 +6,13 @@ import java.util.stream.Stream; /** - * A {@link MultiTrie.Node} that stores child nodes in a {@link Map}. + * A {@link MultiTrie.Node} that stores branch nodes in a {@link Map}. * * @param the type of keys * @param the type of values - * @param the type of this node + * @param the type of branch nodes */ -public abstract class MapNode> implements MultiTrie.Node { +public abstract class MapNode> implements MultiTrie.Node { @Override public Stream streamStems() { return this.getBranches().values().stream().flatMap(MapNode::streamValues); @@ -24,5 +24,5 @@ public Stream streamValues() { } @Pure - protected abstract Map getBranches(); + protected abstract Map getBranches(); } diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MultiTrie.java index eb4249b19..42dde99db 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MultiTrie.java @@ -5,23 +5,31 @@ /** * A multi-trie (or prefix tree) associates a sequence of keys with one or more values. * - *

Values can be looked up by a prefix of their key sequence; all values associated with a sequence beginning with - * the prefix will be returned.
+ *

Values can be looked up by a prefix of their key sequence; a {@link Node} holding all values associated with a + * sequence beginning with the prefix will be returned.
* The prefix is passed key-by-key to {@link Node#next} starting with {@link #getRoot}. * - * @implSpec {@code S} sequence types should represent an ordered sequence of keys of type {@code K}; - * sequences that represent the same sequence of keys should be equivalent - * * @param the type of keys * @param the type of values */ public interface MultiTrie { + /** + * The root is the node associated with the empty sequence. + * + *

Other nodes can be looked up via the root. + */ Node getRoot(); + /** + * @return the total number of values in this trie + */ default long getSize() { return this.getRoot().getSize(); } + /** + * @return {@code true} if this trie contains no values, or {@code false} otherwise + */ default boolean isEmpty() { return this.getSize() == 0; } @@ -51,14 +59,24 @@ interface Node { */ Stream streamValues(); + /** + * @return the total number of {@linkplain #streamValues() values} associated with this node's prefix + */ default long getSize() { return this.streamValues().count(); } + /** + * @return {@code true} if this node contains no {@linkplain #streamValues() values}, or {@code false} otherwise + */ default boolean isEmpty() { return this.getSize() == 0; } + /** + * @return the node associated with the sequence formed by appending the passed + * {@code key} to this node's sequence + */ Node next(K key); } } diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMapNode.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMapNode.java index 6c2726c2a..62ed28413 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMapNode.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMapNode.java @@ -10,9 +10,10 @@ import java.util.stream.Stream; /** - * A {@link MutableMultiTrie.Node} that stores child nodes in a {@link Map}. + * A {@link MutableMultiTrie.Node} that stores branch nodes in a {@link Map}. * - *

Nodes are aware of their keys so that they can manage their presence in maps. + *

Branch nodes are aware of their keys so that they can help their parents manage their presence in maps for + * trimming of empty nodes and adoption of orphan nodes. * * @param the type of keys * @param the type of values @@ -26,7 +27,9 @@ public abstract class MutableMapNode They may be moved to {@link #getBranches()} when they become non-empty. * - * @implNote Using a map with weak value references prevents memory leaks when users look up a sequence with no + * @implNote Keeping orphans in this map ensures there is only ever one node corresponding to a given sequence, + * avoiding any need to merge multiple nodes corresponding to the same sequence.
+ * Using a map with weak value references prevents memory leaks when users look up a sequence with no * values and don't put any value in it. */ final Map orphans = new MapMaker().weakValues().makeMap(); @@ -38,12 +41,12 @@ public Stream streamLeaves() { @Override public B next(K key) { - final B next = this.nextImpl(Utils.requireNonNull(key, "key")); - return next == null ? this.orphans.computeIfAbsent(key, ignored -> this.createBranch(key)) : next; + final B next = this.nextBranch(Utils.requireNonNull(key, "key")); + return next == null ? this.orphans.computeIfAbsent(key, this::createBranch) : next; } @Nullable - protected B nextImpl(K key) { + protected B nextBranch(K key) { return this.getBranches().get(key); } @@ -68,9 +71,16 @@ public boolean clearLeaves() { } } - protected boolean pruneIfEmpty(K key) { - if (this.getBranches().get(key).isEmpty()) { - this.getBranches().remove(key); + /** + * Removes the branch node associated with the passed {@code key} if that node is empty. + * + * @implNote This should only be passed one of this node's branches. + * + * @return {@code true} if the branch was pruned, or {@code false otherwise} + */ + protected boolean pruneIfEmpty(Branch branch) { + if (branch.isEmpty()) { + this.getBranches().remove(branch.getKey()); return true; } else { @@ -88,15 +98,24 @@ protected boolean pruneIfEmpty(K key) { protected abstract Collection getLeaves(); /** + * A non-root node. + * + *

Adds logic for managing its orphan status and propagating pruning upwards. * * @param the type of keys * @param the type of values - * @param the type of this node + * @param the type of this branch and of this branch's branches */ protected abstract static class Branch> extends MutableMapNode { + /** + * @return this branch's parent; may or may not be another branch node + */ @Pure protected abstract MutableMapNode getParent(); + /** + * @return the last key in this branch's sequence; the key this branch's parent stores it under + */ @Pure protected abstract K getKey(); @@ -118,7 +137,7 @@ public void put(V value) { @Override public boolean removeLeaf(V value) { if (this.getLeaves().remove(value)) { - this.getParent().pruneIfEmpty(this.getKey()); + this.getParent().pruneIfEmpty(this); return true; } else { @@ -129,7 +148,7 @@ public boolean removeLeaf(V value) { @Override public boolean clearLeaves() { if (super.clearLeaves()) { - this.getParent().pruneIfEmpty(this.getKey()); + this.getParent().pruneIfEmpty(this); return true; } else { @@ -138,9 +157,9 @@ public boolean clearLeaves() { } @Override - protected boolean pruneIfEmpty(K key) { - if (super.pruneIfEmpty(key)) { - this.getParent().pruneIfEmpty(this.getKey()); + protected boolean pruneIfEmpty(Branch branch) { + if (super.pruneIfEmpty(branch)) { + this.getParent().pruneIfEmpty(this); return true; } else { diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java index 102db5cc0..a81c4e799 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java @@ -64,7 +64,7 @@ public boolean remove(String string, V value) { MutableMapNode node = this.getRoot(); for (int i = 0; i < string.length(); i++) { - node = node.nextImpl(string.charAt(i)); + node = node.nextBranch(string.charAt(i)); if (node == null) { return false; @@ -79,7 +79,7 @@ public boolean removeAll(String string) { MutableMapNode node = this.getRoot(); for (int i = 0; i < string.length(); i++) { - node = node.nextImpl(string.charAt(i)); + node = node.nextBranch(string.charAt(i)); if (node == null) { return false; From 39bbfacc289d26c7751ebf76f8ef1cba6319e817 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Thu, 13 Nov 2025 17:51:43 -0800 Subject: [PATCH 033/124] move NodeView -> MutableMutliTrie.Node.View --- .../multi_trie/CompositeStringMultiTrie.java | 4 +-- .../util/multi_trie/MutableMultiTrie.java | 32 ++++++++++++++++++ .../enigma/util/multi_trie/NodeView.java | 33 ------------------- 3 files changed, 34 insertions(+), 35 deletions(-) delete mode 100644 enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/NodeView.java diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java index ed7e9e82d..6d7a73b0a 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java @@ -54,7 +54,7 @@ private static final class Root extends MutableMapNode branchFactory; - private final NodeView view = new NodeView<>(this); + private final View view = new View<>(this); private Root( Map> branches, Collection leaves, @@ -95,7 +95,7 @@ public static final class Branch extends MutableMapNode.Branch branchFactory; - private final MultiTrie.Node view = new NodeView<>(this); + private final MultiTrie.Node view = new View<>(this); private Branch( MutableMapNode> parent, char key, diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMultiTrie.java index f51bd697f..ba2491771 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMultiTrie.java @@ -1,5 +1,8 @@ package org.quiltmc.enigma.util.multi_trie; +import javax.annotation.Nonnull; +import java.util.stream.Stream; + /** * A multi-trie that allows modification which can also provide unmodifiable views of its contents. * @@ -48,5 +51,34 @@ interface Node extends MultiTrie.Node { * @return a live, unmodifiable view of this node */ MultiTrie.Node getView(); + + class View implements MultiTrie.Node { + protected final Node viewed; + + protected View(Node viewed) { + this.viewed = viewed; + } + + @Override + public Stream streamLeaves() { + return this.viewed.streamLeaves(); + } + + @Override + public Stream streamStems() { + return this.viewed.streamStems(); + } + + @Override + public Stream streamValues() { + return this.viewed.streamValues(); + } + + @Nonnull + @Override + public MultiTrie.Node next(K key) { + return this.viewed.next(key).getView(); + } + } } } diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/NodeView.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/NodeView.java deleted file mode 100644 index 2d898e043..000000000 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/NodeView.java +++ /dev/null @@ -1,33 +0,0 @@ -package org.quiltmc.enigma.util.multi_trie; - -import javax.annotation.Nonnull; -import java.util.stream.Stream; - -public final class NodeView implements MultiTrie.Node { - private final MutableMultiTrie.Node viewed; - - public NodeView(MutableMultiTrie.Node viewed) { - this.viewed = viewed; - } - - @Override - public Stream streamLeaves() { - return this.viewed.streamLeaves(); - } - - @Override - public Stream streamStems() { - return this.viewed.streamStems(); - } - - @Override - public Stream streamValues() { - return this.viewed.streamValues(); - } - - @Nonnull - @Override - public MultiTrie.Node next(K key) { - return this.viewed.next(key).getView(); - } -} From 54c48772dc65c20f652f4e2ac7f384b1371ec57b Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Thu, 13 Nov 2025 18:39:35 -0800 Subject: [PATCH 034/124] add key-by-key tests fix adoption not propagating upward --- .../util/multi_trie/MutableMapNode.java | 38 +++++++++++-- .../CompositeStringMultiTrieTest.java | 55 ++++++++++++++++++- 2 files changed, 87 insertions(+), 6 deletions(-) diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMapNode.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMapNode.java index 62ed28413..82e6ca5d2 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMapNode.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMapNode.java @@ -74,7 +74,7 @@ public boolean clearLeaves() { /** * Removes the branch node associated with the passed {@code key} if that node is empty. * - * @implNote This should only be passed one of this node's branches. + * @implNote This should only be passed one of this node's {@linkplain #getBranches() branches}. * * @return {@code true} if the branch was pruned, or {@code false otherwise} */ @@ -88,6 +88,26 @@ protected boolean pruneIfEmpty(Branch branch) { } } + /** + * If the passed {@code branch} is an {@linkplain #orphans orphan}, + * removes it from {@linkplain #orphans} and puts it in {@linkplain #getBranches() branches}. + * + * @implNote only non-empty branches should be passed to this method; + * it's called when a node may have changed from empty to non-empty + * + * @return {@code true} if the passed {@code branch} was an orphan, or {@code false} otherwise + */ + protected boolean tryAdopt(Branch branch) { + final boolean wasOrphan = this.orphans.remove(branch.getKey()) != null; + if (wasOrphan) { + this.getBranches().put(branch.getKey(), branch.getSelf()); + + return true; + } else { + return false; + } + } + /** * @return a new, empty branch node instance */ @@ -128,10 +148,7 @@ protected abstract static class Branch> extends @Override public void put(V value) { super.put(value); - final boolean wasOrphan = this.getParent().orphans.remove(this.getKey()) != null; - if (wasOrphan) { - this.getParent().getBranches().put(this.getKey(), this.getSelf()); - } + this.getParent().tryAdopt(this); } @Override @@ -166,5 +183,16 @@ protected boolean pruneIfEmpty(Branch branch) { return false; } } + + @Override + protected boolean tryAdopt(Branch branch) { + if (super.tryAdopt(branch)) { + this.getParent().tryAdopt(this); + + return true; + } else { + return false; + } + } } } diff --git a/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java b/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java index 92d6fc5de..a34a1abe5 100644 --- a/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java +++ b/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java @@ -15,12 +15,65 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; -// TODO key-by-key access tests public class CompositeStringMultiTrieTest { private static final String VALUES = "values"; private static final String LEAVES = "leaves"; private static final String BRANCHES = "branches"; + private static final String KEY_BY_KEY_SUBJECT = "key-by-key subject"; + + // test key-by-key put's orphan logic + @Test + void testPutKeyByKeyRootDown() { + final CompositeStringMultiTrie trie = CompositeStringMultiTrie.createHashed(); + + for (int depth = 0; depth < KEY_BY_KEY_SUBJECT.length(); depth++) { + MutableMapNode node = trie.getRoot(); + for (int iKey = 0; iKey <= depth; iKey++) { + node = node.next(KEY_BY_KEY_SUBJECT.charAt(iKey)); + } + + node.put(depth); + + assertOneLeaf(node); + + assertTrieSize(trie, depth + 1); + } + } + + @Test + void testPutKeyByKeyStemUp() { + final CompositeStringMultiTrie trie = CompositeStringMultiTrie.createHashed(); + + for (int depth = KEY_BY_KEY_SUBJECT.length() - 1; depth >= 0; depth--) { + MutableMapNode node = trie.getRoot(); + for (int iKey = 0; iKey <= depth; iKey++) { + node = node.next(KEY_BY_KEY_SUBJECT.charAt(iKey)); + } + + node.put(depth); + + assertOneLeaf(node); + + assertTrieSize(trie, KEY_BY_KEY_SUBJECT.length() - depth); + } + } + + private static void assertOneLeaf(MutableMapNode node) { + assertEquals( + 1, node.streamLeaves().count(), + () -> "Expected node to have only one leaf, but had the following: " + node.streamLeaves().toList() + ); + } + + private static void assertTrieSize(CompositeStringMultiTrie trie, int expectedSize) { + assertEquals( + expectedSize, trie.getSize(), + () -> "Expected node to have %s values, but had the following: %s" + .formatted(expectedSize, trie.getRoot().streamValues().toList()) + ); + } + @Test void testPut() { final CompositeStringMultiTrie trie = Association.createAndPopulateTrie(); From 5504ebdee653ef2b7b81ce849caafc5b5f4add02 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Thu, 13 Nov 2025 19:17:29 -0800 Subject: [PATCH 035/124] javadoc CompositeStringMultiTrie --- .../multi_trie/CompositeStringMultiTrie.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java index 6d7a73b0a..2b2147148 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java @@ -6,14 +6,41 @@ import java.util.Map; import java.util.function.Supplier; +/** + * A {@link StringMultiTrie} that allows customization of nodes' backing data structures. + * + * @param the type of values + * + * @see #of(Supplier, Supplier) + * @see #createHashed() + */ public final class CompositeStringMultiTrie extends StringMultiTrie> { private final Root root; private final View view = new View(); + /** + * Creates a trie with nodes whose branches are held in {@link HashMap}s + * and whose leaves are held in {@link HashSet}s. + * + * @param the type of values stored in the created trie + * + * @see #of(Supplier, Supplier) + */ public static CompositeStringMultiTrie createHashed() { return of(HashMap::new, HashSet::new); } + /** + * Creates a trie with nodes whose branches are held in maps created by the passed {@code branchesFactory} + * and whose leaves are held in collections created by the passed {@code leavesFactory}. + * + * @param branchesFactory a pure method that creates a new, empty {@link Map} in which to hold branch nodes + * @param leavesFactory a pure method that create a new, empty {@link Collection} in which to hold leaf values + * + * @param the type of values stored in the created trie + * + * @see #createHashed() + */ public static CompositeStringMultiTrie of( Supplier>> branchesFactory, Supplier> leavesFactory From f9573881bb01da0d7a02f3c67f445edb95cb422c Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Thu, 13 Nov 2025 19:19:59 -0800 Subject: [PATCH 036/124] clarify tests --- .../enigma/util/multi_trie/CompositeStringMultiTrieTest.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java b/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java index a34a1abe5..43e61a34f 100644 --- a/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java +++ b/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java @@ -24,7 +24,7 @@ public class CompositeStringMultiTrieTest { // test key-by-key put's orphan logic @Test - void testPutKeyByKeyRootDown() { + void testPutKeyByKeyFromRoot() { final CompositeStringMultiTrie trie = CompositeStringMultiTrie.createHashed(); for (int depth = 0; depth < KEY_BY_KEY_SUBJECT.length(); depth++) { @@ -41,8 +41,9 @@ void testPutKeyByKeyRootDown() { } } + // tests that key-by-key put's orphan logic propagates from stems to the root @Test - void testPutKeyByKeyStemUp() { + void testPutKeyByKeyFromStems() { final CompositeStringMultiTrie trie = CompositeStringMultiTrie.createHashed(); for (int depth = KEY_BY_KEY_SUBJECT.length() - 1; depth >= 0; depth--) { From 945b3e9e1ecf577729e4ae140f88f657dfd70fb8 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Fri, 14 Nov 2025 07:26:48 -0800 Subject: [PATCH 037/124] use same Branch.Factory instance for all nodes, inline createRoot --- .../multi_trie/CompositeStringMultiTrie.java | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java index 2b2147148..5b36dfc9d 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java @@ -48,21 +48,14 @@ public static CompositeStringMultiTrie of( return new CompositeStringMultiTrie<>(branchesFactory, leavesFactory); } - private static Root createRoot( - Supplier>> branchesFactory, - Supplier> leavesFactory - ) { - return new Root<>( - branchesFactory.get(), leavesFactory.get(), - new Branch.Factory<>(leavesFactory, branchesFactory) - ); - } - private CompositeStringMultiTrie( Supplier>> childrenFactory, Supplier> leavesFactory ) { - this.root = createRoot(childrenFactory, leavesFactory); + this.root = new Root<>( + childrenFactory.get(), leavesFactory.get(), + new Branch.Factory<>(leavesFactory, childrenFactory) + ); } @Override @@ -181,7 +174,7 @@ CompositeStringMultiTrie.Branch create( ) { return new CompositeStringMultiTrie.Branch<>( parent, key, this.leavesFactory.get(), this.branchesFactory.get(), - new CompositeStringMultiTrie.Branch.Factory<>(this.leavesFactory, this.branchesFactory) + this ); } } From b7c48119b5c0352a0e2a0a942b6c99c075f0fb9a Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Fri, 14 Nov 2025 19:19:45 -0800 Subject: [PATCH 038/124] focus menus search field on parent selection make EnigmaMenu extend MenuElement add SearchableElement add AbstractSearchableEnigmaMenu and make all menus except SearchMenusMenu extend it --- .../gui/element/PlaceheldTextField.java | 33 +++++++++- .../enigma/gui/element/SearchableElement.java | 10 +++ .../element/menu_bar/AbstractEnigmaMenu.java | 4 +- .../AbstractSearchableEnigmaMenu.java | 23 +++++++ .../gui/element/menu_bar/CollabMenu.java | 2 +- .../gui/element/menu_bar/DecompilerMenu.java | 2 +- .../enigma/gui/element/menu_bar/DevMenu.java | 2 +- .../gui/element/menu_bar/EnigmaMenu.java | 10 +-- .../enigma/gui/element/menu_bar/HelpMenu.java | 2 +- .../enigma/gui/element/menu_bar/MenuBar.java | 5 ++ .../gui/element/menu_bar/SearchMenu.java | 2 +- .../gui/element/menu_bar/SearchMenusMenu.java | 63 +++++++++++++++++-- .../menu_bar/file/CrashHistoryMenu.java | 4 +- .../gui/element/menu_bar/file/FileMenu.java | 4 +- .../element/menu_bar/file/OpenRecentMenu.java | 4 +- .../menu_bar/file/SaveMappingsAsMenu.java | 4 +- .../menu_bar/view/EntryTooltipsMenu.java | 4 +- .../element/menu_bar/view/LanguagesMenu.java | 4 +- .../menu_bar/view/NotificationsMenu.java | 4 +- .../gui/element/menu_bar/view/ScaleMenu.java | 4 +- .../gui/element/menu_bar/view/StatsMenu.java | 4 +- .../gui/element/menu_bar/view/ThemesMenu.java | 4 +- .../gui/element/menu_bar/view/ViewMenu.java | 4 +- .../java/org/quiltmc/enigma/util/Utils.java | 12 ++++ 24 files changed, 173 insertions(+), 41 deletions(-) create mode 100644 enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/SearchableElement.java create mode 100644 enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/AbstractSearchableEnigmaMenu.java diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/PlaceheldTextField.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/PlaceheldTextField.java index bab20e378..d805dde85 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/PlaceheldTextField.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/PlaceheldTextField.java @@ -1,21 +1,28 @@ package org.quiltmc.enigma.gui.element; +import org.quiltmc.enigma.util.Utils; + import javax.annotation.Nullable; import javax.swing.JTextField; +import javax.swing.MenuElement; +import javax.swing.MenuSelectionManager; import javax.swing.text.Document; import java.awt.Color; +import java.awt.Component; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Insets; import java.awt.RenderingHints; import java.awt.Toolkit; +import java.awt.event.KeyEvent; +import java.awt.event.MouseEvent; import java.util.Map; /** * A text field that displays placeholder text when it's empty. */ -public class PlaceheldTextField extends JTextField { +public class PlaceheldTextField extends JTextField implements MenuElement { private static final String DESKTOP_FONT_HINTS_KEY = "awt.font.desktophints"; @Nullable @@ -98,7 +105,8 @@ protected void paintComponent(Graphics graphics) { } } - graphics.setColor(this.placeholderColor == null ? this.getForeground() : this.placeholderColor); + Utils.findFirstNonNull(this.placeholderColor, this.getDisabledTextColor(), this.getForeground()) + .ifPresent(graphics::setColor); graphics.setFont(this.getFont()); final Insets insets = this.getInsets(); @@ -116,9 +124,28 @@ public void setPlaceholder(@Nullable String placeholder) { /** * @param color the placeholder color for this field; if {@code null}, the - * {@linkplain #getForeground() foreground color} will be used + * {@linkplain #getDisabledTextColor() disabled color} will be used */ public void setPlaceholderColor(@Nullable Color color) { this.placeholderColor = color; } + + @Override + public void processMouseEvent(MouseEvent event, MenuElement[] path, MenuSelectionManager manager) { } + + @Override + public void processKeyEvent(KeyEvent event, MenuElement[] path, MenuSelectionManager manager) { } + + @Override + public void menuSelectionChanged(boolean isIncluded) { } + + @Override + public MenuElement[] getSubElements() { + return new MenuElement[0]; + } + + @Override + public Component getComponent() { + return this; + } } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/SearchableElement.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/SearchableElement.java new file mode 100644 index 000000000..9aed694a8 --- /dev/null +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/SearchableElement.java @@ -0,0 +1,10 @@ +package org.quiltmc.enigma.gui.element; + +import javax.swing.MenuElement; +import java.util.stream.Stream; + +public interface SearchableElement extends MenuElement { + Stream streamSearchAliases(); + + void onSearchClicked(); +} diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/AbstractEnigmaMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/AbstractEnigmaMenu.java index 472ff03d8..d9f6aff46 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/AbstractEnigmaMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/AbstractEnigmaMenu.java @@ -4,10 +4,10 @@ import javax.swing.JMenu; -public class AbstractEnigmaMenu extends JMenu implements EnigmaMenu { +public abstract class AbstractEnigmaMenu extends JMenu implements EnigmaMenu { protected final Gui gui; - protected AbstractEnigmaMenu(Gui gui) { + public AbstractEnigmaMenu(Gui gui) { this.gui = gui; } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/AbstractSearchableEnigmaMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/AbstractSearchableEnigmaMenu.java new file mode 100644 index 000000000..07772aed1 --- /dev/null +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/AbstractSearchableEnigmaMenu.java @@ -0,0 +1,23 @@ +package org.quiltmc.enigma.gui.element.menu_bar; + +import org.quiltmc.enigma.gui.Gui; +import org.quiltmc.enigma.gui.element.SearchableElement; + +import java.util.stream.Stream; + +public abstract class AbstractSearchableEnigmaMenu extends AbstractEnigmaMenu implements SearchableElement { + protected AbstractSearchableEnigmaMenu(Gui gui) { + super(gui); + } + + @Override + public Stream streamSearchAliases() { + return Stream.of(this.getText()); + } + + @Override + public void onSearchClicked() { + this.setSelected(true); + this.doClick(); + } +} diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/CollabMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/CollabMenu.java index 75efc4e6f..9e22ba29c 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/CollabMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/CollabMenu.java @@ -15,7 +15,7 @@ import java.io.IOException; import java.util.Arrays; -public class CollabMenu extends AbstractEnigmaMenu { +public class CollabMenu extends AbstractSearchableEnigmaMenu { private final JMenuItem connectItem = new JMenuItem(); private final JMenuItem startServerItem = new JMenuItem(); diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/DecompilerMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/DecompilerMenu.java index c75985875..42bef8a8a 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/DecompilerMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/DecompilerMenu.java @@ -10,7 +10,7 @@ import javax.swing.JMenuItem; import javax.swing.JRadioButtonMenuItem; -public class DecompilerMenu extends AbstractEnigmaMenu { +public class DecompilerMenu extends AbstractSearchableEnigmaMenu { private final JMenuItem decompilerSettingsItem = new JMenuItem(); public DecompilerMenu(Gui gui) { diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/DevMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/DevMenu.java index 9794fd735..722f956ac 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/DevMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/DevMenu.java @@ -26,7 +26,7 @@ import java.io.StringWriter; import java.nio.file.Files; -public class DevMenu extends AbstractEnigmaMenu { +public class DevMenu extends AbstractSearchableEnigmaMenu { private final JCheckBoxMenuItem showMappingSourcePluginItem = new JCheckBoxMenuItem(); private final JCheckBoxMenuItem debugTokenHighlightsItem = new JCheckBoxMenuItem(); private final JCheckBoxMenuItem logClientPacketsItem = new JCheckBoxMenuItem(); diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/EnigmaMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/EnigmaMenu.java index 3e1231e76..8c0adc649 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/EnigmaMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/EnigmaMenu.java @@ -2,10 +2,12 @@ import org.quiltmc.enigma.gui.ConnectionState; -public interface EnigmaMenu { - default void setKeyBinds() {} +import javax.swing.MenuElement; - default void updateState(boolean jarOpen, ConnectionState state) {} +public interface EnigmaMenu extends MenuElement { + default void setKeyBinds() { } - default void retranslate() {} + default void updateState(boolean jarOpen, ConnectionState state) { } + + default void retranslate() { } } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/HelpMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/HelpMenu.java index 6ea4edd41..77611d1be 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/HelpMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/HelpMenu.java @@ -7,7 +7,7 @@ import javax.swing.JMenuItem; -public class HelpMenu extends AbstractEnigmaMenu { +public class HelpMenu extends AbstractSearchableEnigmaMenu { private final JMenuItem aboutItem = new JMenuItem(); private final JMenuItem githubItem = new JMenuItem(); private final SearchMenusMenu searchItem; diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/MenuBar.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/MenuBar.java index 934d78a55..158c911bc 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/MenuBar.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/MenuBar.java @@ -8,6 +8,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.stream.Stream; public class MenuBar { private final List menus = new ArrayList<>(); @@ -77,4 +78,8 @@ public CollabMenu getCollabMenu() { public FileMenu getFileMenu() { return this.fileMenu; } + + public Stream streamMenus() { + return this.menus.stream(); + } } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenu.java index 436cb0027..66e428499 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenu.java @@ -8,7 +8,7 @@ import javax.swing.JMenuItem; -public class SearchMenu extends AbstractEnigmaMenu { +public class SearchMenu extends AbstractSearchableEnigmaMenu { private final JMenuItem searchItem = new JMenuItem(GuiUtil.DEOBFUSCATED_ICON); private final JMenuItem searchAllItem = new JMenuItem(GuiUtil.DEOBFUSCATED_ICON); private final JMenuItem searchClassItem = new JMenuItem(GuiUtil.CLASS_ICON); diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java index 679f0a92e..d1d0c1960 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java @@ -3,13 +3,31 @@ import org.quiltmc.enigma.gui.ConnectionState; import org.quiltmc.enigma.gui.Gui; import org.quiltmc.enigma.gui.element.PlaceheldTextField; +import org.quiltmc.enigma.gui.element.SearchableElement; import org.quiltmc.enigma.util.I18n; +import org.quiltmc.enigma.util.multi_trie.CompositeStringMultiTrie; +import org.quiltmc.enigma.util.multi_trie.StringMultiTrie; import javax.swing.JPanel; +import javax.swing.MenuElement; +import java.util.Arrays; +import java.util.stream.Stream; public class SearchMenusMenu extends AbstractEnigmaMenu { - final PlaceheldTextField field = new PlaceheldTextField(); - final JPanel results = new JPanel(); + /** + * @return a breadth-first stream of the passed {@code root} element and all of its sub-elements + */ + private static Stream streamElementTree(MenuElement root) { + return Stream.concat( + Stream.of(root), + Arrays.stream(root.getSubElements()).flatMap(SearchMenusMenu::streamElementTree) + ); + } + + private final PlaceheldTextField field = new PlaceheldTextField(); + private final JPanel results = new JPanel(); + + private StringMultiTrie.View elements; protected SearchMenusMenu(Gui gui) { super(gui); @@ -17,21 +35,56 @@ protected SearchMenusMenu(Gui gui) { this.add(this.field); this.add(this.results); - // TODO focus field on open + SearchMenusMenu.this.field.addHierarchyListener(e -> { + if (SearchMenusMenu.this.field.isShowing()) { + SearchMenusMenu.this.field.requestFocus(); + SearchMenusMenu.this.field.selectAll(); + } + }); // TODO KeyBinds: up/down -> prev/next result, enter -> doClick on selected result this.retranslate(); } + private StringMultiTrie.View getElements() { + if (this.elements == null) { + this.elements = this.buildElementsTrie(); + } + + return this.elements; + } + + private void clearElements() { + this.elements = null; + } + + private StringMultiTrie.View buildElementsTrie() { + final CompositeStringMultiTrie elementsBuilder = CompositeStringMultiTrie.createHashed(); + this.gui.getMenuBar() + .streamMenus() + .flatMap(SearchMenusMenu::streamElementTree) + .mapMulti((element, keep) -> { + if (element instanceof SearchableElement searchable) { + keep.accept(searchable); + } + }) + .forEach(searchable -> searchable + .streamSearchAliases() + .forEach(alias -> elementsBuilder.put(alias, searchable)) + ); + + return elementsBuilder.getView(); + } + @Override public void updateState(boolean jarOpen, ConnectionState state) { - // TODO check any caching + this.clearElements(); } @Override public void retranslate() { - // TODO check any caching + this.clearElements(); this.setText(I18n.translate("menu.help.search")); this.field.setPlaceholder(I18n.translate("menu.help.search.placeholder")); } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/file/CrashHistoryMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/file/CrashHistoryMenu.java index 8d41dd61d..47a9507cd 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/file/CrashHistoryMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/file/CrashHistoryMenu.java @@ -3,13 +3,13 @@ import org.quiltmc.enigma.gui.ConnectionState; import org.quiltmc.enigma.gui.Gui; import org.quiltmc.enigma.gui.dialog.CrashDialog; -import org.quiltmc.enigma.gui.element.menu_bar.AbstractEnigmaMenu; +import org.quiltmc.enigma.gui.element.menu_bar.AbstractSearchableEnigmaMenu; import org.quiltmc.enigma.util.I18n; import javax.swing.ButtonGroup; import javax.swing.JMenuItem; -public class CrashHistoryMenu extends AbstractEnigmaMenu { +public class CrashHistoryMenu extends AbstractSearchableEnigmaMenu { protected CrashHistoryMenu(Gui gui) { super(gui); } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/file/FileMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/file/FileMenu.java index f0d092bac..e34c8b4b9 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/file/FileMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/file/FileMenu.java @@ -7,7 +7,7 @@ import org.quiltmc.enigma.gui.config.keybind.KeyBinds; import org.quiltmc.enigma.gui.dialog.StatsDialog; import org.quiltmc.enigma.gui.dialog.keybind.ConfigureKeyBindsDialog; -import org.quiltmc.enigma.gui.element.menu_bar.AbstractEnigmaMenu; +import org.quiltmc.enigma.gui.element.menu_bar.AbstractSearchableEnigmaMenu; import org.quiltmc.enigma.gui.util.ExtensionFileFilter; import org.quiltmc.enigma.util.I18n; @@ -21,7 +21,7 @@ import java.util.List; import java.util.Optional; -public class FileMenu extends AbstractEnigmaMenu { +public class FileMenu extends AbstractSearchableEnigmaMenu { private final SaveMappingsAsMenu saveMappingsAs; private final CrashHistoryMenu crashHistory; private final OpenRecentMenu openRecent; diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/file/OpenRecentMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/file/OpenRecentMenu.java index ad6142441..d8cd7ead1 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/file/OpenRecentMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/file/OpenRecentMenu.java @@ -4,7 +4,7 @@ import org.quiltmc.enigma.gui.ConnectionState; import org.quiltmc.enigma.gui.Gui; import org.quiltmc.enigma.gui.config.Config; -import org.quiltmc.enigma.gui.element.menu_bar.AbstractEnigmaMenu; +import org.quiltmc.enigma.gui.element.menu_bar.AbstractSearchableEnigmaMenu; import org.quiltmc.enigma.util.I18n; import javax.swing.JMenuItem; @@ -12,7 +12,7 @@ import java.nio.file.Path; import java.util.List; -public class OpenRecentMenu extends AbstractEnigmaMenu { +public class OpenRecentMenu extends AbstractSearchableEnigmaMenu { protected OpenRecentMenu(Gui gui) { super(gui); } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/file/SaveMappingsAsMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/file/SaveMappingsAsMenu.java index d70c0c044..361ca78f8 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/file/SaveMappingsAsMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/file/SaveMappingsAsMenu.java @@ -4,7 +4,7 @@ import org.quiltmc.enigma.gui.ConnectionState; import org.quiltmc.enigma.gui.Gui; import org.quiltmc.enigma.gui.config.Config; -import org.quiltmc.enigma.gui.element.menu_bar.AbstractEnigmaMenu; +import org.quiltmc.enigma.gui.element.menu_bar.AbstractSearchableEnigmaMenu; import org.quiltmc.enigma.gui.util.ExtensionFileFilter; import org.quiltmc.enigma.util.I18n; @@ -16,7 +16,7 @@ import java.util.Map; import java.util.function.Consumer; -public class SaveMappingsAsMenu extends AbstractEnigmaMenu { +public class SaveMappingsAsMenu extends AbstractSearchableEnigmaMenu { private final Map items = new HashMap<>(); protected SaveMappingsAsMenu(Gui gui) { diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/EntryTooltipsMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/EntryTooltipsMenu.java index 1598cef8c..20110e69a 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/EntryTooltipsMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/EntryTooltipsMenu.java @@ -2,14 +2,14 @@ import org.quiltmc.enigma.gui.Gui; import org.quiltmc.enigma.gui.config.Config; -import org.quiltmc.enigma.gui.element.menu_bar.AbstractEnigmaMenu; +import org.quiltmc.enigma.gui.element.menu_bar.AbstractSearchableEnigmaMenu; import org.quiltmc.enigma.util.I18n; import javax.swing.JCheckBoxMenuItem; import static org.quiltmc.enigma.gui.util.GuiUtil.createSyncedMenuCheckBox; -public class EntryTooltipsMenu extends AbstractEnigmaMenu { +public class EntryTooltipsMenu extends AbstractSearchableEnigmaMenu { private final JCheckBoxMenuItem enable = createSyncedMenuCheckBox(Config.editor().entryTooltips.enable); private final JCheckBoxMenuItem interactable = createSyncedMenuCheckBox(Config.editor().entryTooltips.interactable); diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/LanguagesMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/LanguagesMenu.java index 235b51e42..2c206436a 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/LanguagesMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/LanguagesMenu.java @@ -3,7 +3,7 @@ import org.quiltmc.enigma.gui.ConnectionState; import org.quiltmc.enigma.gui.Gui; import org.quiltmc.enigma.gui.config.Config; -import org.quiltmc.enigma.gui.element.menu_bar.AbstractEnigmaMenu; +import org.quiltmc.enigma.gui.element.menu_bar.AbstractSearchableEnigmaMenu; import org.quiltmc.enigma.gui.util.LanguageUtil; import org.quiltmc.enigma.util.I18n; @@ -12,7 +12,7 @@ import java.util.HashMap; import java.util.Map; -public class LanguagesMenu extends AbstractEnigmaMenu { +public class LanguagesMenu extends AbstractSearchableEnigmaMenu { private final Map languages = new HashMap<>(); protected LanguagesMenu(Gui gui) { diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/NotificationsMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/NotificationsMenu.java index 5247bade4..515b6fc32 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/NotificationsMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/NotificationsMenu.java @@ -3,7 +3,7 @@ import org.quiltmc.enigma.gui.ConnectionState; import org.quiltmc.enigma.gui.Gui; import org.quiltmc.enigma.gui.config.Config; -import org.quiltmc.enigma.gui.element.menu_bar.AbstractEnigmaMenu; +import org.quiltmc.enigma.gui.element.menu_bar.AbstractSearchableEnigmaMenu; import org.quiltmc.enigma.util.I18n; import javax.swing.ButtonGroup; @@ -13,7 +13,7 @@ import static org.quiltmc.enigma.gui.NotificationManager.ServerNotificationLevel; -public class NotificationsMenu extends AbstractEnigmaMenu { +public class NotificationsMenu extends AbstractSearchableEnigmaMenu { private final Map buttons = new HashMap<>(); public NotificationsMenu(Gui gui) { diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/ScaleMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/ScaleMenu.java index 983331d4e..4c3368996 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/ScaleMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/ScaleMenu.java @@ -4,7 +4,7 @@ import org.quiltmc.enigma.gui.Gui; import org.quiltmc.enigma.gui.config.Config; import org.quiltmc.enigma.gui.dialog.ChangeDialog; -import org.quiltmc.enigma.gui.element.menu_bar.AbstractEnigmaMenu; +import org.quiltmc.enigma.gui.element.menu_bar.AbstractSearchableEnigmaMenu; import org.quiltmc.enigma.gui.util.ScaleUtil; import org.quiltmc.enigma.util.I18n; @@ -17,7 +17,7 @@ import java.util.function.BiConsumer; import java.util.stream.IntStream; -public class ScaleMenu extends AbstractEnigmaMenu { +public class ScaleMenu extends AbstractSearchableEnigmaMenu { private final int[] defaultOptions = {100, 125, 150, 175, 200}; private final ButtonGroup optionsGroup = new ButtonGroup(); private final Map options = new HashMap<>(); diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/StatsMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/StatsMenu.java index 9390bead8..8bdab604f 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/StatsMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/StatsMenu.java @@ -4,7 +4,7 @@ import org.quiltmc.enigma.gui.ConnectionState; import org.quiltmc.enigma.gui.Gui; import org.quiltmc.enigma.gui.config.Config; -import org.quiltmc.enigma.gui.element.menu_bar.AbstractEnigmaMenu; +import org.quiltmc.enigma.gui.element.menu_bar.AbstractSearchableEnigmaMenu; import org.quiltmc.enigma.util.I18n; import javax.swing.JCheckBoxMenuItem; @@ -15,7 +15,7 @@ import static java.util.concurrent.CompletableFuture.runAsync; -public class StatsMenu extends AbstractEnigmaMenu { +public class StatsMenu extends AbstractSearchableEnigmaMenu { private final JCheckBoxMenuItem enableIcons = new JCheckBoxMenuItem(); private final JCheckBoxMenuItem includeSynthetic = new JCheckBoxMenuItem(); private final JCheckBoxMenuItem countFallback = new JCheckBoxMenuItem(); diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/ThemesMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/ThemesMenu.java index 225092c4b..a9474a2f8 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/ThemesMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/ThemesMenu.java @@ -4,7 +4,7 @@ import org.quiltmc.enigma.gui.Gui; import org.quiltmc.enigma.gui.config.Config; import org.quiltmc.enigma.gui.dialog.ChangeDialog; -import org.quiltmc.enigma.gui.element.menu_bar.AbstractEnigmaMenu; +import org.quiltmc.enigma.gui.element.menu_bar.AbstractSearchableEnigmaMenu; import org.quiltmc.enigma.util.I18n; import javax.swing.ButtonGroup; @@ -15,7 +15,7 @@ import static org.quiltmc.enigma.gui.config.Config.ThemeChoice; -public class ThemesMenu extends AbstractEnigmaMenu { +public class ThemesMenu extends AbstractSearchableEnigmaMenu { private final Map themes = new HashMap<>(); protected ThemesMenu(Gui gui) { diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/ViewMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/ViewMenu.java index a9471b18c..6431e4868 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/ViewMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/ViewMenu.java @@ -3,12 +3,12 @@ import org.quiltmc.enigma.gui.ConnectionState; import org.quiltmc.enigma.gui.Gui; import org.quiltmc.enigma.gui.dialog.FontDialog; -import org.quiltmc.enigma.gui.element.menu_bar.AbstractEnigmaMenu; +import org.quiltmc.enigma.gui.element.menu_bar.AbstractSearchableEnigmaMenu; import org.quiltmc.enigma.util.I18n; import javax.swing.JMenuItem; -public class ViewMenu extends AbstractEnigmaMenu { +public class ViewMenu extends AbstractSearchableEnigmaMenu { private final StatsMenu stats; private final NotificationsMenu notifications; private final LanguagesMenu languages; diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/Utils.java b/enigma/src/main/java/org/quiltmc/enigma/util/Utils.java index a44006400..fd9404543 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/Utils.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/Utils.java @@ -16,6 +16,7 @@ import java.util.Comparator; import java.util.List; import java.util.Locale; +import java.util.Optional; import java.util.Properties; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; @@ -204,4 +205,15 @@ public static T requireNonNull(T value, String name) { return value; } } + + @SafeVarargs + public static Optional findFirstNonNull(T... values) { + for (final T value : values) { + if (value != null) { + return Optional.of(value); + } + } + + return Optional.empty(); + } } From 233d8d8e83b6a0839e6b8d870cfc7ec1c005da39 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sat, 15 Nov 2025 09:54:12 -0800 Subject: [PATCH 039/124] intial (buggy) menu search implemntation --- .../gui/element/menu_bar/SearchMenusMenu.java | 235 +++++++++++++++--- 1 file changed, 195 insertions(+), 40 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java index d1d0c1960..0a55c1cb1 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java @@ -6,86 +6,241 @@ import org.quiltmc.enigma.gui.element.SearchableElement; import org.quiltmc.enigma.util.I18n; import org.quiltmc.enigma.util.multi_trie.CompositeStringMultiTrie; +import org.quiltmc.enigma.util.multi_trie.MultiTrie; import org.quiltmc.enigma.util.multi_trie.StringMultiTrie; -import javax.swing.JPanel; +import javax.annotation.Nullable; +import javax.swing.JMenuItem; import javax.swing.MenuElement; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; import java.util.Arrays; +import java.util.Set; +import java.util.stream.Collectors; import java.util.stream.Stream; public class SearchMenusMenu extends AbstractEnigmaMenu { /** - * @return a breadth-first stream of the passed {@code root} element and all of its sub-elements + * @return a breadth-first stream of the passed {@code root} element and all of its sub-elements, + * excluding {@link SearchMenusMenu}s and their sub-elements */ private static Stream streamElementTree(MenuElement root) { - return Stream.concat( + return root instanceof SearchMenusMenu ? Stream.empty() : Stream.concat( Stream.of(root), Arrays.stream(root.getSubElements()).flatMap(SearchMenusMenu::streamElementTree) ); } private final PlaceheldTextField field = new PlaceheldTextField(); - private final JPanel results = new JPanel(); + private final JMenuItem noResults = new JMenuItem(); - private StringMultiTrie.View elements; + private final ResultManager resultManager = new ResultManager(); protected SearchMenusMenu(Gui gui) { super(gui); + this.noResults.setEnabled(false); + this.noResults.setVisible(false); + this.add(this.field); - this.add(this.results); + this.add(this.noResults); - SearchMenusMenu.this.field.addHierarchyListener(e -> { - if (SearchMenusMenu.this.field.isShowing()) { - SearchMenusMenu.this.field.requestFocus(); - SearchMenusMenu.this.field.selectAll(); + this.field.addHierarchyListener(e -> { + if (this.field.isShowing()) { + this.field.requestFocus(); + this.field.selectAll(); } }); - // TODO KeyBinds: up/down -> prev/next result, enter -> doClick on selected result - - this.retranslate(); - } + this.field.getDocument().addDocumentListener(new DocumentListener() { + void updateResultItems() { + final String searchTerm = SearchMenusMenu.this.field.getText(); + + if (searchTerm.isEmpty()) { + SearchMenusMenu.this.noResults.setVisible(false); + SearchMenusMenu.this.invalidate(); + SearchMenusMenu.this.repaint(); + SearchMenusMenu.this.resultManager.clearCurrent(); + } else { + switch (SearchMenusMenu.this.resultManager.updateResultItems(searchTerm)) { + case NO_RESULTS -> { + SearchMenusMenu.this.noResults.setVisible(false); + + SearchMenusMenu.this.getPopupMenu().pack(); + SearchMenusMenu.this.getPopupMenu().pack(); + } + case SAME_RESULTS -> { } + case DIFFERENT_RESULTS -> { + SearchMenusMenu.this.noResults.setVisible(true); + + SearchMenusMenu.this.getPopupMenu().pack(); + SearchMenusMenu.this.getPopupMenu().pack(); + } + } + } + } - private StringMultiTrie.View getElements() { - if (this.elements == null) { - this.elements = this.buildElementsTrie(); - } + @Override + public void insertUpdate(DocumentEvent e) { + this.updateResultItems(); + } - return this.elements; - } + @Override + public void removeUpdate(DocumentEvent e) { + this.updateResultItems(); + } - private void clearElements() { - this.elements = null; - } + @Override + public void changedUpdate(DocumentEvent e) { + this.updateResultItems(); + } + }); - private StringMultiTrie.View buildElementsTrie() { - final CompositeStringMultiTrie elementsBuilder = CompositeStringMultiTrie.createHashed(); - this.gui.getMenuBar() - .streamMenus() - .flatMap(SearchMenusMenu::streamElementTree) - .mapMulti((element, keep) -> { - if (element instanceof SearchableElement searchable) { - keep.accept(searchable); - } - }) - .forEach(searchable -> searchable - .streamSearchAliases() - .forEach(alias -> elementsBuilder.put(alias, searchable)) - ); + // TODO KeyBinds: up/down -> prev/next result, enter -> doClick on selected result - return elementsBuilder.getView(); + this.retranslate(); } @Override public void updateState(boolean jarOpen, ConnectionState state) { - this.clearElements(); + this.resultManager.clear(); } @Override public void retranslate() { - this.clearElements(); + this.resultManager.clear(); + this.setText(I18n.translate("menu.help.search")); this.field.setPlaceholder(I18n.translate("menu.help.search.placeholder")); } + + private static class Result { + final SearchableElement element; + final String alias; + + @Nullable JMenuItem item; + + Result(SearchableElement element, String alias) { + this.element = element; + this.alias = alias; + } + + JMenuItem getItem() { + if (this.item == null) { + this.item = new JMenuItem(this.alias); + } + + return this.item; + } + } + + private class ResultManager { + @Nullable + StringMultiTrie.View resultTrie; + @Nullable + CurrentResults currentResults; + + /** + * @return {@code true} if there are any results, or {@code false} otherwise + */ + UpdateOutcome updateResultItems(String searchTerm) { + if (this.currentResults == null || !searchTerm.startsWith(this.currentResults.searchTerm)) { + return this.initializeCurrentResults(searchTerm); + } else { + if (this.currentResults.searchTerm.length() == searchTerm.length()) { + return UpdateOutcome.SAME_RESULTS; + } else { + MultiTrie.Node resultNode = this.currentResults.results; + for (int i = this.currentResults.searchTerm.length(); i < searchTerm.length(); i++) { + resultNode = resultNode.next(searchTerm.charAt(i)); + } + + if (resultNode.isEmpty()) { + this.clearCurrent(); + + return UpdateOutcome.NO_RESULTS; + } else { + final Set newResults = resultNode.streamValues().collect(Collectors.toSet()); + + final Set excludedResults = this.currentResults.results.streamValues() + .filter(oldResult -> !newResults.contains(oldResult)) + .map(Result::getItem) + .collect(Collectors.toSet()); + + if (excludedResults.isEmpty()) { + return UpdateOutcome.SAME_RESULTS; + } else { + excludedResults + .forEach(SearchMenusMenu.this::remove); + + this.currentResults = new CurrentResults(resultNode, searchTerm); + + return UpdateOutcome.DIFFERENT_RESULTS; + } + } + } + } + } + + UpdateOutcome initializeCurrentResults(String searchTerm) { + final MultiTrie.Node results = this.getResultTrie().get(searchTerm); + if (results.isEmpty()) { + this.clearCurrent(); + + return UpdateOutcome.NO_RESULTS; + } else { + this.currentResults = new CurrentResults(results, searchTerm); + this.currentResults.results.streamValues().map(Result::getItem).forEach(SearchMenusMenu.this::add); + + return UpdateOutcome.DIFFERENT_RESULTS; + } + } + + StringMultiTrie.View getResultTrie() { + if (this.resultTrie == null) { + this.resultTrie = this.buildResultTrie(); + } + + return this.resultTrie; + } + + void clear() { + this.resultTrie = null; + this.clearCurrent(); + } + + void clearCurrent() { + if (this.currentResults != null) { + this.currentResults.results.streamValues() + .map(Result::getItem) + .forEach(SearchMenusMenu.this::remove); + + this.currentResults = null; + } + } + + StringMultiTrie.View buildResultTrie() { + final CompositeStringMultiTrie elementsBuilder = CompositeStringMultiTrie.createHashed(); + SearchMenusMenu.this.gui.getMenuBar() + .streamMenus() + .flatMap(SearchMenusMenu::streamElementTree) + .mapMulti((element, keep) -> { + if (element instanceof SearchableElement searchable) { + keep.accept(searchable); + } + }) + .forEach(searchable -> searchable + .streamSearchAliases() + .forEach(alias -> elementsBuilder.put(alias, new Result(searchable, alias))) + ); + + return elementsBuilder.getView(); + } + + record CurrentResults(MultiTrie.Node results, String searchTerm) { } + + enum UpdateOutcome { + NO_RESULTS, SAME_RESULTS, DIFFERENT_RESULTS + } + } } From 89b0853d153cd3661a7e87427396ab3f2efc5a17 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sat, 15 Nov 2025 10:50:22 -0800 Subject: [PATCH 040/124] fix selecting field text on packing fix repainting instead of packing when search term is empty add translatable text to noResults fix inverting when noResults is shown --- .../gui/element/menu_bar/SearchMenusMenu.java | 46 ++++++++++++++++--- enigma/src/main/resources/lang/en_us.json | 1 + 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java index 0a55c1cb1..648ed79ca 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java @@ -14,6 +14,10 @@ import javax.swing.MenuElement; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; +import javax.swing.event.MenuEvent; +import javax.swing.event.MenuListener; +import java.awt.event.HierarchyEvent; +import java.awt.event.HierarchyListener; import java.util.Arrays; import java.util.Set; import java.util.stream.Collectors; @@ -45,10 +49,39 @@ protected SearchMenusMenu(Gui gui) { this.add(this.field); this.add(this.noResults); + // Always focus field, but don't always select its text, because it loses focus when packing new search results. this.field.addHierarchyListener(e -> { if (this.field.isShowing()) { this.field.requestFocus(); - this.field.selectAll(); + } + }); + + // Only select field text when the menu is selected, so text isn't selected when packing new search results. + this.addMenuListener(new MenuListener() { + final HierarchyListener fieldTextSelector = new HierarchyListener() { + @Override + public void hierarchyChanged(HierarchyEvent e) { + if (SearchMenusMenu.this.field.isShowing()) { + SearchMenusMenu.this.field.removeHierarchyListener(this); + + SearchMenusMenu.this.field.selectAll(); + } + } + }; + + @Override + public void menuSelected(MenuEvent e) { + SearchMenusMenu.this.field.addHierarchyListener(this.fieldTextSelector); + } + + @Override + public void menuDeselected(MenuEvent e) { + SearchMenusMenu.this.field.removeHierarchyListener(this.fieldTextSelector); + } + + @Override + public void menuCanceled(MenuEvent e) { + SearchMenusMenu.this.field.removeHierarchyListener(this.fieldTextSelector); } }); @@ -58,23 +91,21 @@ void updateResultItems() { if (searchTerm.isEmpty()) { SearchMenusMenu.this.noResults.setVisible(false); - SearchMenusMenu.this.invalidate(); - SearchMenusMenu.this.repaint(); SearchMenusMenu.this.resultManager.clearCurrent(); + + SearchMenusMenu.this.getPopupMenu().pack(); } else { switch (SearchMenusMenu.this.resultManager.updateResultItems(searchTerm)) { case NO_RESULTS -> { - SearchMenusMenu.this.noResults.setVisible(false); + SearchMenusMenu.this.noResults.setVisible(true); SearchMenusMenu.this.getPopupMenu().pack(); - SearchMenusMenu.this.getPopupMenu().pack(); } case SAME_RESULTS -> { } case DIFFERENT_RESULTS -> { - SearchMenusMenu.this.noResults.setVisible(true); + SearchMenusMenu.this.noResults.setVisible(false); SearchMenusMenu.this.getPopupMenu().pack(); - SearchMenusMenu.this.getPopupMenu().pack(); } } } @@ -112,6 +143,7 @@ public void retranslate() { this.setText(I18n.translate("menu.help.search")); this.field.setPlaceholder(I18n.translate("menu.help.search.placeholder")); + this.noResults.setText(I18n.translate("menu.help.search.no_results")); } private static class Result { diff --git a/enigma/src/main/resources/lang/en_us.json b/enigma/src/main/resources/lang/en_us.json index 6243f7be9..1de146387 100644 --- a/enigma/src/main/resources/lang/en_us.json +++ b/enigma/src/main/resources/lang/en_us.json @@ -110,6 +110,7 @@ "menu.help.github": "Github Page", "menu.help.search": "Search menus", "menu.help.search.placeholder": "Search menus...", + "menu.help.search.no_results": "No results", "popup_menu.rename": "Rename", "popup_menu.paste": "Paste text", From dfb5540d40e5326fccc91bc7b5c2d0a8ddbce00d Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sat, 15 Nov 2025 14:13:05 -0800 Subject: [PATCH 041/124] implement case-insensitive StringMultiTrie querying --- .../gui/element/menu_bar/SearchMenusMenu.java | 16 +-- .../multi_trie/CompositeStringMultiTrie.java | 47 ++++++-- .../util/multi_trie/MutableMultiTrie.java | 21 +--- .../util/multi_trie/StringMultiTrie.java | 109 +++++++++++++++--- 4 files changed, 144 insertions(+), 49 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java index 648ed79ca..2ef76d355 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java @@ -6,8 +6,8 @@ import org.quiltmc.enigma.gui.element.SearchableElement; import org.quiltmc.enigma.util.I18n; import org.quiltmc.enigma.util.multi_trie.CompositeStringMultiTrie; -import org.quiltmc.enigma.util.multi_trie.MultiTrie; import org.quiltmc.enigma.util.multi_trie.StringMultiTrie; +import org.quiltmc.enigma.util.multi_trie.StringMultiTrie.CharacterNode; import javax.annotation.Nullable; import javax.swing.JMenuItem; @@ -168,7 +168,7 @@ JMenuItem getItem() { private class ResultManager { @Nullable - StringMultiTrie.View resultTrie; + StringMultiTrie.View resultTrie; @Nullable CurrentResults currentResults; @@ -182,9 +182,9 @@ UpdateOutcome updateResultItems(String searchTerm) { if (this.currentResults.searchTerm.length() == searchTerm.length()) { return UpdateOutcome.SAME_RESULTS; } else { - MultiTrie.Node resultNode = this.currentResults.results; + CharacterNode resultNode = this.currentResults.results; for (int i = this.currentResults.searchTerm.length(); i < searchTerm.length(); i++) { - resultNode = resultNode.next(searchTerm.charAt(i)); + resultNode = resultNode.nextIgnoreCase(searchTerm.charAt(i)); } if (resultNode.isEmpty()) { @@ -215,7 +215,7 @@ UpdateOutcome updateResultItems(String searchTerm) { } UpdateOutcome initializeCurrentResults(String searchTerm) { - final MultiTrie.Node results = this.getResultTrie().get(searchTerm); + final CharacterNode results = this.getResultTrie().getIgnoreCase(searchTerm); if (results.isEmpty()) { this.clearCurrent(); @@ -228,7 +228,7 @@ UpdateOutcome initializeCurrentResults(String searchTerm) { } } - StringMultiTrie.View getResultTrie() { + StringMultiTrie.View getResultTrie() { if (this.resultTrie == null) { this.resultTrie = this.buildResultTrie(); } @@ -251,7 +251,7 @@ void clearCurrent() { } } - StringMultiTrie.View buildResultTrie() { + StringMultiTrie.View buildResultTrie() { final CompositeStringMultiTrie elementsBuilder = CompositeStringMultiTrie.createHashed(); SearchMenusMenu.this.gui.getMenuBar() .streamMenus() @@ -269,7 +269,7 @@ StringMultiTrie.View buildResultTrie() { return elementsBuilder.getView(); } - record CurrentResults(MultiTrie.Node results, String searchTerm) { } + record CurrentResults(CharacterNode results, String searchTerm) { } enum UpdateOutcome { NO_RESULTS, SAME_RESULTS, DIFFERENT_RESULTS diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java index 5b36dfc9d..cbc00c9f5 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java @@ -1,5 +1,9 @@ package org.quiltmc.enigma.util.multi_trie; +import org.quiltmc.enigma.util.multi_trie.CompositeStringMultiTrie.Branch; +import org.quiltmc.enigma.util.multi_trie.CompositeStringMultiTrie.Root; + +import javax.annotation.Nonnull; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; @@ -14,7 +18,7 @@ * @see #of(Supplier, Supplier) * @see #createHashed() */ -public final class CompositeStringMultiTrie extends StringMultiTrie> { +public final class CompositeStringMultiTrie extends StringMultiTrie, Root> { private final Root root; private final View view = new View(); @@ -59,22 +63,24 @@ private CompositeStringMultiTrie( } @Override - public MutableMapNode> getRoot() { + public Root getRoot() { return this.root; } @Override - public StringMultiTrie.View getView() { + public StringMultiTrie.View, Root> getView() { return this.view; } - private static final class Root extends MutableMapNode> { + public static final class Root + extends MutableMapNode> + implements MutableCharacterNode> { private final Collection leaves; private final Map> branches; private final CompositeStringMultiTrie.Branch.Factory branchFactory; - private final View view = new View<>(this); + private final NodeView view = new NodeView<>(this); private Root( Map> branches, Collection leaves, @@ -101,12 +107,12 @@ protected Map> getBranches() { } @Override - public MultiTrie.Node getView() { + public CharacterNode getView() { return this.view; } } - public static final class Branch extends MutableMapNode.Branch> { + public static final class Branch extends StringMultiTrie.BranchNode> { private final MutableMapNode> parent; private final Character key; @@ -115,7 +121,7 @@ public static final class Branch extends MutableMapNode.Branch branchFactory; - private final MultiTrie.Node view = new View<>(this); + private final NodeView view = new NodeView<>(this); private Branch( MutableMapNode> parent, char key, @@ -161,7 +167,7 @@ protected Map> getBranches() { } @Override - public MultiTrie.Node getView() { + public CharacterNode getView() { return this.view; } @@ -180,10 +186,29 @@ CompositeStringMultiTrie.Branch create( } } - private class View extends StringMultiTrie.View { + private class View extends StringMultiTrie.View, Root> { @Override - protected StringMultiTrie getViewed() { + protected CompositeStringMultiTrie getViewed() { return CompositeStringMultiTrie.this; } } + + private static final class NodeView extends Node.View implements CharacterNode { + private final MutableCharacterNode> viewed; + + private NodeView(MutableCharacterNode> viewed) { + this.viewed = viewed; + } + + @Nonnull + @Override + public CharacterNode next(Character key) { + return this.viewed.next(key).getView(); + } + + @Override + protected Node getViewed() { + return this.viewed; + } + } } diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMultiTrie.java index ba2491771..acac6f36a 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMultiTrie.java @@ -1,6 +1,5 @@ package org.quiltmc.enigma.util.multi_trie; -import javax.annotation.Nonnull; import java.util.stream.Stream; /** @@ -52,33 +51,23 @@ interface Node extends MultiTrie.Node { */ MultiTrie.Node getView(); - class View implements MultiTrie.Node { - protected final Node viewed; - - protected View(Node viewed) { - this.viewed = viewed; - } - + abstract class View implements MultiTrie.Node { @Override public Stream streamLeaves() { - return this.viewed.streamLeaves(); + return this.getViewed().streamLeaves(); } @Override public Stream streamStems() { - return this.viewed.streamStems(); + return this.getViewed().streamStems(); } @Override public Stream streamValues() { - return this.viewed.streamValues(); + return this.getViewed().streamValues(); } - @Nonnull - @Override - public MultiTrie.Node next(K key) { - return this.viewed.next(key).getView(); - } + protected abstract Node getViewed(); } } } diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java index a81c4e799..1af1bac8d 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java @@ -1,6 +1,11 @@ package org.quiltmc.enigma.util.multi_trie; import org.quiltmc.enigma.util.Utils; +import org.quiltmc.enigma.util.multi_trie.StringMultiTrie.BranchNode; +import org.quiltmc.enigma.util.multi_trie.StringMultiTrie.MutableCharacterNode; + +import java.util.Optional; +import java.util.function.BiFunction; /** * A {@link MutableMultiTrie} that associates sequences of characters with values of type {@code V}. @@ -19,36 +24,69 @@ * @param the type of values * @param the type of branch nodes */ -public abstract class StringMultiTrie> +public abstract class StringMultiTrie + < + V, B extends BranchNode & MutableCharacterNode, + R extends MutableMapNode & MutableCharacterNode + > implements MutableMultiTrie { + private static Optional tryToggleCase(char c) { + if (Character.isUpperCase(c)) { + return Optional.of(Character.toLowerCase(c)); + } else if (Character.isLowerCase(c)) { + return Optional.of(Character.toUpperCase(c)); + } else { + return Optional.empty(); + } + } + private static final String PREFIX = "prefix"; private static final String STRING = "string"; private static final String VALUE = "value"; @Override - public abstract MutableMapNode getRoot(); + public abstract R getRoot(); @Override - public abstract View getView(); + public abstract View getView(); + + public MutableCharacterNode get(String prefix) { + return this.getImpl(prefix, MutableCharacterNode::next); + } + + public MutableCharacterNode getIgnoreCase(String prefix) { + return this.getImpl(prefix, MutableCharacterNode::nextIgnoreCase); + } - public MutableMapNode get(String prefix) { + private MutableCharacterNode getImpl( + String prefix, BiFunction, Character, MutableCharacterNode> next + ) { Utils.requireNonNull(prefix, PREFIX); - MutableMapNode node = this.getRoot(); + MutableCharacterNode node = this.getRoot(); + if (prefix.isEmpty()) { + return node; + } + for (int i = 0; i < prefix.length(); i++) { - node = node.next(prefix.charAt(i)); + node = next.apply(node, prefix.charAt(i)); } return node; } - public MutableMapNode put(String string, V value) { + public MutableCharacterNode put(String string, V value) { Utils.requireNonNull(string, STRING); Utils.requireNonNull(value, VALUE); - MutableMapNode node = this.getRoot(); - for (int i = 0; i < string.length(); i++) { - final MutableMapNode parent = node; + final R root = this.getRoot(); + if (string.isEmpty()) { + return root; + } + + B node = root.next(string.charAt(0)); + for (int i = 1; i < string.length(); i++) { + final B parent = node; final char key = string.charAt(i); node = node.getBranches().computeIfAbsent(key, ignored -> parent.createBranch(key)); } @@ -89,16 +127,59 @@ public boolean removeAll(String string) { return node.clearLeaves(); } - public abstract static class View implements MultiTrie { + public interface CharacterNode extends MultiTrie.Node { + @Override + CharacterNode next(Character key); + + default CharacterNode nextIgnoreCase(Character key) { + final CharacterNode next = this.next(key); + return next.isEmpty() + ? tryToggleCase(key).map(this::next).orElse(next) + : next; + } + } + + public interface MutableCharacterNode + & MutableCharacterNode> + extends CharacterNode, MutableMultiTrie.Node { + @Override + B next(Character key); + + @Override + CharacterNode getView(); + @Override - public Node getRoot() { + default MutableCharacterNode nextIgnoreCase(Character key) { + final MutableCharacterNode next = this.next(key); + return next.isEmpty() + ? tryToggleCase(key).>map(this::next).orElse(next) + : next; + } + } + + public abstract static class BranchNode> + extends MutableMapNode.Branch + implements MutableCharacterNode { } + + public abstract static class View + < + V, B extends BranchNode & MutableCharacterNode, + R extends MutableMapNode & MutableCharacterNode + > + implements MultiTrie { + @Override + public CharacterNode getRoot() { return this.getViewed().getRoot().getView(); } - public MultiTrie.Node get(String prefix) { + public CharacterNode get(String prefix) { return this.getViewed().get(prefix).getView(); } - protected abstract StringMultiTrie getViewed(); + public CharacterNode getIgnoreCase(String prefix) { + return this.getViewed().getIgnoreCase(prefix).getView(); + } + + protected abstract StringMultiTrie getViewed(); } } From e2c2abc34d96bef9e0b04c1d21de545322630f38 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sat, 15 Nov 2025 14:23:13 -0800 Subject: [PATCH 042/124] update StringMultiTrie javadocs --- .../quiltmc/enigma/util/multi_trie/StringMultiTrie.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java index 1af1bac8d..c1396b3c9 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java @@ -10,16 +10,18 @@ /** * A {@link MutableMultiTrie} that associates sequences of characters with values of type {@code V}. * - *

Adds convenience methods for accessing contents by passing a {@link String} instead of passing individual - * characters to nodes: + *

Adds {@link String}/{@link Character}-specific convenience methods for accessing its contents: *

    *
  • {@link #get(String)} + *
  • {@link #getIgnoreCase(String)} *
  • {@link #put(String, Object)} *
  • {@link #remove(String, Object)} *
  • {@link #removeAll(String)} + *
  • {@link CharacterNode#nextIgnoreCase(Character)} *
* - *

{@linkplain #getView() Views} also provide a {@link #get(String)} method. + *

{@linkplain #getView() Views} also provide {@link View#get(String) get} and + * {@link View#getIgnoreCase(String) getIgnoreCase} methods. * * @param the type of values * @param the type of branch nodes From 44161fd81da88ac3131d945e3038eb2d3b3796b4 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sat, 15 Nov 2025 15:08:19 -0800 Subject: [PATCH 043/124] simplify StringMultiTrie generics fix StringMultiTrie::put --- .../multi_trie/CompositeStringMultiTrie.java | 6 +-- .../util/multi_trie/StringMultiTrie.java | 43 ++++++++----------- 2 files changed, 21 insertions(+), 28 deletions(-) diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java index cbc00c9f5..7dc9d58f1 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java @@ -72,9 +72,7 @@ public StringMultiTrie.View, Root> getView() { return this.view; } - public static final class Root - extends MutableMapNode> - implements MutableCharacterNode> { + public static final class Root extends StringMultiTrie.Root> { private final Collection leaves; private final Map> branches; @@ -112,7 +110,7 @@ public CharacterNode getView() { } } - public static final class Branch extends StringMultiTrie.BranchNode> { + public static final class Branch extends StringMultiTrie.Branch> { private final MutableMapNode> parent; private final Character key; diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java index c1396b3c9..16c5e0791 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java @@ -1,8 +1,8 @@ package org.quiltmc.enigma.util.multi_trie; import org.quiltmc.enigma.util.Utils; -import org.quiltmc.enigma.util.multi_trie.StringMultiTrie.BranchNode; -import org.quiltmc.enigma.util.multi_trie.StringMultiTrie.MutableCharacterNode; +import org.quiltmc.enigma.util.multi_trie.StringMultiTrie.Branch; +import org.quiltmc.enigma.util.multi_trie.StringMultiTrie.Root; import java.util.Optional; import java.util.function.BiFunction; @@ -26,13 +26,9 @@ * @param the type of values * @param the type of branch nodes */ -public abstract class StringMultiTrie - < - V, B extends BranchNode & MutableCharacterNode, - R extends MutableMapNode & MutableCharacterNode - > +public abstract class StringMultiTrie, R extends Root> implements MutableMultiTrie { - private static Optional tryToggleCase(char c) { + protected static Optional tryToggleCase(char c) { if (Character.isUpperCase(c)) { return Optional.of(Character.toLowerCase(c)); } else if (Character.isLowerCase(c)) { @@ -66,10 +62,6 @@ private MutableCharacterNode getImpl( Utils.requireNonNull(prefix, PREFIX); MutableCharacterNode node = this.getRoot(); - if (prefix.isEmpty()) { - return node; - } - for (int i = 0; i < prefix.length(); i++) { node = next.apply(node, prefix.charAt(i)); } @@ -83,19 +75,22 @@ public MutableCharacterNode put(String string, V value) { final R root = this.getRoot(); if (string.isEmpty()) { + root.put(value); + return root; } - B node = root.next(string.charAt(0)); + // Don't use next, initially creating orphans, because we always put a value, + // so all orphans would be adopted anyway. + B branch = root.getBranches().computeIfAbsent(string.charAt(0), root::createBranch); for (int i = 1; i < string.length(); i++) { - final B parent = node; - final char key = string.charAt(i); - node = node.getBranches().computeIfAbsent(key, ignored -> parent.createBranch(key)); + final B parent = branch; + branch = branch.getBranches().computeIfAbsent(string.charAt(i), parent::createBranch); } - node.put(value); + branch.put(value); - return node; + return branch; } public boolean remove(String string, V value) { @@ -159,15 +154,15 @@ default MutableCharacterNode nextIgnoreCase(Character key) { } } - public abstract static class BranchNode> + public abstract static class Root> + extends MutableMapNode + implements MutableCharacterNode { } + + public abstract static class Branch> extends MutableMapNode.Branch implements MutableCharacterNode { } - public abstract static class View - < - V, B extends BranchNode & MutableCharacterNode, - R extends MutableMapNode & MutableCharacterNode - > + public abstract static class View, R extends Root> implements MultiTrie { @Override public CharacterNode getRoot() { From c309cc917d59682dc712d0b2767d520779522fde Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sat, 15 Nov 2025 15:20:56 -0800 Subject: [PATCH 044/124] convolute StringMultiTrie generics (to improve theoretical extensibility) --- .../multi_trie/CompositeStringMultiTrie.java | 12 ++++++--- .../util/multi_trie/StringMultiTrie.java | 25 ++++++++++--------- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java index 7dc9d58f1..333806856 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java @@ -72,7 +72,9 @@ public StringMultiTrie.View, Root> getView() { return this.view; } - public static final class Root extends StringMultiTrie.Root> { + public static final class Root + extends MutableMapNode> + implements MutableCharacterNode> { private final Collection leaves; private final Map> branches; @@ -110,7 +112,9 @@ public CharacterNode getView() { } } - public static final class Branch extends StringMultiTrie.Branch> { + public static final class Branch + extends MutableMapNode.Branch> + implements MutableCharacterNode> { private final MutableMapNode> parent; private final Character key; @@ -192,9 +196,9 @@ protected CompositeStringMultiTrie getViewed() { } private static final class NodeView extends Node.View implements CharacterNode { - private final MutableCharacterNode> viewed; + final MutableCharacterNode> viewed; - private NodeView(MutableCharacterNode> viewed) { + NodeView(MutableCharacterNode> viewed) { this.viewed = viewed; } diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java index 16c5e0791..4185cf901 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java @@ -1,8 +1,7 @@ package org.quiltmc.enigma.util.multi_trie; import org.quiltmc.enigma.util.Utils; -import org.quiltmc.enigma.util.multi_trie.StringMultiTrie.Branch; -import org.quiltmc.enigma.util.multi_trie.StringMultiTrie.Root; +import org.quiltmc.enigma.util.multi_trie.StringMultiTrie.MutableCharacterNode; import java.util.Optional; import java.util.function.BiFunction; @@ -26,7 +25,12 @@ * @param the type of values * @param the type of branch nodes */ -public abstract class StringMultiTrie, R extends Root> +public abstract class StringMultiTrie + < + V, + B extends MutableMapNode.Branch & MutableCharacterNode, + R extends MutableMapNode & MutableCharacterNode + > implements MutableMultiTrie { protected static Optional tryToggleCase(char c) { if (Character.isUpperCase(c)) { @@ -154,15 +158,12 @@ default MutableCharacterNode nextIgnoreCase(Character key) { } } - public abstract static class Root> - extends MutableMapNode - implements MutableCharacterNode { } - - public abstract static class Branch> - extends MutableMapNode.Branch - implements MutableCharacterNode { } - - public abstract static class View, R extends Root> + public abstract static class View + < + V, + B extends MutableMapNode.Branch & MutableCharacterNode, + R extends MutableMapNode & MutableCharacterNode + > implements MultiTrie { @Override public CharacterNode getRoot() { From e1b9cd5d45567dda3ce08fab5281a1c9cb23bc8d Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sat, 15 Nov 2025 15:57:38 -0800 Subject: [PATCH 045/124] add case-insensitive tests --- .../CompositeStringMultiTrieTest.java | 72 ++++++++++++++++++- 1 file changed, 69 insertions(+), 3 deletions(-) diff --git a/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java b/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java index 43e61a34f..c9412d921 100644 --- a/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java +++ b/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java @@ -3,7 +3,9 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMultimap; import org.junit.jupiter.api.Test; +import org.opentest4j.AssertionFailedError; import org.quiltmc.enigma.util.multi_trie.MultiTrie.Node; +import org.quiltmc.enigma.util.multi_trie.StringMultiTrie.MutableCharacterNode; import java.util.Collection; import java.util.List; @@ -22,13 +24,36 @@ public class CompositeStringMultiTrieTest { private static final String KEY_BY_KEY_SUBJECT = "key-by-key subject"; + private static final String IGNORE_CASE_SUBJECT = "aBrAcAdAnIeL"; + + @SuppressWarnings("SameParameterValue") + private static String caseInverted(String string) { + final StringBuilder builder = new StringBuilder(); + for (int i = 0; i < string.length(); i++) { + final char c = string.charAt(i); + + final char inverted; + if (Character.isLowerCase(c)) { + inverted = Character.toUpperCase(c); + } else if (Character.isUpperCase(c)) { + inverted = Character.toLowerCase(c); + } else { + inverted = c; + } + + builder.append(inverted); + } + + return builder.toString(); + } + // test key-by-key put's orphan logic @Test void testPutKeyByKeyFromRoot() { final CompositeStringMultiTrie trie = CompositeStringMultiTrie.createHashed(); for (int depth = 0; depth < KEY_BY_KEY_SUBJECT.length(); depth++) { - MutableMapNode node = trie.getRoot(); + MutableCharacterNode node = trie.getRoot(); for (int iKey = 0; iKey <= depth; iKey++) { node = node.next(KEY_BY_KEY_SUBJECT.charAt(iKey)); } @@ -47,7 +72,7 @@ void testPutKeyByKeyFromStems() { final CompositeStringMultiTrie trie = CompositeStringMultiTrie.createHashed(); for (int depth = KEY_BY_KEY_SUBJECT.length() - 1; depth >= 0; depth--) { - MutableMapNode node = trie.getRoot(); + MutableCharacterNode node = trie.getRoot(); for (int iKey = 0; iKey <= depth; iKey++) { node = node.next(KEY_BY_KEY_SUBJECT.charAt(iKey)); } @@ -60,7 +85,7 @@ void testPutKeyByKeyFromStems() { } } - private static void assertOneLeaf(MutableMapNode node) { + private static void assertOneLeaf(MutableCharacterNode node) { assertEquals( 1, node.streamLeaves().count(), () -> "Expected node to have only one leaf, but had the following: " + node.streamLeaves().toList() @@ -210,6 +235,47 @@ private static void assertUnorderedContentsForPrefix( ); } + @Test + void testNextIgnoreCase() { + final CompositeStringMultiTrie trie = CompositeStringMultiTrie.createHashed(); + + trie.put(IGNORE_CASE_SUBJECT, IGNORE_CASE_SUBJECT); + + final String invertedSubject = caseInverted(IGNORE_CASE_SUBJECT); + MutableCharacterNode node = trie.getRoot(); + for (int i = 0; i < invertedSubject.length(); i++) { + node = node.nextIgnoreCase(invertedSubject.charAt(i)); + + assertOneValue(node); + } + + assertOneLeaf(node); + } + + private static void assertOneValue(MutableCharacterNode node) { + assertEquals( + 1, node.getSize(), + "Expected node to have only one value, but had the following: " + node.streamValues().toList() + ); + } + + @Test + void testGetIgnoreCase() { + final CompositeStringMultiTrie trie = CompositeStringMultiTrie.createHashed(); + + trie.put(IGNORE_CASE_SUBJECT, IGNORE_CASE_SUBJECT); + + final String invertedSubject = caseInverted(IGNORE_CASE_SUBJECT); + + final MutableCharacterNode node = trie.getIgnoreCase(invertedSubject); + + assertOneValue(node); + + node.streamLeaves() + .findAny() + .orElseThrow(() -> new AssertionFailedError("Expected node to have a leaf, but had none!")); + } + record Association(String key) { static final Association EMPTY = new Association(""); From bdcddd1c89d8825d6584f38fdb9716c1d83f70fb Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sat, 15 Nov 2025 17:39:52 -0800 Subject: [PATCH 046/124] add SearchableElement::getSearchName and use that for SearchMenusMenu.Result's instead of every alias implement a convention for translatable aliases add several aliases to menus --- .../enigma/gui/element/SearchableElement.java | 17 ++++++++++++++++- .../menu_bar/AbstractSearchableEnigmaMenu.java | 6 ++---- .../enigma/gui/element/menu_bar/CollabMenu.java | 9 ++++++++- .../gui/element/menu_bar/DecompilerMenu.java | 9 ++++++++- .../enigma/gui/element/menu_bar/DevMenu.java | 9 ++++++++- .../enigma/gui/element/menu_bar/HelpMenu.java | 9 ++++++++- .../enigma/gui/element/menu_bar/SearchMenu.java | 9 ++++++++- .../gui/element/menu_bar/SearchMenusMenu.java | 13 ++++++------- .../element/menu_bar/file/CrashHistoryMenu.java | 9 ++++++++- .../gui/element/menu_bar/file/FileMenu.java | 9 ++++++++- .../element/menu_bar/file/OpenRecentMenu.java | 9 ++++++++- .../menu_bar/file/SaveMappingsAsMenu.java | 9 ++++++++- .../menu_bar/view/EntryTooltipsMenu.java | 9 ++++++++- .../element/menu_bar/view/LanguagesMenu.java | 9 ++++++++- .../menu_bar/view/NotificationsMenu.java | 9 ++++++++- .../gui/element/menu_bar/view/ScaleMenu.java | 9 ++++++++- .../gui/element/menu_bar/view/StatsMenu.java | 9 ++++++++- .../gui/element/menu_bar/view/ThemesMenu.java | 9 ++++++++- .../gui/element/menu_bar/view/ViewMenu.java | 9 ++++++++- enigma/src/main/resources/lang/en_us.json | 4 ++++ 20 files changed, 156 insertions(+), 28 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/SearchableElement.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/SearchableElement.java index 9aed694a8..93c4b0ae2 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/SearchableElement.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/SearchableElement.java @@ -1,10 +1,25 @@ package org.quiltmc.enigma.gui.element; +import org.quiltmc.enigma.util.I18n; + import javax.swing.MenuElement; +import java.util.Arrays; import java.util.stream.Stream; public interface SearchableElement extends MenuElement { - Stream streamSearchAliases(); + default Stream streamSearchAliases() { + final String aliases = I18n + .translateOrNull(this.getAliasesTranslationKeyPrefix() + ".aliases"); + + return Stream.concat( + Stream.of(this.getSearchName()), + aliases == null ? Stream.empty() : Arrays.stream(aliases.split(";")) + ); + } + + String getSearchName(); + + String getAliasesTranslationKeyPrefix(); void onSearchClicked(); } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/AbstractSearchableEnigmaMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/AbstractSearchableEnigmaMenu.java index 07772aed1..345accde1 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/AbstractSearchableEnigmaMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/AbstractSearchableEnigmaMenu.java @@ -3,16 +3,14 @@ import org.quiltmc.enigma.gui.Gui; import org.quiltmc.enigma.gui.element.SearchableElement; -import java.util.stream.Stream; - public abstract class AbstractSearchableEnigmaMenu extends AbstractEnigmaMenu implements SearchableElement { protected AbstractSearchableEnigmaMenu(Gui gui) { super(gui); } @Override - public Stream streamSearchAliases() { - return Stream.of(this.getText()); + public String getSearchName() { + return this.getText(); } @Override diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/CollabMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/CollabMenu.java index 9e22ba29c..4b7a30783 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/CollabMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/CollabMenu.java @@ -16,6 +16,8 @@ import java.util.Arrays; public class CollabMenu extends AbstractSearchableEnigmaMenu { + private static final String TRANSLATION_KEY = "menu.collab"; + private final JMenuItem connectItem = new JMenuItem(); private final JMenuItem startServerItem = new JMenuItem(); @@ -31,7 +33,7 @@ public CollabMenu(Gui gui) { @Override public void retranslate() { - this.setText(I18n.translate("menu.collab")); + this.setText(I18n.translate(TRANSLATION_KEY)); this.retranslate(this.gui.getConnectionState()); } @@ -102,4 +104,9 @@ public void onStartServerClicked() { this.gui.getController().disconnectIfConnected(null); } } + + @Override + public String getAliasesTranslationKeyPrefix() { + return TRANSLATION_KEY; + } } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/DecompilerMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/DecompilerMenu.java index 42bef8a8a..f34e7b98e 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/DecompilerMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/DecompilerMenu.java @@ -11,6 +11,8 @@ import javax.swing.JRadioButtonMenuItem; public class DecompilerMenu extends AbstractSearchableEnigmaMenu { + private static final String TRANSLATION_KEY = "menu.decompiler"; + private final JMenuItem decompilerSettingsItem = new JMenuItem(); public DecompilerMenu(Gui gui) { @@ -41,7 +43,12 @@ public DecompilerMenu(Gui gui) { @Override public void retranslate() { - this.setText(I18n.translate("menu.decompiler")); + this.setText(I18n.translate(TRANSLATION_KEY)); this.decompilerSettingsItem.setText(I18n.translate("menu.decompiler.settings")); } + + @Override + public String getAliasesTranslationKeyPrefix() { + return TRANSLATION_KEY; + } } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/DevMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/DevMenu.java index 722f956ac..9dff80ff4 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/DevMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/DevMenu.java @@ -27,6 +27,8 @@ import java.nio.file.Files; public class DevMenu extends AbstractSearchableEnigmaMenu { + private static final String TRANSLATION_KEY = "dev.menu"; + private final JCheckBoxMenuItem showMappingSourcePluginItem = new JCheckBoxMenuItem(); private final JCheckBoxMenuItem debugTokenHighlightsItem = new JCheckBoxMenuItem(); private final JCheckBoxMenuItem logClientPacketsItem = new JCheckBoxMenuItem(); @@ -48,7 +50,7 @@ public DevMenu(Gui gui) { @Override public void retranslate() { - this.setText("Dev"); + this.setText(I18n.translate(TRANSLATION_KEY)); this.showMappingSourcePluginItem.setText(I18n.translate("dev.menu.show_mapping_source_plugin")); this.debugTokenHighlightsItem.setText(I18n.translate("dev.menu.debug_token_highlights")); @@ -128,4 +130,9 @@ private void onPrintMappingTreeClicked() { this.showSavableTextAreaDialog(I18n.translate("dev.mapping_tree"), text.toString(), "mapping_tree.txt"); } + + @Override + public String getAliasesTranslationKeyPrefix() { + return TRANSLATION_KEY; + } } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/HelpMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/HelpMenu.java index 77611d1be..2a9758aa7 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/HelpMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/HelpMenu.java @@ -8,6 +8,8 @@ import javax.swing.JMenuItem; public class HelpMenu extends AbstractSearchableEnigmaMenu { + private static final String TRANSLATION_KEY = "menu.help"; + private final JMenuItem aboutItem = new JMenuItem(); private final JMenuItem githubItem = new JMenuItem(); private final SearchMenusMenu searchItem; @@ -27,7 +29,7 @@ public HelpMenu(Gui gui) { @Override public void retranslate() { - this.setText(I18n.translate("menu.help")); + this.setText(I18n.translate(TRANSLATION_KEY)); this.aboutItem.setText(I18n.translate("menu.help.about")); this.githubItem.setText(I18n.translate("menu.help.github")); this.searchItem.retranslate(); @@ -36,4 +38,9 @@ public void retranslate() { private void onGithubClicked() { GuiUtil.openUrl("https://github.com/QuiltMC/Enigma"); } + + @Override + public String getAliasesTranslationKeyPrefix() { + return TRANSLATION_KEY; + } } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenu.java index 66e428499..76de2301e 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenu.java @@ -9,6 +9,8 @@ import javax.swing.JMenuItem; public class SearchMenu extends AbstractSearchableEnigmaMenu { + private static final String TRANSLATION_KEY = "menu.search"; + private final JMenuItem searchItem = new JMenuItem(GuiUtil.DEOBFUSCATED_ICON); private final JMenuItem searchAllItem = new JMenuItem(GuiUtil.DEOBFUSCATED_ICON); private final JMenuItem searchClassItem = new JMenuItem(GuiUtil.CLASS_ICON); @@ -33,7 +35,7 @@ public SearchMenu(Gui gui) { @Override public void retranslate() { - this.setText(I18n.translate("menu.search")); + this.setText(I18n.translate(TRANSLATION_KEY)); this.searchItem.setText(I18n.translate("menu.search")); this.searchAllItem.setText(I18n.translate("menu.search.all")); this.searchClassItem.setText(I18n.translate("menu.search.class")); @@ -55,4 +57,9 @@ private void onSearchClicked(boolean clear, SearchDialog.Type... types) { this.gui.getSearchDialog().show(clear, types); } } + + @Override + public String getAliasesTranslationKeyPrefix() { + return TRANSLATION_KEY; + } } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java index 2ef76d355..c9016fbd8 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java @@ -148,18 +148,16 @@ public void retranslate() { private static class Result { final SearchableElement element; - final String alias; @Nullable JMenuItem item; - Result(SearchableElement element, String alias) { + Result(SearchableElement element) { this.element = element; - this.alias = alias; } JMenuItem getItem() { if (this.item == null) { - this.item = new JMenuItem(this.alias); + this.item = new JMenuItem(this.element.getSearchName()); } return this.item; @@ -261,9 +259,10 @@ void clearCurrent() { keep.accept(searchable); } }) - .forEach(searchable -> searchable - .streamSearchAliases() - .forEach(alias -> elementsBuilder.put(alias, new Result(searchable, alias))) + .map(Result::new) + .forEach(result -> result + .element.streamSearchAliases() + .forEach(alias -> elementsBuilder.put(alias, result)) ); return elementsBuilder.getView(); diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/file/CrashHistoryMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/file/CrashHistoryMenu.java index 47a9507cd..6579d70b9 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/file/CrashHistoryMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/file/CrashHistoryMenu.java @@ -10,13 +10,15 @@ import javax.swing.JMenuItem; public class CrashHistoryMenu extends AbstractSearchableEnigmaMenu { + private static final String TRANSLATION_KEY = "menu.file.crash_history"; + protected CrashHistoryMenu(Gui gui) { super(gui); } @Override public void retranslate() { - this.setText(I18n.translate("menu.file.crash_history")); + this.setText(I18n.translate(TRANSLATION_KEY)); } @Override @@ -40,4 +42,9 @@ public void updateState(boolean jarOpen, ConnectionState state) { private void onCrashClicked(Throwable throwable) { CrashDialog.show(throwable, false); } + + @Override + public String getAliasesTranslationKeyPrefix() { + return TRANSLATION_KEY; + } } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/file/FileMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/file/FileMenu.java index e34c8b4b9..485df5ac9 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/file/FileMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/file/FileMenu.java @@ -22,6 +22,8 @@ import java.util.Optional; public class FileMenu extends AbstractSearchableEnigmaMenu { + private static final String TRANSLATION_KEY = "menu.file"; + private final SaveMappingsAsMenu saveMappingsAs; private final CrashHistoryMenu crashHistory; private final OpenRecentMenu openRecent; @@ -122,7 +124,7 @@ public void updateState(boolean jarOpen, ConnectionState state) { @Override public void retranslate() { - this.setText(I18n.translate("menu.file")); + this.setText(I18n.translate(TRANSLATION_KEY)); this.jarOpenItem.setText(I18n.translate("menu.file.jar.open")); this.jarCloseItem.setText(I18n.translate("menu.file.jar.close")); this.openRecent.retranslate(); @@ -262,4 +264,9 @@ private void onExportJarClicked() { Config.main().stats.lastSelectedDir.setValue(this.gui.exportJarFileChooser.getCurrentDirectory().getAbsolutePath(), true); } } + + @Override + public String getAliasesTranslationKeyPrefix() { + return TRANSLATION_KEY; + } } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/file/OpenRecentMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/file/OpenRecentMenu.java index d8cd7ead1..88b82e1bb 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/file/OpenRecentMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/file/OpenRecentMenu.java @@ -13,13 +13,15 @@ import java.util.List; public class OpenRecentMenu extends AbstractSearchableEnigmaMenu { + private static final String TRANSLATION_KEY = "menu.file.open_recent_project"; + protected OpenRecentMenu(Gui gui) { super(gui); } @Override public void retranslate() { - this.setText(I18n.translate("menu.file.open_recent_project")); + this.setText(I18n.translate(TRANSLATION_KEY)); } @Override @@ -87,4 +89,9 @@ private static Path findCommonPath(Path a, Path b) { return i != 0 ? a.getRoot().resolve(a.subpath(0, i)) : null; } + + @Override + public String getAliasesTranslationKeyPrefix() { + return TRANSLATION_KEY; + } } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/file/SaveMappingsAsMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/file/SaveMappingsAsMenu.java index 361ca78f8..60a2c7a21 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/file/SaveMappingsAsMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/file/SaveMappingsAsMenu.java @@ -17,6 +17,8 @@ import java.util.function.Consumer; public class SaveMappingsAsMenu extends AbstractSearchableEnigmaMenu { + private static final String TRANSLATION_KEY = "menu.file.mappings.save_as"; + private final Map items = new HashMap<>(); protected SaveMappingsAsMenu(Gui gui) { @@ -33,7 +35,7 @@ protected SaveMappingsAsMenu(Gui gui) { @Override public void retranslate() { - this.setText(I18n.translate("menu.file.mappings.save_as")); + this.setText(I18n.translate(TRANSLATION_KEY)); this.forEachFormat(format -> this.items.get(format).setText(I18n.translate(format.getId()))); } @@ -65,4 +67,9 @@ private void forEachFormat(Consumer consumer) { } }); } + + @Override + public String getAliasesTranslationKeyPrefix() { + return TRANSLATION_KEY; + } } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/EntryTooltipsMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/EntryTooltipsMenu.java index 20110e69a..a3cdc3d25 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/EntryTooltipsMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/EntryTooltipsMenu.java @@ -10,6 +10,8 @@ import static org.quiltmc.enigma.gui.util.GuiUtil.createSyncedMenuCheckBox; public class EntryTooltipsMenu extends AbstractSearchableEnigmaMenu { + private static final String TRANSLATION_KEY = "menu.view.entry_tooltips"; + private final JCheckBoxMenuItem enable = createSyncedMenuCheckBox(Config.editor().entryTooltips.enable); private final JCheckBoxMenuItem interactable = createSyncedMenuCheckBox(Config.editor().entryTooltips.interactable); @@ -24,8 +26,13 @@ protected EntryTooltipsMenu(Gui gui) { @Override public void retranslate() { - this.setText(I18n.translate("menu.view.entry_tooltips")); + this.setText(I18n.translate(TRANSLATION_KEY)); this.enable.setText(I18n.translate("menu.view.entry_tooltips.enable")); this.interactable.setText(I18n.translate("menu.view.entry_tooltips.interactable")); } + + @Override + public String getAliasesTranslationKeyPrefix() { + return TRANSLATION_KEY; + } } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/LanguagesMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/LanguagesMenu.java index 2c206436a..c7a2bd2a7 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/LanguagesMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/LanguagesMenu.java @@ -13,6 +13,8 @@ import java.util.Map; public class LanguagesMenu extends AbstractSearchableEnigmaMenu { + private static final String TRANSLATION_KEY = "menu.view.languages"; + private final Map languages = new HashMap<>(); protected LanguagesMenu(Gui gui) { @@ -31,7 +33,7 @@ protected LanguagesMenu(Gui gui) { @Override public void retranslate() { - this.setText(I18n.translate("menu.view.languages")); + this.setText(I18n.translate(TRANSLATION_KEY)); for (String lang : I18n.getAvailableLanguages()) { this.languages.get(lang).setText(I18n.getLanguageName(lang)); @@ -52,4 +54,9 @@ private void onLanguageButtonClicked(String lang) { I18n.setLanguage(lang); LanguageUtil.dispatchLanguageChange(); } + + @Override + public String getAliasesTranslationKeyPrefix() { + return TRANSLATION_KEY; + } } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/NotificationsMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/NotificationsMenu.java index 515b6fc32..e8e11c330 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/NotificationsMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/NotificationsMenu.java @@ -14,6 +14,8 @@ import static org.quiltmc.enigma.gui.NotificationManager.ServerNotificationLevel; public class NotificationsMenu extends AbstractSearchableEnigmaMenu { + private static final String TRANSLATION_KEY = "menu.view.notifications"; + private final Map buttons = new HashMap<>(); public NotificationsMenu(Gui gui) { @@ -31,7 +33,7 @@ public NotificationsMenu(Gui gui) { @Override public void retranslate() { - this.setText(I18n.translate("menu.view.notifications")); + this.setText(I18n.translate(TRANSLATION_KEY)); for (ServerNotificationLevel level : ServerNotificationLevel.values()) { this.buttons.get(level).setText(level.getText()); @@ -44,4 +46,9 @@ public void updateState(boolean jarOpen, ConnectionState state) { this.buttons.get(level).setSelected(level.equals(Config.main().serverNotificationLevel.value())); } } + + @Override + public String getAliasesTranslationKeyPrefix() { + return TRANSLATION_KEY; + } } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/ScaleMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/ScaleMenu.java index 4c3368996..06d99f7c9 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/ScaleMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/ScaleMenu.java @@ -18,6 +18,8 @@ import java.util.stream.IntStream; public class ScaleMenu extends AbstractSearchableEnigmaMenu { + private static final String TRANSLATION_KEY = "menu.view.scale"; + private final int[] defaultOptions = {100, 125, 150, 175, 200}; private final ButtonGroup optionsGroup = new ButtonGroup(); private final Map options = new HashMap<>(); @@ -49,7 +51,7 @@ protected ScaleMenu(Gui gui) { @Override public void retranslate() { - this.setText(I18n.translate("menu.view.scale")); + this.setText(I18n.translate(TRANSLATION_KEY)); this.customScaleButton.setText(I18n.translate("menu.view.scale.custom")); this.forEachDefaultScaleOption((scaleFactor, realFactor) -> this.options.get(realFactor).setText(String.format("%d%%", scaleFactor))); @@ -97,4 +99,9 @@ private void forEachDefaultScaleOption(BiConsumer consumer) { option -> consumer.accept(option, option / 100f) ); } + + @Override + public String getAliasesTranslationKeyPrefix() { + return TRANSLATION_KEY; + } } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/StatsMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/StatsMenu.java index 8bdab604f..13ba9bbdf 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/StatsMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/StatsMenu.java @@ -16,6 +16,8 @@ import static java.util.concurrent.CompletableFuture.runAsync; public class StatsMenu extends AbstractSearchableEnigmaMenu { + private static final String TRANSLATION_KEY = "menu.view.stat_icons"; + private final JCheckBoxMenuItem enableIcons = new JCheckBoxMenuItem(); private final JCheckBoxMenuItem includeSynthetic = new JCheckBoxMenuItem(); private final JCheckBoxMenuItem countFallback = new JCheckBoxMenuItem(); @@ -44,7 +46,7 @@ public StatsMenu(Gui gui) { @Override public void retranslate() { - this.setText(I18n.translate("menu.view.stat_icons")); + this.setText(I18n.translate(TRANSLATION_KEY)); this.enableIcons.setText(I18n.translate("menu.view.stat_icons.enable_icons")); this.includeSynthetic.setText(I18n.translate("menu.view.stat_icons.include_synthetic")); @@ -98,4 +100,9 @@ private void onCheckboxClicked(StatType type) { private void updateIconsLater() { SwingUtilities.invokeLater(() -> runAsync(() -> this.gui.getController().regenerateAndUpdateStatIcons())); } + + @Override + public String getAliasesTranslationKeyPrefix() { + return TRANSLATION_KEY; + } } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/ThemesMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/ThemesMenu.java index a9474a2f8..fd17c2b04 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/ThemesMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/ThemesMenu.java @@ -16,6 +16,8 @@ import static org.quiltmc.enigma.gui.config.Config.ThemeChoice; public class ThemesMenu extends AbstractSearchableEnigmaMenu { + private static final String TRANSLATION_KEY = "menu.view.themes"; + private final Map themes = new HashMap<>(); protected ThemesMenu(Gui gui) { @@ -34,7 +36,7 @@ protected ThemesMenu(Gui gui) { @Override public void retranslate() { - this.setText(I18n.translate("menu.view.themes")); + this.setText(I18n.translate(TRANSLATION_KEY)); for (ThemeChoice theme : ThemeChoice.values()) { this.themes.get(theme).setText(I18n.translate("menu.view.themes." + theme.name().toLowerCase(Locale.ROOT))); @@ -54,4 +56,9 @@ private void onThemeClicked(ThemeChoice theme) { Config.main().theme.setValue(theme, true); ChangeDialog.show(this.gui.getFrame()); } + + @Override + public String getAliasesTranslationKeyPrefix() { + return TRANSLATION_KEY; + } } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/ViewMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/ViewMenu.java index 6431e4868..c85abddeb 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/ViewMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/ViewMenu.java @@ -9,6 +9,8 @@ import javax.swing.JMenuItem; public class ViewMenu extends AbstractSearchableEnigmaMenu { + private static final String TRANSLATION_KEY = "menu.view"; + private final StatsMenu stats; private final NotificationsMenu notifications; private final LanguagesMenu languages; @@ -40,7 +42,7 @@ public ViewMenu(Gui gui) { @Override public void retranslate() { - this.setText(I18n.translate("menu.view")); + this.setText(I18n.translate(TRANSLATION_KEY)); this.themes.retranslate(); this.notifications.retranslate(); @@ -62,4 +64,9 @@ public void updateState(boolean jarOpen, ConnectionState state) { private void onFontClicked(Gui gui) { FontDialog.display(gui.getFrame()); } + + @Override + public String getAliasesTranslationKeyPrefix() { + return TRANSLATION_KEY; + } } diff --git a/enigma/src/main/resources/lang/en_us.json b/enigma/src/main/resources/lang/en_us.json index 1de146387..3cdbfcd50 100644 --- a/enigma/src/main/resources/lang/en_us.json +++ b/enigma/src/main/resources/lang/en_us.json @@ -67,6 +67,7 @@ "menu.view": "View", "menu.view.notifications": "Server Notifications", "menu.view.themes": "Themes", + "menu.view.themes.aliases": "Skins", "menu.view.themes.default": "Default", "menu.view.themes.darcula": "Darcula", "menu.view.themes.darcerula": "Darcerula", @@ -95,6 +96,7 @@ "menu.search.field": "Search Fields", "menu.search.only_exact_matches": "Exact matches only", "menu.collab": "Collab", + "menu.collab.aliases": "Collaboration;Multiplayer", "menu.collab.connect": "Connect to Server", "menu.collab.connect.error": "Error connecting to server", "menu.collab.disconnect": "Disconnect", @@ -398,6 +400,8 @@ "notification.level.no_chat": "No chat messages", "notification.level.full": "All server notifications", + "dev.menu": "Dev", + "dev.menu.aliases": "Development;Debugging", "dev.menu.show_mapping_source_plugin": "Show mapping source plugin", "dev.menu.debug_token_highlights": "Debug token highlights", "dev.menu.log_client_packets": "Log client packets", From b3d3b075b5448f8a5c70e8425d242acbdc7a97c5 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sat, 15 Nov 2025 19:12:27 -0800 Subject: [PATCH 047/124] add CONTRIBUTING.md with information on translations, esp. alias translations --- CONTRIBUTING.md | 69 +++++++++++++++++++ README.md | 4 ++ .../enigma/gui/element/SearchableElement.java | 18 ++++- 3 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..cd14258d0 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,69 @@ +# Contributing + +Thank you for your interest in contributing to Enigma! + +We recommend discussing your contribution with other members of the community - either directly in your pull request, +or in our other community spaces. We're always happy to help if you need us! + +Enigma is distributed under the [LGPL-3.0](LICENSE). + +## Translating +Translations are loaded from [enigma/src/main/resources/lang/](enigma/src/main/resources/lang/). + +These are the currently supported languages and their corresponding files: + +| Language | File | +|----------------------------|--------------| +| English (U.S.) **default** | `en_us.json` | +| Chinese (Simplified) | `zh_cn.json` | +| French | `fr_fr.json` | +| German | `de_de.json` | +| Japanese | `ja_jp.json` | + +If a language you'd like to translate isn't on the list, feel free to ask for help on +[Quilt's Discord Server](https://discord.quiltmc.org/)! + +### Search Aliases +Many elements in Enigma's GUI support search aliases, but most don't have any aliases. +A full list of search alias translation keys is [below](#complete-list-of-search-alias-translation-keys). +Search aliases are alternative names for an element that a user might search for when looking for that element. +For example, the `Dev` menu element has two search aliases: `"Development"` and `"Debugging"`. This means that if a user +searches for "Debug", the `Dev` menu will be a search result. + +Search aliases are language-specific, so there's no need to translate the English aliases if they aren't likely +to be searched for in your target language. In fact, any language may add additional aliases that aren't present in the +English translation. + +Since elements can have multiple search aliases, their translations can be lists. Aliases are separated by `;`.
+For example, the `Dev` menu's aliases look like this in the translation file: `"Development;Debugging"` +This means that aliases may not contain the `;` character. + +Some things to keep in mind when adding search aliases: +- elements' names are always searchable; there's no need to add their names to their aliases +- searching is case-insensitive, so there's no need to add variations that only differ in capitalization +- searching matches prefixes, so there's no need to add variations that are prefixes of one another, +just add the longest variation (note that the element name may be a prefix of an alias, as is the case with `Dev`'s +`"Development"` alias) + +If you'd like to add search aliases to an element that doesn't already have aliases, add its alias translation key to +its translation file. + +#### Complete list of search alias translation keys +| Element | Translation Key | +|-----------------------------|-------------------------------------------| +| `Dev` menu | `"dev.menu.aliases"` | +| `Collab` menu | `"menu.collab.aliases"` | +| `Decompiler` menu | `"menu.decompiler.aliases"` | +| `Help` menu | `"menu.help.aliases"` | +| `Search` menu | `"menu.search.aliases"` | +| `Crash History` menu | `"menu.file.crash_history.aliases"` | +| `File` menu | `"menu.file.aliases"` | +| `Open Recent Project` menu | `"menu.file.open_recent_project.aliases"` | +| `Save Mappings As...` menu | `"menu.file.mappings.save_as.aliases"` | +| `View` menu | `"menu.view.aliases"` | +| `Entry Tooltips` menu | `"menu.view.entry_tooltips.aliases"` | +| `Languages` menu | `"menu.view.languages.aliases"` | +| `Server Notifications` menu | `"menu.view.notifications.aliases"` | +| `Scale` menu | `"menu.view.scale.aliases"` | +| `Stat Icons` menu | `"menu.view.stat_icons.aliases"` | +| `Themes` menu | `"menu.view.themes.aliases"` | diff --git a/README.md b/README.md index 4b694320a..b8fe4d941 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,10 @@ A tool for deobfuscation of Java bytecode. Forked from , originally created by [Jeff Martin](https://www.cuchazinteractive.com/). +## Contributing + +See [CONTRIBUTING.md](CONTRIBUTING.md) + ## License Enigma is distributed under the [LGPL-3.0](LICENSE). diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/SearchableElement.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/SearchableElement.java index 93c4b0ae2..b32036396 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/SearchableElement.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/SearchableElement.java @@ -7,18 +7,32 @@ import java.util.stream.Stream; public interface SearchableElement extends MenuElement { + + String ALIASES_SUFFIX = ".aliases"; + String ALIAS_DELIMITER = ";"; + default Stream streamSearchAliases() { final String aliases = I18n - .translateOrNull(this.getAliasesTranslationKeyPrefix() + ".aliases"); + .translateOrNull(this.getAliasesTranslationKeyPrefix() + ALIASES_SUFFIX); return Stream.concat( Stream.of(this.getSearchName()), - aliases == null ? Stream.empty() : Arrays.stream(aliases.split(";")) + aliases == null ? Stream.empty() : Arrays.stream(aliases.split(ALIAS_DELIMITER)) ); } String getSearchName(); + /** + * Returns a translation key prefix used to retrieve translatable search aliases.
+ * Usually the prefix is the translation key of the translatable element. + * + *

{@value ALIASES_SUFFIX} is appended to create the complete translation key.
+ * Alias translations hold multiple aliases separated by {@value ALIAS_DELIMITER}. + * + *

All alias translation key prefixes should be documented in {@code CONTRIBUTING.md} under
+ * {@code Translating > Search Aliases > Complete list of search alias translation keys} + */ String getAliasesTranslationKeyPrefix(); void onSearchClicked(); From 2d633fdea337bd90dc520fde3a0b8334d8a476bf Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Tue, 18 Nov 2025 15:16:00 -0800 Subject: [PATCH 048/124] replace javax nullity annotations with jspecify's replace Pure annotations with javadocs --- .../gui/element/PlaceheldTextField.java | 2 +- .../gui/element/menu_bar/SearchMenusMenu.java | 5 ++-- .../quiltmc/enigma/util/CompositeBiMap.java | 30 ++++++++----------- .../multi_trie/CompositeStringMultiTrie.java | 4 +-- .../enigma/util/multi_trie/MapNode.java | 6 ++-- .../util/multi_trie/MutableMapNode.java | 19 +++++++----- 6 files changed, 33 insertions(+), 33 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/PlaceheldTextField.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/PlaceheldTextField.java index d805dde85..4ff77013d 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/PlaceheldTextField.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/PlaceheldTextField.java @@ -1,8 +1,8 @@ package org.quiltmc.enigma.gui.element; +import org.jspecify.annotations.Nullable; import org.quiltmc.enigma.util.Utils; -import javax.annotation.Nullable; import javax.swing.JTextField; import javax.swing.MenuElement; import javax.swing.MenuSelectionManager; diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java index c9016fbd8..19f62bdd3 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java @@ -1,5 +1,6 @@ package org.quiltmc.enigma.gui.element.menu_bar; +import org.jspecify.annotations.Nullable; import org.quiltmc.enigma.gui.ConnectionState; import org.quiltmc.enigma.gui.Gui; import org.quiltmc.enigma.gui.element.PlaceheldTextField; @@ -9,7 +10,6 @@ import org.quiltmc.enigma.util.multi_trie.StringMultiTrie; import org.quiltmc.enigma.util.multi_trie.StringMultiTrie.CharacterNode; -import javax.annotation.Nullable; import javax.swing.JMenuItem; import javax.swing.MenuElement; import javax.swing.event.DocumentEvent; @@ -165,8 +165,7 @@ JMenuItem getItem() { } private class ResultManager { - @Nullable - StringMultiTrie.View resultTrie; + StringMultiTrie.@Nullable View resultTrie; @Nullable CurrentResults currentResults; diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/CompositeBiMap.java b/enigma/src/main/java/org/quiltmc/enigma/util/CompositeBiMap.java index 6569f90f4..53ee23ad5 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/CompositeBiMap.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/CompositeBiMap.java @@ -2,9 +2,8 @@ import com.google.common.collect.BiMap; import com.google.common.collect.MapMaker; +import org.jspecify.annotations.NonNull; -import javax.annotation.CheckForNull; -import javax.annotation.Nonnull; import java.util.Collection; import java.util.Iterator; import java.util.Map; @@ -63,7 +62,6 @@ public V get(Object key) { * * @see #put(Object, Object) */ - @CheckForNull @Override public V put(K key, V value) { if (this.containsValue(value)) { @@ -88,7 +86,6 @@ public V remove(Object key) { } } - @CheckForNull @Override public V forcePut(K key, V value) { this.reverse.remove(value); @@ -109,21 +106,21 @@ public void clear() { @SuppressWarnings("SuspiciousMethodCalls") @Override - @Nonnull + @NonNull public Set keySet() { return new LiveSet<>(Map.Entry::getKey, "key", this.forward::keySet, this.forward::containsKey); } @SuppressWarnings("SuspiciousMethodCalls") @Override - @Nonnull + @NonNull public Set values() { return new LiveSet<>(Map.Entry::getValue, "value", this.reverse::keySet, this.reverse::containsKey); } @SuppressWarnings("SuspiciousMethodCalls") @Override - @Nonnull + @NonNull public Set> entrySet() { return new LiveSet<>( Function.identity(), "entry", this.forward::entrySet, @@ -134,7 +131,7 @@ public Set> entrySet() { } @Override - @Nonnull + @NonNull public BiMap inverse() { if (this.inverse == null) { this.inverse = new Inverse<>(this); @@ -150,7 +147,7 @@ private static class Inverse extends CompositeBiMap { } @Override - @Nonnull + @NonNull public BiMap inverse() { return this.inverse; } @@ -193,20 +190,19 @@ public boolean contains(Object o) { } @Override - @Nonnull + @NonNull public Iterator iterator() { return new LiveIterator(); } @Override - @Nonnull - public Object[] toArray() { + public Object @NonNull[] toArray() { return this.getDelegateSet.get().toArray(); } @Override - @Nonnull - public T[] toArray(@Nonnull T[] array) { + @NonNull + public T @NonNull[] toArray(T @NonNull[] array) { return this.getDelegateSet.get().toArray(array); } @@ -221,7 +217,7 @@ public boolean remove(Object o) { } @Override - public boolean containsAll(@Nonnull Collection collection) { + public boolean containsAll(Collection collection) { for (final Object o : collection) { if (!this.containsElement.test(o)) { return false; @@ -232,12 +228,12 @@ public boolean containsAll(@Nonnull Collection collection) { } @Override - public boolean addAll(@Nonnull Collection collection) { + public boolean addAll(@NonNull Collection collection) { throw addExceptionOf(this.elementName); } @Override - public boolean retainAll(@Nonnull Collection collection) { + public boolean retainAll(@NonNull Collection collection) { return this.removeAllMatching(key -> !collection.contains(key)); } diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java index 333806856..3e37b2a81 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java @@ -1,9 +1,9 @@ package org.quiltmc.enigma.util.multi_trie; +import org.jspecify.annotations.NonNull; import org.quiltmc.enigma.util.multi_trie.CompositeStringMultiTrie.Branch; import org.quiltmc.enigma.util.multi_trie.CompositeStringMultiTrie.Root; -import javax.annotation.Nonnull; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; @@ -202,7 +202,7 @@ private static final class NodeView extends Node.View implement this.viewed = viewed; } - @Nonnull + @NonNull @Override public CharacterNode next(Character key) { return this.viewed.next(key).getView(); diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MapNode.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MapNode.java index d8e6e5cbd..e9663b72c 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MapNode.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MapNode.java @@ -1,7 +1,5 @@ package org.quiltmc.enigma.util.multi_trie; -import org.checkerframework.dataflow.qual.Pure; - import java.util.Map; import java.util.stream.Stream; @@ -23,6 +21,8 @@ public Stream streamValues() { return Stream.concat(this.streamLeaves(), this.streamStems()); } - @Pure + /** + * Implementations should be pure (stateless, no side effects). + */ protected abstract Map getBranches(); } diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMapNode.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMapNode.java index 82e6ca5d2..155b7cbe1 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMapNode.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMapNode.java @@ -1,10 +1,9 @@ package org.quiltmc.enigma.util.multi_trie; import com.google.common.collect.MapMaker; -import org.checkerframework.dataflow.qual.Pure; +import org.jspecify.annotations.Nullable; import org.quiltmc.enigma.util.Utils; -import javax.annotation.Nullable; import java.util.Collection; import java.util.Map; import java.util.stream.Stream; @@ -109,12 +108,15 @@ protected boolean tryAdopt(Branch branch) { } /** + * Implementations should be pure (stateless, no side effects). + * * @return a new, empty branch node instance */ - @Pure protected abstract B createBranch(K key); - @Pure + /** + * Implementations should be pure (stateless, no side effects). + */ protected abstract Collection getLeaves(); /** @@ -128,21 +130,24 @@ protected boolean tryAdopt(Branch branch) { */ protected abstract static class Branch> extends MutableMapNode { /** + * Implementations should be pure (stateless, no side effects). + * * @return this branch's parent; may or may not be another branch node */ - @Pure protected abstract MutableMapNode getParent(); /** + * Implementations should be pure (stateless, no side effects). + * * @return the last key in this branch's sequence; the key this branch's parent stores it under */ - @Pure protected abstract K getKey(); /** + * Implementations should be pure (stateless, no side effects). + * * @return this branch */ - @Pure protected abstract B getSelf(); @Override From 8fbdead40c35a1e097091747983fa6ab819bc514 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Tue, 18 Nov 2025 15:21:26 -0800 Subject: [PATCH 049/124] checkstyle --- .../enigma/gui/element/SearchableElement.java | 1 - .../util/multi_trie/StringMultiTrie.java | 8 +- .../CompositeStringMultiTrieTest.java | 96 +++++++++---------- 3 files changed, 52 insertions(+), 53 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/SearchableElement.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/SearchableElement.java index b32036396..efdf23871 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/SearchableElement.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/SearchableElement.java @@ -7,7 +7,6 @@ import java.util.stream.Stream; public interface SearchableElement extends MenuElement { - String ALIASES_SUFFIX = ".aliases"; String ALIAS_DELIMITER = ";"; diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java index 4185cf901..42aeca9d3 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java @@ -135,8 +135,8 @@ public interface CharacterNode extends MultiTrie.Node { default CharacterNode nextIgnoreCase(Character key) { final CharacterNode next = this.next(key); return next.isEmpty() - ? tryToggleCase(key).map(this::next).orElse(next) - : next; + ? tryToggleCase(key).map(this::next).orElse(next) + : next; } } @@ -153,8 +153,8 @@ public interface MutableCharacterNode default MutableCharacterNode nextIgnoreCase(Character key) { final MutableCharacterNode next = this.next(key); return next.isEmpty() - ? tryToggleCase(key).>map(this::next).orElse(next) - : next; + ? tryToggleCase(key).>map(this::next).orElse(next) + : next; } } diff --git a/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java b/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java index c9412d921..4f26dcde2 100644 --- a/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java +++ b/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java @@ -87,16 +87,16 @@ void testPutKeyByKeyFromStems() { private static void assertOneLeaf(MutableCharacterNode node) { assertEquals( - 1, node.streamLeaves().count(), - () -> "Expected node to have only one leaf, but had the following: " + node.streamLeaves().toList() + 1, node.streamLeaves().count(), + () -> "Expected node to have only one leaf, but had the following: " + node.streamLeaves().toList() ); } private static void assertTrieSize(CompositeStringMultiTrie trie, int expectedSize) { assertEquals( - expectedSize, trie.getSize(), - () -> "Expected node to have %s values, but had the following: %s" - .formatted(expectedSize, trie.getRoot().streamValues().toList()) + expectedSize, trie.getSize(), + () -> "Expected node to have %s values, but had the following: %s" + .formatted(expectedSize, trie.getRoot().streamValues().toList()) ); } @@ -110,15 +110,15 @@ void testPut() { assertUnorderedContentsForPrefix(prefix, VALUES, associations.stream(), node.streamValues()); assertUnorderedContentsForPrefix( - prefix, LEAVES, - associations.stream().filter(association -> association.isLeafOf(prefix)), - node.streamLeaves() + prefix, LEAVES, + associations.stream().filter(association -> association.isLeafOf(prefix)), + node.streamLeaves() ); assertUnorderedContentsForPrefix( - prefix, BRANCHES, - associations.stream().filter(association -> association.isBranchOf(prefix)), - node.streamStems() + prefix, BRANCHES, + associations.stream().filter(association -> association.isBranchOf(prefix)), + node.streamStems() ); }); } @@ -131,21 +131,21 @@ void testPutMulti() { final Node node = trie.get(prefix); assertUnorderedContentsForPrefix( - prefix, VALUES, - MultiAssociation.streamWith(associations.stream()), - node.streamValues() + prefix, VALUES, + MultiAssociation.streamWith(associations.stream()), + node.streamValues() ); assertUnorderedContentsForPrefix( - prefix, LEAVES, - MultiAssociation.streamWith(associations.stream().filter(association -> association.isLeafOf(prefix))), - node.streamLeaves() + prefix, LEAVES, + MultiAssociation.streamWith(associations.stream().filter(association -> association.isLeafOf(prefix))), + node.streamLeaves() ); assertUnorderedContentsForPrefix( - prefix, BRANCHES, - MultiAssociation.streamWith(associations.stream().filter(a -> a.isBranchOf(prefix))), - node.streamStems() + prefix, BRANCHES, + MultiAssociation.streamWith(associations.stream().filter(a -> a.isBranchOf(prefix))), + node.streamStems() ); }); } @@ -185,16 +185,16 @@ void testRemoveAll() { Association.BY_PREFIX.asMap().forEach((prefix, associations) -> { final List leaves = associations.stream() - .filter(association -> association.isLeafOf(prefix)) - .toList(); + .filter(association -> association.isLeafOf(prefix)) + .toList(); final boolean expectRemoval = !leaves.isEmpty(); assertEquals(expectRemoval, trie.removeAll(prefix), () -> { return expectRemoval - ? "Expected removal of leaves with prefix \"%s\": %s" - .formatted(prefix, MultiAssociation.streamWith(leaves.stream()).toList()) - : "Expected no removal of nodes with prefix \"%s\": %s" - .formatted(prefix, MultiAssociation.streamWith(associations.stream()).toList()); + ? "Expected removal of leaves with prefix \"%s\": %s" + .formatted(prefix, MultiAssociation.streamWith(leaves.stream()).toList()) + : "Expected no removal of nodes with prefix \"%s\": %s" + .formatted(prefix, MultiAssociation.streamWith(associations.stream()).toList()); }); }); @@ -202,26 +202,26 @@ void testRemoveAll() { } private static void assertRemovalResult( - CompositeStringMultiTrie trie, boolean expectRemoval, String prefix, T value + CompositeStringMultiTrie trie, boolean expectRemoval, String prefix, T value ) { assertEquals( - expectRemoval, - trie.remove(prefix, value), - () -> "Expected%s removal of \"%s\" with prefix \"%s\"!" - .formatted(expectRemoval ? "" : " no", value, prefix) + expectRemoval, + trie.remove(prefix, value), + () -> "Expected%s removal of \"%s\" with prefix \"%s\"!" + .formatted(expectRemoval ? "" : " no", value, prefix) ); } private static void assertEmpty(CompositeStringMultiTrie trie) { assertTrue( - trie.isEmpty(), - () ->"Expected trie to be empty, but had it contents: " + trie.getRoot().streamValues().toList() + trie.isEmpty(), + () -> "Expected trie to be empty, but had it contents: " + trie.getRoot().streamValues().toList() ); final Map> rootChildren = trie.getRoot().getBranches(); assertTrue( - rootChildren.isEmpty(), - () -> "Expected root's children to be pruned, but it had children: " + rootChildren + rootChildren.isEmpty(), + () -> "Expected root's children to be pruned, but it had children: " + rootChildren ); } @@ -229,9 +229,9 @@ private static void assertUnorderedContentsForPrefix( String prefix, String arrayName, Stream expected, Stream actual ) { assertThat( - "Unexpected %s for prefix \"%s\"!".formatted(arrayName, prefix), - actual.toList(), - containsInAnyOrder(expected.toArray()) + "Unexpected %s for prefix \"%s\"!".formatted(arrayName, prefix), + actual.toList(), + containsInAnyOrder(expected.toArray()) ); } @@ -254,8 +254,8 @@ void testNextIgnoreCase() { private static void assertOneValue(MutableCharacterNode node) { assertEquals( - 1, node.getSize(), - "Expected node to have only one value, but had the following: " + node.streamValues().toList() + 1, node.getSize(), + "Expected node to have only one value, but had the following: " + node.streamValues().toList() ); } @@ -272,8 +272,8 @@ void testGetIgnoreCase() { assertOneValue(node); node.streamLeaves() - .findAny() - .orElseThrow(() -> new AssertionFailedError("Expected node to have a leaf, but had none!")); + .findAny() + .orElseThrow(() -> new AssertionFailedError("Expected node to have a leaf, but had none!")); } record Association(String key) { @@ -299,12 +299,12 @@ record Association(String key) { static final Association EERHT = new Association("EERHT"); static final ImmutableList ALL = ImmutableList.of( - EMPTY, - A, AB, ABC, - BA, CBA, - I, II, III, - ONE, TWO, THREE, - ENO, OWT, EERHT + EMPTY, + A, AB, ABC, + BA, CBA, + I, II, III, + ONE, TWO, THREE, + ENO, OWT, EERHT ); static final ImmutableMultimap BY_PREFIX; From e0fb42df2bb385928dcc827bb382908d0a5871ec Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Tue, 18 Nov 2025 15:37:49 -0800 Subject: [PATCH 050/124] inline association instances in ALL --- .../CompositeStringMultiTrieTest.java | 33 ++++--------------- 1 file changed, 6 insertions(+), 27 deletions(-) diff --git a/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java b/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java index 4f26dcde2..c4aae5e20 100644 --- a/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java +++ b/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java @@ -277,34 +277,13 @@ void testGetIgnoreCase() { } record Association(String key) { - static final Association EMPTY = new Association(""); - - static final Association A = new Association("A"); - static final Association AB = new Association("AB"); - static final Association ABC = new Association("ABC"); - - static final Association BA = new Association("BA"); - static final Association CBA = new Association("CBA"); - - static final Association I = new Association("I"); - static final Association II = new Association("II"); - static final Association III = new Association("III"); - - static final Association ONE = new Association("ONE"); - static final Association TWO = new Association("TWO"); - static final Association THREE = new Association("THREE"); - - static final Association ENO = new Association("ENO"); - static final Association OWT = new Association("OWT"); - static final Association EERHT = new Association("EERHT"); - static final ImmutableList ALL = ImmutableList.of( - EMPTY, - A, AB, ABC, - BA, CBA, - I, II, III, - ONE, TWO, THREE, - ENO, OWT, EERHT + new Association(""), + new Association("A"), new Association("AB"), new Association("ABC"), + new Association("BA"), new Association("CBA"), + new Association("I"), new Association("II"), new Association("III"), + new Association("ONE"), new Association("TWO"), new Association("THREE"), + new Association("ENO"), new Association("OWT"), new Association("EERHT") ); static final ImmutableMultimap BY_PREFIX; From 62b130d230bb2aeff0421c1331ead6cb9588dc46 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Tue, 18 Nov 2025 15:47:38 -0800 Subject: [PATCH 051/124] re-rename MutableMultiTrie getView methods -> view --- .../enigma/gui/element/menu_bar/SearchMenusMenu.java | 2 +- .../util/multi_trie/CompositeStringMultiTrie.java | 8 ++++---- .../enigma/util/multi_trie/MutableMultiTrie.java | 4 ++-- .../enigma/util/multi_trie/StringMultiTrie.java | 12 ++++++------ 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java index 19f62bdd3..874b33e59 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java @@ -264,7 +264,7 @@ void clearCurrent() { .forEach(alias -> elementsBuilder.put(alias, result)) ); - return elementsBuilder.getView(); + return elementsBuilder.view(); } record CurrentResults(CharacterNode results, String searchTerm) { } diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java index 3e37b2a81..18ad84e4d 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java @@ -68,7 +68,7 @@ public Root getRoot() { } @Override - public StringMultiTrie.View, Root> getView() { + public StringMultiTrie.View, Root> view() { return this.view; } @@ -107,7 +107,7 @@ protected Map> getBranches() { } @Override - public CharacterNode getView() { + public CharacterNode view() { return this.view; } } @@ -169,7 +169,7 @@ protected Map> getBranches() { } @Override - public CharacterNode getView() { + public CharacterNode view() { return this.view; } @@ -205,7 +205,7 @@ private static final class NodeView extends Node.View implement @NonNull @Override public CharacterNode next(Character key) { - return this.viewed.next(key).getView(); + return this.viewed.next(key).view(); } @Override diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMultiTrie.java index acac6f36a..429767eb0 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMultiTrie.java @@ -15,7 +15,7 @@ public interface MutableMultiTrie extends MultiTrie { /** * @return a live, unmodifiable view of this trie */ - MultiTrie getView(); + MultiTrie view(); /** * A mutable node representing values associated with a {@link MutableMultiTrie}. @@ -49,7 +49,7 @@ interface Node extends MultiTrie.Node { /** * @return a live, unmodifiable view of this node */ - MultiTrie.Node getView(); + MultiTrie.Node view(); abstract class View implements MultiTrie.Node { @Override diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java index 42aeca9d3..4160e83e7 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java @@ -19,7 +19,7 @@ *

  • {@link CharacterNode#nextIgnoreCase(Character)} * * - *

    {@linkplain #getView() Views} also provide {@link View#get(String) get} and + *

    {@linkplain #view() Views} also provide {@link View#get(String) get} and * {@link View#getIgnoreCase(String) getIgnoreCase} methods. * * @param the type of values @@ -50,7 +50,7 @@ protected static Optional tryToggleCase(char c) { public abstract R getRoot(); @Override - public abstract View getView(); + public abstract View view(); public MutableCharacterNode get(String prefix) { return this.getImpl(prefix, MutableCharacterNode::next); @@ -147,7 +147,7 @@ public interface MutableCharacterNode B next(Character key); @Override - CharacterNode getView(); + CharacterNode view(); @Override default MutableCharacterNode nextIgnoreCase(Character key) { @@ -167,15 +167,15 @@ public abstract static class View implements MultiTrie { @Override public CharacterNode getRoot() { - return this.getViewed().getRoot().getView(); + return this.getViewed().getRoot().view(); } public CharacterNode get(String prefix) { - return this.getViewed().get(prefix).getView(); + return this.getViewed().get(prefix).view(); } public CharacterNode getIgnoreCase(String prefix) { - return this.getViewed().getIgnoreCase(prefix).getView(); + return this.getViewed().getIgnoreCase(prefix).view(); } protected abstract StringMultiTrie getViewed(); From 1e0c741aba2a9aed59feb50dd5b19faaeafadbc5 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Tue, 18 Nov 2025 19:24:20 -0800 Subject: [PATCH 052/124] add previous methods to MutliTrie.Node, eliminate MapNode --- .../multi_trie/CompositeStringMultiTrie.java | 67 ++++++++++++++++--- .../enigma/util/multi_trie/MapNode.java | 28 -------- .../enigma/util/multi_trie/MultiTrie.java | 40 ++++++++++- .../util/multi_trie/MutableMapNode.java | 20 +++++- .../util/multi_trie/MutableMultiTrie.java | 11 +++ .../util/multi_trie/StringMultiTrie.java | 12 ++++ 6 files changed, 138 insertions(+), 40 deletions(-) delete mode 100644 enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MapNode.java diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java index 18ad84e4d..c3b8c9fc6 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java @@ -91,6 +91,21 @@ private Root( this.branchFactory = branchFactory; } + @Override + public Root previous() { + return this; + } + + @Override + public Root previous(int steps) { + return this; + } + + @Override + public int getDepth() { + return 0; + } + @Override protected CompositeStringMultiTrie.Branch createBranch(Character key) { return this.branchFactory.create(key, this); @@ -116,7 +131,9 @@ public static final class Branch extends MutableMapNode.Branch> implements MutableCharacterNode> { private final MutableMapNode> parent; + private final MutableCharacterNode> previous; private final Character key; + private final int depth; private final Collection leaves; private final Map> branches; @@ -125,19 +142,42 @@ public static final class Branch private final NodeView view = new NodeView<>(this); - private Branch( - MutableMapNode> parent, char key, - Collection leaves, Map> branches, - CompositeStringMultiTrie.Branch.Factory branchFactory + private < + P extends MutableMapNode> + & MutableCharacterNode> + > Branch( + P parent, char key, + int depth, Collection leaves, Map> branches, + Factory branchFactory ) { + // two references to the same instance because both its types are necessary this.parent = parent; + this.previous = parent; + this.key = key; + this.depth = depth; this.leaves = leaves; this.branches = branches; this.branchFactory = branchFactory; } + @Override + public MutableCharacterNode> previous() { + return this.previous; + } + + @Override + public MutableCharacterNode> previous(int steps) { + return MultiTrie.Node.>> + previous(this, steps, MutableCharacterNode::previous); + } + + @Override + public int getDepth() { + return this.depth; + } + @Override protected MutableMapNode> getParent() { return this.parent; @@ -177,11 +217,12 @@ private record Factory( Supplier> leavesFactory, Supplier>> branchesFactory ) { - CompositeStringMultiTrie.Branch create( - char key, MutableMapNode> parent - ) { + < + P extends MutableMapNode> + & MutableCharacterNode> + > CompositeStringMultiTrie.Branch create(char key, P parent) { return new CompositeStringMultiTrie.Branch<>( - parent, key, this.leavesFactory.get(), this.branchesFactory.get(), + parent, key, parent.getDepth() + 1, this.leavesFactory.get(), this.branchesFactory.get(), this ); } @@ -208,6 +249,16 @@ public CharacterNode next(Character key) { return this.viewed.next(key).view(); } + @Override + public CharacterNode previous() { + return this.viewed.previous().view(); + } + + @Override + public CharacterNode previous(int steps) { + return this.viewed.previous(steps).view(); + } + @Override protected Node getViewed() { return this.viewed; diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MapNode.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MapNode.java deleted file mode 100644 index e9663b72c..000000000 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MapNode.java +++ /dev/null @@ -1,28 +0,0 @@ -package org.quiltmc.enigma.util.multi_trie; - -import java.util.Map; -import java.util.stream.Stream; - -/** - * A {@link MultiTrie.Node} that stores branch nodes in a {@link Map}. - * - * @param the type of keys - * @param the type of values - * @param the type of branch nodes - */ -public abstract class MapNode> implements MultiTrie.Node { - @Override - public Stream streamStems() { - return this.getBranches().values().stream().flatMap(MapNode::streamValues); - } - - @Override - public Stream streamValues() { - return Stream.concat(this.streamLeaves(), this.streamStems()); - } - - /** - * Implementations should be pure (stateless, no side effects). - */ - protected abstract Map getBranches(); -} diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MultiTrie.java index 42dde99db..9e6e9f23e 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MultiTrie.java @@ -1,5 +1,8 @@ package org.quiltmc.enigma.util.multi_trie; +import com.google.common.base.Preconditions; + +import java.util.function.UnaryOperator; import java.util.stream.Stream; /** @@ -41,6 +44,21 @@ default boolean isEmpty() { * @param the type of values */ interface Node { + static > N previous(N node, int steps, UnaryOperator previous) { + Preconditions.checkArgument(steps >= 0, "steps must not be negative!"); + + if (steps == 0) { + return node; + } + + N prev = previous.apply(node); + while (--steps > 0 && prev.getDepth() > 0) { + prev = previous.apply(prev); + } + + return prev; + } + /** * @return a {@link Stream} containing all values with no more keys in their associated sequence;
    * i.e. the prefix this node is associated with is the whole sequence the values are associated with @@ -57,7 +75,9 @@ interface Node { /** * @return a {@link Stream} containing all values associated with the prefix this node is associated with */ - Stream streamValues(); + default Stream streamValues() { + return Stream.concat(this.streamLeaves(), this.streamStems()); + } /** * @return the total number of {@linkplain #streamValues() values} associated with this node's prefix @@ -78,5 +98,23 @@ default boolean isEmpty() { * {@code key} to this node's sequence */ Node next(K key); + + /** + * @return this node's parent if it is not the {@linkplain #getRoot() root}, otherwise returns this node + */ + Node previous(); + + /** + * Equivalent to chaining {@link #previous()} calls a number of times equal to the passed {@code steps}. + * + * @throws IllegalArgumentException if the passed {@code steps} is negative + */ + Node previous(int steps); + + /** + * @return this node's depth: the length of the prefix it is associated with; the root returns {@code 0}, + * non-roots return positive integers + */ + int getDepth(); } } diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMapNode.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMapNode.java index 155b7cbe1..3d24a0c12 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMapNode.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMapNode.java @@ -3,6 +3,7 @@ import com.google.common.collect.MapMaker; import org.jspecify.annotations.Nullable; import org.quiltmc.enigma.util.Utils; +import org.quiltmc.enigma.util.multi_trie.MutableMapNode.Branch; import java.util.Collection; import java.util.Map; @@ -18,9 +19,7 @@ * @param the type of values * @param the type of branch nodes */ -public abstract class MutableMapNode> - extends MapNode - implements MutableMultiTrie.Node { +public abstract class MutableMapNode> implements MutableMultiTrie.Node { /** * Orphans are empty nodes. * @@ -33,6 +32,21 @@ public abstract class MutableMapNode orphans = new MapMaker().weakValues().makeMap(); + @Override + public Stream streamStems() { + return this.getBranches().values().stream().flatMap(MutableMapNode::streamValues); + } + + @Override + public Stream streamValues() { + return Stream.concat(this.streamLeaves(), this.streamStems()); + } + + /** + * Implementations should be pure (stateless, no side effects). + */ + protected abstract Map getBranches(); + @Override public Stream streamLeaves() { return this.getLeaves().stream(); diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMultiTrie.java index 429767eb0..525c84a61 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMultiTrie.java @@ -27,6 +27,12 @@ interface Node extends MultiTrie.Node { @Override Node next(K key); + @Override + Node previous(); + + @Override + Node previous(int steps); + /** * @param value a value to add to this node's leaves, associating it with the sequence leading to this node. */ @@ -67,6 +73,11 @@ public Stream streamValues() { return this.getViewed().streamValues(); } + @Override + public int getDepth() { + return this.getViewed().getDepth(); + } + protected abstract Node getViewed(); } } diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java index 4160e83e7..8385bf0e3 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java @@ -138,6 +138,12 @@ default CharacterNode nextIgnoreCase(Character key) { ? tryToggleCase(key).map(this::next).orElse(next) : next; } + + @Override + CharacterNode previous(); + + @Override + CharacterNode previous(int steps); } public interface MutableCharacterNode @@ -146,6 +152,12 @@ public interface MutableCharacterNode @Override B next(Character key); + @Override + MutableCharacterNode previous(); + + @Override + MutableCharacterNode previous(int steps); + @Override CharacterNode view(); From 942f118f01d2bbd0fc34bb9ad586637ce9502885 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Tue, 18 Nov 2025 19:26:55 -0800 Subject: [PATCH 053/124] make orphans private --- .../java/org/quiltmc/enigma/util/multi_trie/MutableMapNode.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMapNode.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMapNode.java index 3d24a0c12..743a352c2 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMapNode.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMapNode.java @@ -30,7 +30,7 @@ public abstract class MutableMapNode> implements * Using a map with weak value references prevents memory leaks when users look up a sequence with no * values and don't put any value in it. */ - final Map orphans = new MapMaker().weakValues().makeMap(); + private final Map orphans = new MapMaker().weakValues().makeMap(); @Override public Stream streamStems() { From 0b6654ea6ac9f92a2112c31a7a120a602ddb3efd Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 19 Nov 2025 08:36:55 -0800 Subject: [PATCH 054/124] convert StringMultiTrie to interface --- .../multi_trie/CompositeStringMultiTrie.java | 2 +- .../util/multi_trie/StringMultiTrie.java | 32 +++++++++---------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java index c3b8c9fc6..6d62cdea7 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java @@ -18,7 +18,7 @@ * @see #of(Supplier, Supplier) * @see #createHashed() */ -public final class CompositeStringMultiTrie extends StringMultiTrie, Root> { +public final class CompositeStringMultiTrie implements StringMultiTrie, Root> { private final Root root; private final View view = new View(); diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java index 8385bf0e3..e4214de7f 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java @@ -25,14 +25,14 @@ * @param the type of values * @param the type of branch nodes */ -public abstract class StringMultiTrie +public interface StringMultiTrie < V, B extends MutableMapNode.Branch & MutableCharacterNode, R extends MutableMapNode & MutableCharacterNode > - implements MutableMultiTrie { - protected static Optional tryToggleCase(char c) { + extends MutableMultiTrie { + static Optional tryToggleCase(char c) { if (Character.isUpperCase(c)) { return Optional.of(Character.toLowerCase(c)); } else if (Character.isLowerCase(c)) { @@ -42,21 +42,21 @@ protected static Optional tryToggleCase(char c) { } } - private static final String PREFIX = "prefix"; - private static final String STRING = "string"; - private static final String VALUE = "value"; + String PREFIX = "prefix"; + String STRING = "string"; + String VALUE = "value"; @Override - public abstract R getRoot(); + R getRoot(); @Override - public abstract View view(); + View view(); - public MutableCharacterNode get(String prefix) { + default MutableCharacterNode get(String prefix) { return this.getImpl(prefix, MutableCharacterNode::next); } - public MutableCharacterNode getIgnoreCase(String prefix) { + default MutableCharacterNode getIgnoreCase(String prefix) { return this.getImpl(prefix, MutableCharacterNode::nextIgnoreCase); } @@ -73,7 +73,7 @@ private MutableCharacterNode getImpl( return node; } - public MutableCharacterNode put(String string, V value) { + default MutableCharacterNode put(String string, V value) { Utils.requireNonNull(string, STRING); Utils.requireNonNull(value, VALUE); @@ -97,7 +97,7 @@ public MutableCharacterNode put(String string, V value) { return branch; } - public boolean remove(String string, V value) { + default boolean remove(String string, V value) { Utils.requireNonNull(string, STRING); Utils.requireNonNull(value, VALUE); @@ -113,7 +113,7 @@ public boolean remove(String string, V value) { return node.removeLeaf(value); } - public boolean removeAll(String string) { + default boolean removeAll(String string) { Utils.requireNonNull(string, STRING); MutableMapNode node = this.getRoot(); @@ -128,7 +128,7 @@ public boolean removeAll(String string) { return node.clearLeaves(); } - public interface CharacterNode extends MultiTrie.Node { + interface CharacterNode extends MultiTrie.Node { @Override CharacterNode next(Character key); @@ -146,7 +146,7 @@ default CharacterNode nextIgnoreCase(Character key) { CharacterNode previous(int steps); } - public interface MutableCharacterNode + interface MutableCharacterNode & MutableCharacterNode> extends CharacterNode, MutableMultiTrie.Node { @Override @@ -170,7 +170,7 @@ default MutableCharacterNode nextIgnoreCase(Character key) { } } - public abstract static class View + abstract class View < V, B extends MutableMapNode.Branch & MutableCharacterNode, From 319777a9b17f7cc819230f39c9c99c7d7d46686e Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 19 Nov 2025 09:11:54 -0800 Subject: [PATCH 055/124] don't require MutableMapNodes in StringMultiTrie --- .../gui/element/menu_bar/SearchMenusMenu.java | 6 +- .../multi_trie/CompositeStringMultiTrie.java | 38 +++--- .../util/multi_trie/StringMultiTrie.java | 117 ++++++++---------- .../CompositeStringMultiTrieTest.java | 12 +- 4 files changed, 77 insertions(+), 96 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java index 874b33e59..99bc770ce 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java @@ -165,7 +165,7 @@ JMenuItem getItem() { } private class ResultManager { - StringMultiTrie.@Nullable View resultTrie; + StringMultiTrie.@Nullable View resultTrie; @Nullable CurrentResults currentResults; @@ -225,7 +225,7 @@ UpdateOutcome initializeCurrentResults(String searchTerm) { } } - StringMultiTrie.View getResultTrie() { + StringMultiTrie.View getResultTrie() { if (this.resultTrie == null) { this.resultTrie = this.buildResultTrie(); } @@ -248,7 +248,7 @@ void clearCurrent() { } } - StringMultiTrie.View buildResultTrie() { + StringMultiTrie.View buildResultTrie() { final CompositeStringMultiTrie elementsBuilder = CompositeStringMultiTrie.createHashed(); SearchMenusMenu.this.gui.getMenuBar() .streamMenus() diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java index 6d62cdea7..397fdced2 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java @@ -1,8 +1,6 @@ package org.quiltmc.enigma.util.multi_trie; import org.jspecify.annotations.NonNull; -import org.quiltmc.enigma.util.multi_trie.CompositeStringMultiTrie.Branch; -import org.quiltmc.enigma.util.multi_trie.CompositeStringMultiTrie.Root; import java.util.Collection; import java.util.HashMap; @@ -18,7 +16,7 @@ * @see #of(Supplier, Supplier) * @see #createHashed() */ -public final class CompositeStringMultiTrie implements StringMultiTrie, Root> { +public final class CompositeStringMultiTrie implements StringMultiTrie { private final Root root; private final View view = new View(); @@ -68,13 +66,13 @@ public Root getRoot() { } @Override - public StringMultiTrie.View, Root> view() { + public StringMultiTrie.View view() { return this.view; } public static final class Root extends MutableMapNode> - implements MutableCharacterNode> { + implements MutableCharacterNode { private final Collection leaves; private final Map> branches; @@ -129,9 +127,9 @@ public CharacterNode view() { public static final class Branch extends MutableMapNode.Branch> - implements MutableCharacterNode> { + implements MutableCharacterNode { private final MutableMapNode> parent; - private final MutableCharacterNode> previous; + private final MutableCharacterNode previous; private final Character key; private final int depth; @@ -142,13 +140,11 @@ public static final class Branch private final NodeView view = new NodeView<>(this); - private < - P extends MutableMapNode> - & MutableCharacterNode> - > Branch( - P parent, char key, - int depth, Collection leaves, Map> branches, - Factory branchFactory + private

    > & MutableCharacterNode> + Branch( + P parent, char key, + int depth, Collection leaves, Map> branches, + Factory branchFactory ) { // two references to the same instance because both its types are necessary this.parent = parent; @@ -163,13 +159,13 @@ > Branch( } @Override - public MutableCharacterNode> previous() { + public MutableCharacterNode previous() { return this.previous; } @Override - public MutableCharacterNode> previous(int steps) { - return MultiTrie.Node.>> + public MutableCharacterNode previous(int steps) { + return MultiTrie.Node.> previous(this, steps, MutableCharacterNode::previous); } @@ -219,7 +215,7 @@ private record Factory( ) { < P extends MutableMapNode> - & MutableCharacterNode> + & MutableCharacterNode > CompositeStringMultiTrie.Branch create(char key, P parent) { return new CompositeStringMultiTrie.Branch<>( parent, key, parent.getDepth() + 1, this.leavesFactory.get(), this.branchesFactory.get(), @@ -229,7 +225,7 @@ > CompositeStringMultiTrie.Branch create(char key, P parent) { } } - private class View extends StringMultiTrie.View, Root> { + private class View extends StringMultiTrie.View.AbstractView { @Override protected CompositeStringMultiTrie getViewed() { return CompositeStringMultiTrie.this; @@ -237,9 +233,9 @@ protected CompositeStringMultiTrie getViewed() { } private static final class NodeView extends Node.View implements CharacterNode { - final MutableCharacterNode> viewed; + final MutableCharacterNode viewed; - NodeView(MutableCharacterNode> viewed) { + NodeView(MutableCharacterNode viewed) { this.viewed = viewed; } diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java index e4214de7f..78446953f 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java @@ -1,7 +1,6 @@ package org.quiltmc.enigma.util.multi_trie; import org.quiltmc.enigma.util.Utils; -import org.quiltmc.enigma.util.multi_trie.StringMultiTrie.MutableCharacterNode; import java.util.Optional; import java.util.function.BiFunction; @@ -19,19 +18,12 @@ *

  • {@link CharacterNode#nextIgnoreCase(Character)} * * - *

    {@linkplain #view() Views} also provide {@link View#get(String) get} and - * {@link View#getIgnoreCase(String) getIgnoreCase} methods. + *

    {@linkplain #view() Views} also provide {@link View.AbstractView#get(String) get} and + * {@link View.AbstractView#getIgnoreCase(String) getIgnoreCase} methods. * * @param the type of values - * @param the type of branch nodes */ -public interface StringMultiTrie - < - V, - B extends MutableMapNode.Branch & MutableCharacterNode, - R extends MutableMapNode & MutableCharacterNode - > - extends MutableMultiTrie { +public interface StringMultiTrie extends MutableMultiTrie { static Optional tryToggleCase(char c) { if (Character.isUpperCase(c)) { return Optional.of(Character.toLowerCase(c)); @@ -47,25 +39,25 @@ static Optional tryToggleCase(char c) { String VALUE = "value"; @Override - R getRoot(); + MutableCharacterNode getRoot(); @Override - View view(); + View view(); - default MutableCharacterNode get(String prefix) { + default MutableCharacterNode get(String prefix) { return this.getImpl(prefix, MutableCharacterNode::next); } - default MutableCharacterNode getIgnoreCase(String prefix) { + default MutableCharacterNode getIgnoreCase(String prefix) { return this.getImpl(prefix, MutableCharacterNode::nextIgnoreCase); } - private MutableCharacterNode getImpl( - String prefix, BiFunction, Character, MutableCharacterNode> next + private MutableCharacterNode getImpl( + String prefix, BiFunction, Character, MutableCharacterNode> next ) { Utils.requireNonNull(prefix, PREFIX); - MutableCharacterNode node = this.getRoot(); + MutableCharacterNode node = this.getRoot(); for (int i = 0; i < prefix.length(); i++) { node = next.apply(node, prefix.charAt(i)); } @@ -73,39 +65,29 @@ private MutableCharacterNode getImpl( return node; } - default MutableCharacterNode put(String string, V value) { + default MutableCharacterNode put(String string, V value) { Utils.requireNonNull(string, STRING); Utils.requireNonNull(value, VALUE); - final R root = this.getRoot(); - if (string.isEmpty()) { - root.put(value); - - return root; - } - - // Don't use next, initially creating orphans, because we always put a value, - // so all orphans would be adopted anyway. - B branch = root.getBranches().computeIfAbsent(string.charAt(0), root::createBranch); - for (int i = 1; i < string.length(); i++) { - final B parent = branch; - branch = branch.getBranches().computeIfAbsent(string.charAt(i), parent::createBranch); + MutableCharacterNode node = this.getRoot(); + for (int i = 0; i < string.length(); i++) { + node = node.next(string.charAt(i)); } - branch.put(value); + node.put(value); - return branch; + return node; } default boolean remove(String string, V value) { Utils.requireNonNull(string, STRING); Utils.requireNonNull(value, VALUE); - MutableMapNode node = this.getRoot(); + MutableCharacterNode node = this.getRoot(); for (int i = 0; i < string.length(); i++) { - node = node.nextBranch(string.charAt(i)); + node = node.next(string.charAt(i)); - if (node == null) { + if (node.isEmpty()) { return false; } } @@ -116,11 +98,11 @@ default boolean remove(String string, V value) { default boolean removeAll(String string) { Utils.requireNonNull(string, STRING); - MutableMapNode node = this.getRoot(); + MutableCharacterNode node = this.getRoot(); for (int i = 0; i < string.length(); i++) { - node = node.nextBranch(string.charAt(i)); + node = node.next(string.charAt(i)); - if (node == null) { + if (node.isEmpty()) { return false; } } @@ -146,50 +128,53 @@ default CharacterNode nextIgnoreCase(Character key) { CharacterNode previous(int steps); } - interface MutableCharacterNode - & MutableCharacterNode> - extends CharacterNode, MutableMultiTrie.Node { + interface MutableCharacterNode extends CharacterNode, MutableMultiTrie.Node { @Override - B next(Character key); + MutableCharacterNode next(Character key); @Override - MutableCharacterNode previous(); + MutableCharacterNode previous(); @Override - MutableCharacterNode previous(int steps); + MutableCharacterNode previous(int steps); @Override CharacterNode view(); @Override - default MutableCharacterNode nextIgnoreCase(Character key) { - final MutableCharacterNode next = this.next(key); + default MutableCharacterNode nextIgnoreCase(Character key) { + final MutableCharacterNode next = this.next(key); return next.isEmpty() - ? tryToggleCase(key).>map(this::next).orElse(next) + ? tryToggleCase(key).map(this::next).orElse(next) : next; } } - abstract class View - < - V, - B extends MutableMapNode.Branch & MutableCharacterNode, - R extends MutableMapNode & MutableCharacterNode - > - implements MultiTrie { + interface View extends MultiTrie { @Override - public CharacterNode getRoot() { - return this.getViewed().getRoot().view(); - } + CharacterNode getRoot(); - public CharacterNode get(String prefix) { - return this.getViewed().get(prefix).view(); - } + CharacterNode get(String prefix); - public CharacterNode getIgnoreCase(String prefix) { - return this.getViewed().getIgnoreCase(prefix).view(); - } + CharacterNode getIgnoreCase(String prefix); + + abstract class AbstractView implements View { + @Override + public CharacterNode getRoot() { + return this.getViewed().getRoot().view(); + } + + @Override + public CharacterNode get(String prefix) { + return this.getViewed().get(prefix).view(); + } + + @Override + public CharacterNode getIgnoreCase(String prefix) { + return this.getViewed().getIgnoreCase(prefix).view(); + } - protected abstract StringMultiTrie getViewed(); + protected abstract StringMultiTrie getViewed(); + } } } diff --git a/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java b/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java index c4aae5e20..cc80a92a0 100644 --- a/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java +++ b/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java @@ -53,7 +53,7 @@ void testPutKeyByKeyFromRoot() { final CompositeStringMultiTrie trie = CompositeStringMultiTrie.createHashed(); for (int depth = 0; depth < KEY_BY_KEY_SUBJECT.length(); depth++) { - MutableCharacterNode node = trie.getRoot(); + MutableCharacterNode node = trie.getRoot(); for (int iKey = 0; iKey <= depth; iKey++) { node = node.next(KEY_BY_KEY_SUBJECT.charAt(iKey)); } @@ -72,7 +72,7 @@ void testPutKeyByKeyFromStems() { final CompositeStringMultiTrie trie = CompositeStringMultiTrie.createHashed(); for (int depth = KEY_BY_KEY_SUBJECT.length() - 1; depth >= 0; depth--) { - MutableCharacterNode node = trie.getRoot(); + MutableCharacterNode node = trie.getRoot(); for (int iKey = 0; iKey <= depth; iKey++) { node = node.next(KEY_BY_KEY_SUBJECT.charAt(iKey)); } @@ -85,7 +85,7 @@ void testPutKeyByKeyFromStems() { } } - private static void assertOneLeaf(MutableCharacterNode node) { + private static void assertOneLeaf(MutableCharacterNode node) { assertEquals( 1, node.streamLeaves().count(), () -> "Expected node to have only one leaf, but had the following: " + node.streamLeaves().toList() @@ -242,7 +242,7 @@ void testNextIgnoreCase() { trie.put(IGNORE_CASE_SUBJECT, IGNORE_CASE_SUBJECT); final String invertedSubject = caseInverted(IGNORE_CASE_SUBJECT); - MutableCharacterNode node = trie.getRoot(); + MutableCharacterNode node = trie.getRoot(); for (int i = 0; i < invertedSubject.length(); i++) { node = node.nextIgnoreCase(invertedSubject.charAt(i)); @@ -252,7 +252,7 @@ void testNextIgnoreCase() { assertOneLeaf(node); } - private static void assertOneValue(MutableCharacterNode node) { + private static void assertOneValue(MutableCharacterNode node) { assertEquals( 1, node.getSize(), "Expected node to have only one value, but had the following: " + node.streamValues().toList() @@ -267,7 +267,7 @@ void testGetIgnoreCase() { final String invertedSubject = caseInverted(IGNORE_CASE_SUBJECT); - final MutableCharacterNode node = trie.getIgnoreCase(invertedSubject); + final MutableCharacterNode node = trie.getIgnoreCase(invertedSubject); assertOneValue(node); From b640b2dbc3c28fe4e9f13693093354477e5e157e Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 19 Nov 2025 09:31:09 -0800 Subject: [PATCH 056/124] separate StringMultiTrie and MutableStringMultiTrie interfaces --- .../gui/element/menu_bar/SearchMenusMenu.java | 7 +- .../multi_trie/CompositeStringMultiTrie.java | 12 +- .../multi_trie/MutableStringMultiTrie.java | 148 ++++++++++++++++++ .../util/multi_trie/StringMultiTrie.java | 132 ++-------------- .../CompositeStringMultiTrieTest.java | 2 +- 5 files changed, 169 insertions(+), 132 deletions(-) create mode 100644 enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableStringMultiTrie.java diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java index 99bc770ce..effee9f2c 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java @@ -165,7 +165,8 @@ JMenuItem getItem() { } private class ResultManager { - StringMultiTrie.@Nullable View resultTrie; + @Nullable + StringMultiTrie resultTrie; @Nullable CurrentResults currentResults; @@ -225,7 +226,7 @@ UpdateOutcome initializeCurrentResults(String searchTerm) { } } - StringMultiTrie.View getResultTrie() { + StringMultiTrie getResultTrie() { if (this.resultTrie == null) { this.resultTrie = this.buildResultTrie(); } @@ -248,7 +249,7 @@ void clearCurrent() { } } - StringMultiTrie.View buildResultTrie() { + StringMultiTrie buildResultTrie() { final CompositeStringMultiTrie elementsBuilder = CompositeStringMultiTrie.createHashed(); SearchMenusMenu.this.gui.getMenuBar() .streamMenus() diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java index 397fdced2..d4ed568a2 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java @@ -16,7 +16,7 @@ * @see #of(Supplier, Supplier) * @see #createHashed() */ -public final class CompositeStringMultiTrie implements StringMultiTrie { +public final class CompositeStringMultiTrie implements MutableStringMultiTrie { private final Root root; private final View view = new View(); @@ -66,7 +66,7 @@ public Root getRoot() { } @Override - public StringMultiTrie.View view() { + public MutableStringMultiTrie.View view() { return this.view; } @@ -225,14 +225,16 @@ > CompositeStringMultiTrie.Branch create(char key, P parent) { } } - private class View extends StringMultiTrie.View.AbstractView { + private class View extends MutableStringMultiTrie.View.AbstractView { @Override protected CompositeStringMultiTrie getViewed() { return CompositeStringMultiTrie.this; } } - private static final class NodeView extends Node.View implements CharacterNode { + private static final class NodeView + extends MutableMultiTrie.Node.View + implements CharacterNode { final MutableCharacterNode viewed; NodeView(MutableCharacterNode viewed) { @@ -256,7 +258,7 @@ public CharacterNode previous(int steps) { } @Override - protected Node getViewed() { + protected MutableMultiTrie.Node getViewed() { return this.viewed; } } diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableStringMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableStringMultiTrie.java new file mode 100644 index 000000000..3f735cf53 --- /dev/null +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableStringMultiTrie.java @@ -0,0 +1,148 @@ +package org.quiltmc.enigma.util.multi_trie; + +import org.quiltmc.enigma.util.Utils; + +import java.util.Optional; + +/** + * A {@link MutableMultiTrie} that associates sequences of characters with values of type {@code V}. + * + *

    Adds {@link String}/{@link Character}-specific convenience methods for accessing its contents: + *

      + *
    • {@link #get(String)} + *
    • {@link #getIgnoreCase(String)} + *
    • {@link #put(String, Object)} + *
    • {@link #remove(String, Object)} + *
    • {@link #removeAll(String)} + *
    • {@link CharacterNode#nextIgnoreCase(Character)} + *
    + * + *

    {@linkplain #view() Views} also provide {@link View.AbstractView#get(String) get} and + * {@link View.AbstractView#getIgnoreCase(String) getIgnoreCase} methods. + * + * @param the type of values + */ +public interface MutableStringMultiTrie extends MutableMultiTrie, StringMultiTrie { + static Optional tryToggleCase(char c) { + if (Character.isUpperCase(c)) { + return Optional.of(Character.toLowerCase(c)); + } else if (Character.isLowerCase(c)) { + return Optional.of(Character.toUpperCase(c)); + } else { + return Optional.empty(); + } + } + + String STRING = "string"; + String VALUE = "value"; + + @Override + MutableCharacterNode getRoot(); + + @Override + default MutableCharacterNode get(String prefix) { + return StringMultiTrie.get(prefix, this.getRoot(), MutableCharacterNode::next); + } + + @Override + default MutableCharacterNode getIgnoreCase(String prefix) { + return StringMultiTrie.get(prefix, this.getRoot(), MutableCharacterNode::nextIgnoreCase); + } + + @Override + View view(); + + default MutableCharacterNode put(String string, V value) { + Utils.requireNonNull(string, STRING); + Utils.requireNonNull(value, VALUE); + + MutableCharacterNode node = this.getRoot(); + for (int i = 0; i < string.length(); i++) { + node = node.next(string.charAt(i)); + } + + node.put(value); + + return node; + } + + default boolean remove(String string, V value) { + Utils.requireNonNull(string, STRING); + Utils.requireNonNull(value, VALUE); + + MutableCharacterNode node = this.getRoot(); + for (int i = 0; i < string.length(); i++) { + node = node.next(string.charAt(i)); + + if (node.isEmpty()) { + return false; + } + } + + return node.removeLeaf(value); + } + + default boolean removeAll(String string) { + Utils.requireNonNull(string, STRING); + + MutableCharacterNode node = this.getRoot(); + for (int i = 0; i < string.length(); i++) { + node = node.next(string.charAt(i)); + + if (node.isEmpty()) { + return false; + } + } + + return node.clearLeaves(); + } + + interface MutableCharacterNode extends CharacterNode, MutableMultiTrie.Node { + @Override + MutableCharacterNode next(Character key); + + @Override + MutableCharacterNode previous(); + + @Override + MutableCharacterNode previous(int steps); + + @Override + CharacterNode view(); + + @Override + default MutableCharacterNode nextIgnoreCase(Character key) { + final MutableCharacterNode next = this.next(key); + return next.isEmpty() + ? tryToggleCase(key).map(this::next).orElse(next) + : next; + } + } + + interface View extends StringMultiTrie { + @Override + CharacterNode get(String prefix); + + @Override + CharacterNode getIgnoreCase(String prefix); + + abstract class AbstractView implements View { + @Override + public CharacterNode getRoot() { + return this.getViewed().getRoot().view(); + } + + @Override + public CharacterNode get(String prefix) { + return this.getViewed().get(prefix).view(); + } + + @Override + public CharacterNode getIgnoreCase(String prefix) { + return this.getViewed().getIgnoreCase(prefix).view(); + } + + protected abstract MutableStringMultiTrie getViewed(); + } + } +} diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java index 78446953f..f307be9e2 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java @@ -6,24 +6,18 @@ import java.util.function.BiFunction; /** - * A {@link MutableMultiTrie} that associates sequences of characters with values of type {@code V}. + * A {@link MultiTrie} that associates sequences of characters with values of type {@code V}. * *

    Adds {@link String}/{@link Character}-specific convenience methods for accessing its contents: *

      *
    • {@link #get(String)} *
    • {@link #getIgnoreCase(String)} - *
    • {@link #put(String, Object)} - *
    • {@link #remove(String, Object)} - *
    • {@link #removeAll(String)} *
    • {@link CharacterNode#nextIgnoreCase(Character)} *
    * - *

    {@linkplain #view() Views} also provide {@link View.AbstractView#get(String) get} and - * {@link View.AbstractView#getIgnoreCase(String) getIgnoreCase} methods. - * * @param the type of values */ -public interface StringMultiTrie extends MutableMultiTrie { +public interface StringMultiTrie extends MultiTrie { static Optional tryToggleCase(char c) { if (Character.isUpperCase(c)) { return Optional.of(Character.toLowerCase(c)); @@ -34,30 +28,10 @@ static Optional tryToggleCase(char c) { } } - String PREFIX = "prefix"; - String STRING = "string"; - String VALUE = "value"; - - @Override - MutableCharacterNode getRoot(); - - @Override - View view(); - - default MutableCharacterNode get(String prefix) { - return this.getImpl(prefix, MutableCharacterNode::next); - } + static > N get(String prefix, N root, BiFunction next) { + Utils.requireNonNull(prefix, "prefix"); - default MutableCharacterNode getIgnoreCase(String prefix) { - return this.getImpl(prefix, MutableCharacterNode::nextIgnoreCase); - } - - private MutableCharacterNode getImpl( - String prefix, BiFunction, Character, MutableCharacterNode> next - ) { - Utils.requireNonNull(prefix, PREFIX); - - MutableCharacterNode node = this.getRoot(); + N node = root; for (int i = 0; i < prefix.length(); i++) { node = next.apply(node, prefix.charAt(i)); } @@ -65,50 +39,12 @@ private MutableCharacterNode getImpl( return node; } - default MutableCharacterNode put(String string, V value) { - Utils.requireNonNull(string, STRING); - Utils.requireNonNull(value, VALUE); - - MutableCharacterNode node = this.getRoot(); - for (int i = 0; i < string.length(); i++) { - node = node.next(string.charAt(i)); - } - - node.put(value); - - return node; - } - - default boolean remove(String string, V value) { - Utils.requireNonNull(string, STRING); - Utils.requireNonNull(value, VALUE); - - MutableCharacterNode node = this.getRoot(); - for (int i = 0; i < string.length(); i++) { - node = node.next(string.charAt(i)); - - if (node.isEmpty()) { - return false; - } - } - - return node.removeLeaf(value); - } - - default boolean removeAll(String string) { - Utils.requireNonNull(string, STRING); - - MutableCharacterNode node = this.getRoot(); - for (int i = 0; i < string.length(); i++) { - node = node.next(string.charAt(i)); + @Override + CharacterNode getRoot(); - if (node.isEmpty()) { - return false; - } - } + CharacterNode get(String prefix); - return node.clearLeaves(); - } + CharacterNode getIgnoreCase(String prefix); interface CharacterNode extends MultiTrie.Node { @Override @@ -127,54 +63,4 @@ default CharacterNode nextIgnoreCase(Character key) { @Override CharacterNode previous(int steps); } - - interface MutableCharacterNode extends CharacterNode, MutableMultiTrie.Node { - @Override - MutableCharacterNode next(Character key); - - @Override - MutableCharacterNode previous(); - - @Override - MutableCharacterNode previous(int steps); - - @Override - CharacterNode view(); - - @Override - default MutableCharacterNode nextIgnoreCase(Character key) { - final MutableCharacterNode next = this.next(key); - return next.isEmpty() - ? tryToggleCase(key).map(this::next).orElse(next) - : next; - } - } - - interface View extends MultiTrie { - @Override - CharacterNode getRoot(); - - CharacterNode get(String prefix); - - CharacterNode getIgnoreCase(String prefix); - - abstract class AbstractView implements View { - @Override - public CharacterNode getRoot() { - return this.getViewed().getRoot().view(); - } - - @Override - public CharacterNode get(String prefix) { - return this.getViewed().get(prefix).view(); - } - - @Override - public CharacterNode getIgnoreCase(String prefix) { - return this.getViewed().getIgnoreCase(prefix).view(); - } - - protected abstract StringMultiTrie getViewed(); - } - } } diff --git a/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java b/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java index cc80a92a0..104178cd2 100644 --- a/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java +++ b/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java @@ -5,7 +5,7 @@ import org.junit.jupiter.api.Test; import org.opentest4j.AssertionFailedError; import org.quiltmc.enigma.util.multi_trie.MultiTrie.Node; -import org.quiltmc.enigma.util.multi_trie.StringMultiTrie.MutableCharacterNode; +import org.quiltmc.enigma.util.multi_trie.MutableStringMultiTrie.MutableCharacterNode; import java.util.Collection; import java.util.List; From b1d88f33648a5661de0a9661e97adc4155551633 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 19 Nov 2025 10:00:15 -0800 Subject: [PATCH 057/124] update [Mutable]StringMultiTrie javadocs rename [Mutable]CharacterNode -> Node eliminate MutableStringMultiTrie.View (but keep AbstractView) --- .../gui/element/menu_bar/SearchMenusMenu.java | 8 +- .../multi_trie/CompositeStringMultiTrie.java | 55 ++++++-------- .../multi_trie/MutableStringMultiTrie.java | 76 ++++++++----------- .../util/multi_trie/StringMultiTrie.java | 50 ++++++------ .../CompositeStringMultiTrieTest.java | 21 +++-- 5 files changed, 94 insertions(+), 116 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java index effee9f2c..86138b035 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java @@ -8,7 +8,7 @@ import org.quiltmc.enigma.util.I18n; import org.quiltmc.enigma.util.multi_trie.CompositeStringMultiTrie; import org.quiltmc.enigma.util.multi_trie.StringMultiTrie; -import org.quiltmc.enigma.util.multi_trie.StringMultiTrie.CharacterNode; +import org.quiltmc.enigma.util.multi_trie.StringMultiTrie.Node; import javax.swing.JMenuItem; import javax.swing.MenuElement; @@ -180,7 +180,7 @@ UpdateOutcome updateResultItems(String searchTerm) { if (this.currentResults.searchTerm.length() == searchTerm.length()) { return UpdateOutcome.SAME_RESULTS; } else { - CharacterNode resultNode = this.currentResults.results; + Node resultNode = this.currentResults.results; for (int i = this.currentResults.searchTerm.length(); i < searchTerm.length(); i++) { resultNode = resultNode.nextIgnoreCase(searchTerm.charAt(i)); } @@ -213,7 +213,7 @@ UpdateOutcome updateResultItems(String searchTerm) { } UpdateOutcome initializeCurrentResults(String searchTerm) { - final CharacterNode results = this.getResultTrie().getIgnoreCase(searchTerm); + final Node results = this.getResultTrie().getIgnoreCase(searchTerm); if (results.isEmpty()) { this.clearCurrent(); @@ -268,7 +268,7 @@ StringMultiTrie buildResultTrie() { return elementsBuilder.view(); } - record CurrentResults(CharacterNode results, String searchTerm) { } + record CurrentResults(Node results, String searchTerm) { } enum UpdateOutcome { NO_RESULTS, SAME_RESULTS, DIFFERENT_RESULTS diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java index d4ed568a2..f2911943f 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java @@ -1,7 +1,5 @@ package org.quiltmc.enigma.util.multi_trie; -import org.jspecify.annotations.NonNull; - import java.util.Collection; import java.util.HashMap; import java.util.HashSet; @@ -66,13 +64,13 @@ public Root getRoot() { } @Override - public MutableStringMultiTrie.View view() { + public StringMultiTrie view() { return this.view; } public static final class Root extends MutableMapNode> - implements MutableCharacterNode { + implements Node { private final Collection leaves; private final Map> branches; @@ -120,16 +118,15 @@ protected Map> getBranches() { } @Override - public CharacterNode view() { + public StringMultiTrie.Node view() { return this.view; } } - public static final class Branch - extends MutableMapNode.Branch> - implements MutableCharacterNode { + public static final class Branch extends MutableMapNode.Branch> implements Node { private final MutableMapNode> parent; - private final MutableCharacterNode previous; + private final Node previous; + private final Character key; private final int depth; @@ -140,11 +137,10 @@ public static final class Branch private final NodeView view = new NodeView<>(this); - private

    > & MutableCharacterNode> - Branch( - P parent, char key, - int depth, Collection leaves, Map> branches, - Factory branchFactory + private

    > & Node> Branch( + P parent, char key, + int depth, Collection leaves, Map> branches, + Factory branchFactory ) { // two references to the same instance because both its types are necessary this.parent = parent; @@ -159,14 +155,14 @@ public static final class Branch } @Override - public MutableCharacterNode previous() { + public Node previous() { return this.previous; } @Override - public MutableCharacterNode previous(int steps) { - return MultiTrie.Node.> - previous(this, steps, MutableCharacterNode::previous); + public Node previous(int steps) { + return MultiTrie.Node.> + previous(this, steps, Node::previous); } @Override @@ -205,7 +201,7 @@ protected Map> getBranches() { } @Override - public CharacterNode view() { + public StringMultiTrie.Node view() { return this.view; } @@ -213,10 +209,8 @@ private record Factory( Supplier> leavesFactory, Supplier>> branchesFactory ) { - < - P extends MutableMapNode> - & MutableCharacterNode - > CompositeStringMultiTrie.Branch create(char key, P parent) { +

    > & Node> + CompositeStringMultiTrie.Branch create(char key, P parent) { return new CompositeStringMultiTrie.Branch<>( parent, key, parent.getDepth() + 1, this.leavesFactory.get(), this.branchesFactory.get(), this @@ -225,7 +219,7 @@ > CompositeStringMultiTrie.Branch create(char key, P parent) { } } - private class View extends MutableStringMultiTrie.View.AbstractView { + private class View extends AbstractView { @Override protected CompositeStringMultiTrie getViewed() { return CompositeStringMultiTrie.this; @@ -234,26 +228,25 @@ protected CompositeStringMultiTrie getViewed() { private static final class NodeView extends MutableMultiTrie.Node.View - implements CharacterNode { - final MutableCharacterNode viewed; + implements StringMultiTrie.Node { + final Node viewed; - NodeView(MutableCharacterNode viewed) { + NodeView(Node viewed) { this.viewed = viewed; } - @NonNull @Override - public CharacterNode next(Character key) { + public StringMultiTrie.Node next(Character key) { return this.viewed.next(key).view(); } @Override - public CharacterNode previous() { + public StringMultiTrie.Node previous() { return this.viewed.previous().view(); } @Override - public CharacterNode previous(int steps) { + public StringMultiTrie.Node previous(int steps) { return this.viewed.previous(steps).view(); } diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableStringMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableStringMultiTrie.java index 3f735cf53..f130e06c2 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableStringMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableStringMultiTrie.java @@ -5,20 +5,16 @@ import java.util.Optional; /** - * A {@link MutableMultiTrie} that associates sequences of characters with values of type {@code V}. + * A {@linkplain MutableMultiTrie mutable} {@link StringMultiTrie}. * - *

    Adds {@link String}/{@link Character}-specific convenience methods for accessing its contents: + *

    Adds {@link String}-specific mutation methods: *

      - *
    • {@link #get(String)} - *
    • {@link #getIgnoreCase(String)} *
    • {@link #put(String, Object)} *
    • {@link #remove(String, Object)} *
    • {@link #removeAll(String)} - *
    • {@link CharacterNode#nextIgnoreCase(Character)} *
    * - *

    {@linkplain #view() Views} also provide {@link View.AbstractView#get(String) get} and - * {@link View.AbstractView#getIgnoreCase(String) getIgnoreCase} methods. + *

    {@linkplain #view() Views} are also {@link StringMultiTrie}s. * * @param the type of values */ @@ -37,26 +33,26 @@ static Optional tryToggleCase(char c) { String VALUE = "value"; @Override - MutableCharacterNode getRoot(); + Node getRoot(); @Override - default MutableCharacterNode get(String prefix) { - return StringMultiTrie.get(prefix, this.getRoot(), MutableCharacterNode::next); + default Node get(String prefix) { + return StringMultiTrie.get(prefix, this.getRoot(), Node::next); } @Override - default MutableCharacterNode getIgnoreCase(String prefix) { - return StringMultiTrie.get(prefix, this.getRoot(), MutableCharacterNode::nextIgnoreCase); + default Node getIgnoreCase(String prefix) { + return StringMultiTrie.get(prefix, this.getRoot(), Node::nextIgnoreCase); } @Override - View view(); + StringMultiTrie view(); - default MutableCharacterNode put(String string, V value) { + default Node put(String string, V value) { Utils.requireNonNull(string, STRING); Utils.requireNonNull(value, VALUE); - MutableCharacterNode node = this.getRoot(); + Node node = this.getRoot(); for (int i = 0; i < string.length(); i++) { node = node.next(string.charAt(i)); } @@ -70,7 +66,7 @@ default boolean remove(String string, V value) { Utils.requireNonNull(string, STRING); Utils.requireNonNull(value, VALUE); - MutableCharacterNode node = this.getRoot(); + Node node = this.getRoot(); for (int i = 0; i < string.length(); i++) { node = node.next(string.charAt(i)); @@ -85,7 +81,7 @@ default boolean remove(String string, V value) { default boolean removeAll(String string) { Utils.requireNonNull(string, STRING); - MutableCharacterNode node = this.getRoot(); + Node node = this.getRoot(); for (int i = 0; i < string.length(); i++) { node = node.next(string.charAt(i)); @@ -97,52 +93,44 @@ default boolean removeAll(String string) { return node.clearLeaves(); } - interface MutableCharacterNode extends CharacterNode, MutableMultiTrie.Node { + interface Node extends StringMultiTrie.Node, MutableMultiTrie.Node { @Override - MutableCharacterNode next(Character key); + Node next(Character key); @Override - MutableCharacterNode previous(); + Node previous(); @Override - MutableCharacterNode previous(int steps); + Node previous(int steps); @Override - CharacterNode view(); + StringMultiTrie.Node view(); @Override - default MutableCharacterNode nextIgnoreCase(Character key) { - final MutableCharacterNode next = this.next(key); + default Node nextIgnoreCase(Character key) { + final Node next = this.next(key); return next.isEmpty() ? tryToggleCase(key).map(this::next).orElse(next) : next; } } - interface View extends StringMultiTrie { + abstract class AbstractView implements StringMultiTrie { @Override - CharacterNode get(String prefix); + public Node getRoot() { + return this.getViewed().getRoot().view(); + } @Override - CharacterNode getIgnoreCase(String prefix); - - abstract class AbstractView implements View { - @Override - public CharacterNode getRoot() { - return this.getViewed().getRoot().view(); - } - - @Override - public CharacterNode get(String prefix) { - return this.getViewed().get(prefix).view(); - } - - @Override - public CharacterNode getIgnoreCase(String prefix) { - return this.getViewed().getIgnoreCase(prefix).view(); - } + public Node get(String prefix) { + return this.getViewed().get(prefix).view(); + } - protected abstract MutableStringMultiTrie getViewed(); + @Override + public Node getIgnoreCase(String prefix) { + return this.getViewed().getIgnoreCase(prefix).view(); } + + protected abstract MutableStringMultiTrie getViewed(); } } diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java index f307be9e2..c2f3837a2 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java @@ -2,33 +2,22 @@ import org.quiltmc.enigma.util.Utils; -import java.util.Optional; import java.util.function.BiFunction; /** * A {@link MultiTrie} that associates sequences of characters with values of type {@code V}. * - *

    Adds {@link String}/{@link Character}-specific convenience methods for accessing its contents: + *

    Adds {@link String}/{@link Character}-specific access methods: *

      *
    • {@link #get(String)} *
    • {@link #getIgnoreCase(String)} - *
    • {@link CharacterNode#nextIgnoreCase(Character)} + *
    • {@link Node#nextIgnoreCase(Character)} *
    * * @param the type of values */ public interface StringMultiTrie extends MultiTrie { - static Optional tryToggleCase(char c) { - if (Character.isUpperCase(c)) { - return Optional.of(Character.toLowerCase(c)); - } else if (Character.isLowerCase(c)) { - return Optional.of(Character.toUpperCase(c)); - } else { - return Optional.empty(); - } - } - - static > N get(String prefix, N root, BiFunction next) { + static > N get(String prefix, N root, BiFunction next) { Utils.requireNonNull(prefix, "prefix"); N node = root; @@ -40,27 +29,36 @@ static > N get(String prefix, N root, BiFunction getRoot(); + Node getRoot(); - CharacterNode get(String prefix); + Node get(String prefix); - CharacterNode getIgnoreCase(String prefix); + Node getIgnoreCase(String prefix); - interface CharacterNode extends MultiTrie.Node { + interface Node extends MultiTrie.Node { @Override - CharacterNode next(Character key); + Node next(Character key); - default CharacterNode nextIgnoreCase(Character key) { - final CharacterNode next = this.next(key); - return next.isEmpty() - ? tryToggleCase(key).map(this::next).orElse(next) - : next; + default Node nextIgnoreCase(Character key) { + final Node next = this.next(key); + if (next.isEmpty()) { + final char c = key; + if (Character.isUpperCase(c)) { + return this.next(Character.toLowerCase(c)); + } else if (Character.isLowerCase(c)) { + return this.next(Character.toUpperCase(c)); + } else { + return next; + } + } else { + return next; + } } @Override - CharacterNode previous(); + Node previous(); @Override - CharacterNode previous(int steps); + Node previous(int steps); } } diff --git a/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java b/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java index 104178cd2..e4fd3c046 100644 --- a/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java +++ b/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java @@ -4,8 +4,7 @@ import com.google.common.collect.ImmutableMultimap; import org.junit.jupiter.api.Test; import org.opentest4j.AssertionFailedError; -import org.quiltmc.enigma.util.multi_trie.MultiTrie.Node; -import org.quiltmc.enigma.util.multi_trie.MutableStringMultiTrie.MutableCharacterNode; +import org.quiltmc.enigma.util.multi_trie.MutableStringMultiTrie.Node; import java.util.Collection; import java.util.List; @@ -53,7 +52,7 @@ void testPutKeyByKeyFromRoot() { final CompositeStringMultiTrie trie = CompositeStringMultiTrie.createHashed(); for (int depth = 0; depth < KEY_BY_KEY_SUBJECT.length(); depth++) { - MutableCharacterNode node = trie.getRoot(); + Node node = trie.getRoot(); for (int iKey = 0; iKey <= depth; iKey++) { node = node.next(KEY_BY_KEY_SUBJECT.charAt(iKey)); } @@ -72,7 +71,7 @@ void testPutKeyByKeyFromStems() { final CompositeStringMultiTrie trie = CompositeStringMultiTrie.createHashed(); for (int depth = KEY_BY_KEY_SUBJECT.length() - 1; depth >= 0; depth--) { - MutableCharacterNode node = trie.getRoot(); + Node node = trie.getRoot(); for (int iKey = 0; iKey <= depth; iKey++) { node = node.next(KEY_BY_KEY_SUBJECT.charAt(iKey)); } @@ -85,7 +84,7 @@ void testPutKeyByKeyFromStems() { } } - private static void assertOneLeaf(MutableCharacterNode node) { + private static void assertOneLeaf(Node node) { assertEquals( 1, node.streamLeaves().count(), () -> "Expected node to have only one leaf, but had the following: " + node.streamLeaves().toList() @@ -105,7 +104,7 @@ void testPut() { final CompositeStringMultiTrie trie = Association.createAndPopulateTrie(); Association.BY_PREFIX.asMap().forEach((prefix, associations) -> { - final Node node = trie.get(prefix); + final MultiTrie.Node node = trie.get(prefix); assertUnorderedContentsForPrefix(prefix, VALUES, associations.stream(), node.streamValues()); @@ -128,7 +127,7 @@ void testPutMulti() { final CompositeStringMultiTrie trie = MultiAssociation.createAndPopulateTrie(); Association.BY_PREFIX.asMap().forEach((prefix, associations) -> { - final Node node = trie.get(prefix); + final MultiTrie.Node node = trie.get(prefix); assertUnorderedContentsForPrefix( prefix, VALUES, @@ -218,7 +217,7 @@ private static void assertEmpty(CompositeStringMultiTrie trie) { () -> "Expected trie to be empty, but had it contents: " + trie.getRoot().streamValues().toList() ); - final Map> rootChildren = trie.getRoot().getBranches(); + final Map> rootChildren = trie.getRoot().getBranches(); assertTrue( rootChildren.isEmpty(), () -> "Expected root's children to be pruned, but it had children: " + rootChildren @@ -242,7 +241,7 @@ void testNextIgnoreCase() { trie.put(IGNORE_CASE_SUBJECT, IGNORE_CASE_SUBJECT); final String invertedSubject = caseInverted(IGNORE_CASE_SUBJECT); - MutableCharacterNode node = trie.getRoot(); + Node node = trie.getRoot(); for (int i = 0; i < invertedSubject.length(); i++) { node = node.nextIgnoreCase(invertedSubject.charAt(i)); @@ -252,7 +251,7 @@ void testNextIgnoreCase() { assertOneLeaf(node); } - private static void assertOneValue(MutableCharacterNode node) { + private static void assertOneValue(Node node) { assertEquals( 1, node.getSize(), "Expected node to have only one value, but had the following: " + node.streamValues().toList() @@ -267,7 +266,7 @@ void testGetIgnoreCase() { final String invertedSubject = caseInverted(IGNORE_CASE_SUBJECT); - final MutableCharacterNode node = trie.getIgnoreCase(invertedSubject); + final Node node = trie.getIgnoreCase(invertedSubject); assertOneValue(node); From 9f739b63ebb9df492b7f16b3d53059139349d200 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 19 Nov 2025 10:27:38 -0800 Subject: [PATCH 058/124] reduce visibility of CompositeSTringMultiTrie.Root --- .../util/multi_trie/CompositeStringMultiTrie.java | 11 +++++++---- .../util/multi_trie/CompositeStringMultiTrieTest.java | 3 ++- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java index f2911943f..664700d40 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java @@ -1,5 +1,7 @@ package org.quiltmc.enigma.util.multi_trie; +import com.google.common.annotations.VisibleForTesting; + import java.util.Collection; import java.util.HashMap; import java.util.HashSet; @@ -59,7 +61,7 @@ private CompositeStringMultiTrie( } @Override - public Root getRoot() { + public Node getRoot() { return this.root; } @@ -68,7 +70,8 @@ public StringMultiTrie view() { return this.view; } - public static final class Root + @VisibleForTesting + static final class Root extends MutableMapNode> implements Node { private final Collection leaves; @@ -88,12 +91,12 @@ private Root( } @Override - public Root previous() { + public Node previous() { return this; } @Override - public Root previous(int steps) { + public Node previous(int steps) { return this; } diff --git a/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java b/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java index e4fd3c046..b84c6704d 100644 --- a/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java +++ b/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java @@ -217,7 +217,8 @@ private static void assertEmpty(CompositeStringMultiTrie trie) { () -> "Expected trie to be empty, but had it contents: " + trie.getRoot().streamValues().toList() ); - final Map> rootChildren = trie.getRoot().getBranches(); + final Map> rootChildren = + ((CompositeStringMultiTrie.Root) trie.getRoot()).getBranches(); assertTrue( rootChildren.isEmpty(), () -> "Expected root's children to be pruned, but it had children: " + rootChildren From 686a169068b6eee79ac8f827b517d1bc0d7e24e8 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 19 Nov 2025 10:56:15 -0800 Subject: [PATCH 059/124] rename MutableMapNode::tryAdopt -> adoptIfOrphan put pruned nodes back in orphans in case users hold references to pruned nodes --- .../multi_trie/CompositeStringMultiTrie.java | 5 ---- .../util/multi_trie/MutableMapNode.java | 30 +++++++++---------- 2 files changed, 14 insertions(+), 21 deletions(-) diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java index 664700d40..f787d53e1 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java @@ -183,11 +183,6 @@ protected Character getKey() { return this.key; } - @Override - protected CompositeStringMultiTrie.Branch getSelf() { - return this; - } - @Override protected CompositeStringMultiTrie.Branch createBranch(Character key) { return this.branchFactory.create(key, this); diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMapNode.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMapNode.java index 743a352c2..d0b2f81eb 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMapNode.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableMapNode.java @@ -93,7 +93,12 @@ public boolean clearLeaves() { */ protected boolean pruneIfEmpty(Branch branch) { if (branch.isEmpty()) { - this.getBranches().remove(branch.getKey()); + final K key = branch.getKey(); + final B removed = this.getBranches().remove(key); + if (removed != null) { + // put back in orphans in case a user is still holding a reference + this.orphans.put(key, removed); + } return true; } else { @@ -110,10 +115,10 @@ protected boolean pruneIfEmpty(Branch branch) { * * @return {@code true} if the passed {@code branch} was an orphan, or {@code false} otherwise */ - protected boolean tryAdopt(Branch branch) { - final boolean wasOrphan = this.orphans.remove(branch.getKey()) != null; - if (wasOrphan) { - this.getBranches().put(branch.getKey(), branch.getSelf()); + protected boolean adoptIfOrphan(Branch branch) { + final B orphan = this.orphans.remove(branch.getKey()); + if (orphan != null) { + this.getBranches().put(branch.getKey(), orphan); return true; } else { @@ -157,17 +162,10 @@ protected abstract static class Branch> extends */ protected abstract K getKey(); - /** - * Implementations should be pure (stateless, no side effects). - * - * @return this branch - */ - protected abstract B getSelf(); - @Override public void put(V value) { super.put(value); - this.getParent().tryAdopt(this); + this.getParent().adoptIfOrphan(this); } @Override @@ -204,9 +202,9 @@ protected boolean pruneIfEmpty(Branch branch) { } @Override - protected boolean tryAdopt(Branch branch) { - if (super.tryAdopt(branch)) { - this.getParent().tryAdopt(this); + protected boolean adoptIfOrphan(Branch branch) { + if (super.adoptIfOrphan(branch)) { + this.getParent().adoptIfOrphan(this); return true; } else { From d0998b18c5c29e589f6cc31ed08317d41a530ece Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 19 Nov 2025 11:34:14 -0800 Subject: [PATCH 060/124] add testDepth --- .../CompositeStringMultiTrieTest.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java b/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java index b84c6704d..1b21e71df 100644 --- a/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java +++ b/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java @@ -13,6 +13,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -84,6 +85,23 @@ void testPutKeyByKeyFromStems() { } } + @Test + void testDepth() { + final CompositeStringMultiTrie trie = CompositeStringMultiTrie.createHashed(); + + for (int depth = 0; depth < KEY_BY_KEY_SUBJECT.length(); depth++) { + final Node root = trie.getRoot(); + assertThat("Unexpected root node depth", root.getDepth(), is(0)); + + Node node = root; + for (int iKey = 0; iKey <= depth; iKey++) { + node = node.next(KEY_BY_KEY_SUBJECT.charAt(iKey)); + } + + assertThat("Unexpected branch node depth", node.getDepth(), is(depth + 1)); + } + } + private static void assertOneLeaf(Node node) { assertEquals( 1, node.streamLeaves().count(), From 92e7d290c3bd4e2e178f1456e0c6b7aa541f104f Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 19 Nov 2025 12:05:00 -0800 Subject: [PATCH 061/124] add testPrevious --- .../CompositeStringMultiTrieTest.java | 37 ++++++++++++++++++- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java b/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java index 1b21e71df..0d61d4ea8 100644 --- a/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java +++ b/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java @@ -6,6 +6,7 @@ import org.opentest4j.AssertionFailedError; import org.quiltmc.enigma.util.multi_trie.MutableStringMultiTrie.Node; +import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; @@ -14,7 +15,9 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.sameInstance; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; public class CompositeStringMultiTrieTest { @@ -91,14 +94,44 @@ void testDepth() { for (int depth = 0; depth < KEY_BY_KEY_SUBJECT.length(); depth++) { final Node root = trie.getRoot(); - assertThat("Unexpected root node depth", root.getDepth(), is(0)); + assertThat("Root node depth", root.getDepth(), is(0)); Node node = root; for (int iKey = 0; iKey <= depth; iKey++) { node = node.next(KEY_BY_KEY_SUBJECT.charAt(iKey)); } - assertThat("Unexpected branch node depth", node.getDepth(), is(depth + 1)); + assertThat("Branch node depth", node.getDepth(), is(depth + 1)); + } + } + + @Test + void testPrevious() { + final CompositeStringMultiTrie trie = CompositeStringMultiTrie.createHashed(); + + final Node root = trie.getRoot(); + assertSame(root, root.previous(), "Expected root.previous() to return itself!"); + + final List> nodes = new ArrayList<>(); + nodes.add(root); + + Node node = root; + for (int iKey = 0; iKey < KEY_BY_KEY_SUBJECT.length(); iKey++) { + node = node.next(KEY_BY_KEY_SUBJECT.charAt(iKey)); + nodes.add(node); + } + + for (int i = nodes.size() - 1; i > 0; i--) { + final Node subjectNode = nodes.get(i); + final Node expectedPrev = nodes.get(i - 1); + + assertThat(expectedPrev, sameInstance(subjectNode.previous())); + + for (int steps = 0; steps < subjectNode.getDepth(); steps++) { + final Node expectedStepPrev = nodes.get(i - steps); + + assertThat(expectedStepPrev, sameInstance(subjectNode.previous(steps))); + } } } From 84b63bbd23e3cd87375feddb8398b4fb99431b62 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 19 Nov 2025 13:47:00 -0800 Subject: [PATCH 062/124] fix ignore case methods so they return all results --- .../gui/element/menu_bar/SearchMenusMenu.java | 42 +++++++++++----- .../multi_trie/CompositeStringMultiTrie.java | 6 +++ .../multi_trie/MutableStringMultiTrie.java | 48 +++++++++++-------- .../util/multi_trie/StringMultiTrie.java | 34 +++++++------ .../CompositeStringMultiTrieTest.java | 40 ++++++++++++---- 5 files changed, 111 insertions(+), 59 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java index 86138b035..68ac5938d 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java @@ -1,5 +1,6 @@ package org.quiltmc.enigma.gui.element.menu_bar; +import com.google.common.collect.ImmutableList; import org.jspecify.annotations.Nullable; import org.quiltmc.enigma.gui.ConnectionState; import org.quiltmc.enigma.gui.Gui; @@ -7,6 +8,7 @@ import org.quiltmc.enigma.gui.element.SearchableElement; import org.quiltmc.enigma.util.I18n; import org.quiltmc.enigma.util.multi_trie.CompositeStringMultiTrie; +import org.quiltmc.enigma.util.multi_trie.MultiTrie; import org.quiltmc.enigma.util.multi_trie.StringMultiTrie; import org.quiltmc.enigma.util.multi_trie.StringMultiTrie.Node; @@ -19,10 +21,13 @@ import java.awt.event.HierarchyEvent; import java.awt.event.HierarchyListener; import java.util.Arrays; +import java.util.List; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; +import static com.google.common.collect.ImmutableList.toImmutableList; + public class SearchMenusMenu extends AbstractEnigmaMenu { /** * @return a breadth-first stream of the passed {@code root} element and all of its sub-elements, @@ -180,19 +185,22 @@ UpdateOutcome updateResultItems(String searchTerm) { if (this.currentResults.searchTerm.length() == searchTerm.length()) { return UpdateOutcome.SAME_RESULTS; } else { - Node resultNode = this.currentResults.results; + List> resultNodes = this.currentResults.nodes; for (int i = this.currentResults.searchTerm.length(); i < searchTerm.length(); i++) { - resultNode = resultNode.nextIgnoreCase(searchTerm.charAt(i)); + final Character key = searchTerm.charAt(i); + resultNodes = resultNodes.stream().flatMap(node -> node.streamNextIgnoreCase(key)).toList(); } - if (resultNode.isEmpty()) { + if (resultNodes.isEmpty()) { this.clearCurrent(); return UpdateOutcome.NO_RESULTS; } else { - final Set newResults = resultNode.streamValues().collect(Collectors.toSet()); + final Set newResults = resultNodes.stream() + .flatMap(MultiTrie.Node::streamValues) + .collect(Collectors.toSet()); - final Set excludedResults = this.currentResults.results.streamValues() + final Set excludedResults = this.currentResults.stream() .filter(oldResult -> !newResults.contains(oldResult)) .map(Result::getItem) .collect(Collectors.toSet()); @@ -203,7 +211,7 @@ UpdateOutcome updateResultItems(String searchTerm) { excludedResults .forEach(SearchMenusMenu.this::remove); - this.currentResults = new CurrentResults(resultNode, searchTerm); + this.currentResults = new CurrentResults(resultNodes, searchTerm); return UpdateOutcome.DIFFERENT_RESULTS; } @@ -213,14 +221,16 @@ UpdateOutcome updateResultItems(String searchTerm) { } UpdateOutcome initializeCurrentResults(String searchTerm) { - final Node results = this.getResultTrie().getIgnoreCase(searchTerm); - if (results.isEmpty()) { + final ImmutableList> resultNodes = this.getResultTrie() + .streamIgnoreCase(searchTerm) + .collect(toImmutableList()); + if (resultNodes.isEmpty()) { this.clearCurrent(); return UpdateOutcome.NO_RESULTS; } else { - this.currentResults = new CurrentResults(results, searchTerm); - this.currentResults.results.streamValues().map(Result::getItem).forEach(SearchMenusMenu.this::add); + this.currentResults = new CurrentResults(resultNodes, searchTerm); + this.currentResults.stream().map(Result::getItem).forEach(SearchMenusMenu.this::add); return UpdateOutcome.DIFFERENT_RESULTS; } @@ -241,7 +251,7 @@ void clear() { void clearCurrent() { if (this.currentResults != null) { - this.currentResults.results.streamValues() + this.currentResults.stream() .map(Result::getItem) .forEach(SearchMenusMenu.this::remove); @@ -268,7 +278,15 @@ StringMultiTrie buildResultTrie() { return elementsBuilder.view(); } - record CurrentResults(Node results, String searchTerm) { } + record CurrentResults(ImmutableList> nodes, String searchTerm) { + CurrentResults(Iterable> nodes, String searchTerm) { + this(ImmutableList.copyOf(nodes), searchTerm); + } + + Stream stream() { + return this.nodes.stream().flatMap(StringMultiTrie.Node::streamValues); + } + } enum UpdateOutcome { NO_RESULTS, SAME_RESULTS, DIFFERENT_RESULTS diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java index f787d53e1..dfe6ce3b9 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java @@ -7,6 +7,7 @@ import java.util.HashSet; import java.util.Map; import java.util.function.Supplier; +import java.util.stream.Stream; /** * A {@link StringMultiTrie} that allows customization of nodes' backing data structures. @@ -238,6 +239,11 @@ public StringMultiTrie.Node next(Character key) { return this.viewed.next(key).view(); } + @Override + public Stream> streamNextIgnoreCase(Character key) { + return this.viewed.streamNextIgnoreCase(key); + } + @Override public StringMultiTrie.Node previous() { return this.viewed.previous().view(); diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableStringMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableStringMultiTrie.java index f130e06c2..37d93b0f4 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableStringMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableStringMultiTrie.java @@ -2,7 +2,8 @@ import org.quiltmc.enigma.util.Utils; -import java.util.Optional; +import java.util.List; +import java.util.stream.Stream; /** * A {@linkplain MutableMultiTrie mutable} {@link StringMultiTrie}. @@ -19,16 +20,6 @@ * @param the type of values */ public interface MutableStringMultiTrie extends MutableMultiTrie, StringMultiTrie { - static Optional tryToggleCase(char c) { - if (Character.isUpperCase(c)) { - return Optional.of(Character.toLowerCase(c)); - } else if (Character.isLowerCase(c)) { - return Optional.of(Character.toUpperCase(c)); - } else { - return Optional.empty(); - } - } - String STRING = "string"; String VALUE = "value"; @@ -41,8 +32,26 @@ default Node get(String prefix) { } @Override - default Node getIgnoreCase(String prefix) { - return StringMultiTrie.get(prefix, this.getRoot(), Node::nextIgnoreCase); + default Stream> streamIgnoreCase(String prefix) { + Utils.requireNonNull(prefix, "prefix"); + + if (this.isEmpty()) { + return Stream.empty(); + } + + List> nodes = List.of(this.getRoot().view()); + for (int i = 0; i < prefix.length(); i++) { + final Character key = prefix.charAt(i); + nodes = nodes.stream() + .flatMap(node -> node.streamNextIgnoreCase(key)) + .filter(node -> !node.isEmpty()) + .toList(); + if (nodes.isEmpty()) { + return Stream.empty(); + } + } + + return nodes.stream(); } @Override @@ -107,11 +116,12 @@ interface Node extends StringMultiTrie.Node, MutableMultiTrie.Node view(); @Override - default Node nextIgnoreCase(Character key) { + default Stream> streamNextIgnoreCase(Character key) { final Node next = this.next(key); - return next.isEmpty() - ? tryToggleCase(key).map(this::next).orElse(next) - : next; + return Stream.concat( + next.isEmpty() ? Stream.empty() : Stream.of(next.view()), + StringMultiTrie.tryToggleCase(key).map(this::next).map(Node::view).stream() + ); } } @@ -127,8 +137,8 @@ public Node get(String prefix) { } @Override - public Node getIgnoreCase(String prefix) { - return this.getViewed().getIgnoreCase(prefix).view(); + public Stream> streamIgnoreCase(String prefix) { + return this.getViewed().streamIgnoreCase(prefix); } protected abstract MutableStringMultiTrie getViewed(); diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java index c2f3837a2..ac3e87b6f 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java @@ -2,7 +2,9 @@ import org.quiltmc.enigma.util.Utils; +import java.util.Optional; import java.util.function.BiFunction; +import java.util.stream.Stream; /** * A {@link MultiTrie} that associates sequences of characters with values of type {@code V}. @@ -10,13 +12,23 @@ *

    Adds {@link String}/{@link Character}-specific access methods: *

      *
    • {@link #get(String)} - *
    • {@link #getIgnoreCase(String)} - *
    • {@link Node#nextIgnoreCase(Character)} + *
    • {@link #streamIgnoreCase(String)} + *
    • {@link Node#streamNextIgnoreCase(Character)} *
    * * @param the type of values */ public interface StringMultiTrie extends MultiTrie { + static Optional tryToggleCase(char c) { + if (Character.isUpperCase(c)) { + return Optional.of(Character.toLowerCase(c)); + } else if (Character.isLowerCase(c)) { + return Optional.of(Character.toUpperCase(c)); + } else { + return Optional.empty(); + } + } + static > N get(String prefix, N root, BiFunction next) { Utils.requireNonNull(prefix, "prefix"); @@ -33,27 +45,13 @@ static > N get(String prefix, N root, BiFunction get(String prefix); - Node getIgnoreCase(String prefix); + Stream> streamIgnoreCase(String prefix); interface Node extends MultiTrie.Node { @Override Node next(Character key); - default Node nextIgnoreCase(Character key) { - final Node next = this.next(key); - if (next.isEmpty()) { - final char c = key; - if (Character.isUpperCase(c)) { - return this.next(Character.toLowerCase(c)); - } else if (Character.isLowerCase(c)) { - return this.next(Character.toUpperCase(c)); - } else { - return next; - } - } else { - return next; - } - } + Stream> streamNextIgnoreCase(Character key); @Override Node previous(); diff --git a/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java b/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java index 0d61d4ea8..85e9c015a 100644 --- a/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java +++ b/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java @@ -135,7 +135,7 @@ void testPrevious() { } } - private static void assertOneLeaf(Node node) { + private static void assertOneLeaf(StringMultiTrie.Node node) { assertEquals( 1, node.streamLeaves().count(), () -> "Expected node to have only one leaf, but had the following: " + node.streamLeaves().toList() @@ -287,23 +287,25 @@ private static void assertUnorderedContentsForPrefix( } @Test - void testNextIgnoreCase() { + void testStreamNextIgnoreCase() { final CompositeStringMultiTrie trie = CompositeStringMultiTrie.createHashed(); trie.put(IGNORE_CASE_SUBJECT, IGNORE_CASE_SUBJECT); final String invertedSubject = caseInverted(IGNORE_CASE_SUBJECT); - Node node = trie.getRoot(); + List> nodes = List.of(trie.getRoot()); for (int i = 0; i < invertedSubject.length(); i++) { - node = node.nextIgnoreCase(invertedSubject.charAt(i)); + final char key = invertedSubject.charAt(i); + nodes = nodes.stream().flatMap(node -> node.streamNextIgnoreCase(key)).toList(); + assertNodeCount(nodes, 1); - assertOneValue(node); + assertOneValue(nodes.get(0)); } - assertOneLeaf(node); + assertOneLeaf(nodes.get(0)); } - private static void assertOneValue(Node node) { + private static void assertOneValue(StringMultiTrie.Node node) { assertEquals( 1, node.getSize(), "Expected node to have only one value, but had the following: " + node.streamValues().toList() @@ -318,13 +320,31 @@ void testGetIgnoreCase() { final String invertedSubject = caseInverted(IGNORE_CASE_SUBJECT); - final Node node = trie.getIgnoreCase(invertedSubject); + final List> singleNodes = trie.streamIgnoreCase(invertedSubject).toList(); + assertNodeCount(singleNodes, 1); - assertOneValue(node); + final StringMultiTrie.Node singleNode = singleNodes.get(0); + assertOneValue(singleNode); - node.streamLeaves() + singleNode.streamLeaves() .findAny() .orElseThrow(() -> new AssertionFailedError("Expected node to have a leaf, but had none!")); + + trie.put(invertedSubject, invertedSubject); + + final List> nodes = trie.streamIgnoreCase(IGNORE_CASE_SUBJECT).toList(); + assertNodeCount(nodes, 2); + + final List> invertedNodes = trie.streamIgnoreCase(invertedSubject).toList(); + + assertThat( + "Searching by non/inverted case keys should yield the same results!", + nodes, containsInAnyOrder(invertedNodes.toArray()) + ); + } + + private static void assertNodeCount(Collection> nodes, int expected) { + assertThat("Node count", nodes.size(), is(expected)); } record Association(String key) { From e82ff68a74a2185a220a08df906a9e07f8bbf5e5 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 19 Nov 2025 13:57:08 -0800 Subject: [PATCH 063/124] filter empty nodes in streamNextIgnoreCase --- .../enigma/util/multi_trie/MutableStringMultiTrie.java | 6 +++++- .../enigma/util/multi_trie/StringMultiTrie.java | 10 ++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableStringMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableStringMultiTrie.java index 37d93b0f4..6d5e1ef61 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableStringMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableStringMultiTrie.java @@ -120,7 +120,11 @@ default Stream> streamNextIgnoreCase(Character key) { final Node next = this.next(key); return Stream.concat( next.isEmpty() ? Stream.empty() : Stream.of(next.view()), - StringMultiTrie.tryToggleCase(key).map(this::next).map(Node::view).stream() + StringMultiTrie.tryToggleCase(key) + .map(this::next) + .filter(node -> !node.isEmpty()) + .map(Node::view) + .stream() ); } } diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java index ac3e87b6f..0f3cc40e7 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java @@ -43,14 +43,24 @@ static > N get(String prefix, N root, BiFunction getRoot(); + /** + * @return the node associated with the passed {@code prefix} + */ Node get(String prefix); + /** + * @return a {@link Stream} of all nodes associated with the passed {@code prefix}, ignoring case + */ Stream> streamIgnoreCase(String prefix); interface Node extends MultiTrie.Node { @Override Node next(Character key); + /** + * @return a stream of 0, 1, or 2 non-{@linkplain #isEmpty() empty} nodes associated with the sequence formed + * by appending the passed {@code key} or its case variant to this node's sequence + */ Stream> streamNextIgnoreCase(Character key); @Override From aea8b1704754e03ea3c1e5d69f7445e767f4435f Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 19 Nov 2025 14:03:23 -0800 Subject: [PATCH 064/124] add Node::isNonEmpty --- .../quiltmc/enigma/util/multi_trie/MultiTrie.java | 13 ++++++++++++- .../util/multi_trie/MutableStringMultiTrie.java | 4 ++-- .../enigma/util/multi_trie/StringMultiTrie.java | 2 +- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MultiTrie.java index 9e6e9f23e..06898d50c 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MultiTrie.java @@ -88,9 +88,20 @@ default long getSize() { /** * @return {@code true} if this node contains no {@linkplain #streamValues() values}, or {@code false} otherwise + * + * @see #isNonEmpty() */ default boolean isEmpty() { - return this.getSize() == 0; + return !this.isNonEmpty(); + } + + /** + * @return {@code false} if this node contains no {@linkplain #streamValues()}, or {@code true} otherwise + * + * @see #isEmpty() + */ + default boolean isNonEmpty() { + return this.getSize() > 0; } /** diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableStringMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableStringMultiTrie.java index 6d5e1ef61..d3c0820e6 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableStringMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/MutableStringMultiTrie.java @@ -44,7 +44,7 @@ default Stream> streamIgnoreCase(String prefix) { final Character key = prefix.charAt(i); nodes = nodes.stream() .flatMap(node -> node.streamNextIgnoreCase(key)) - .filter(node -> !node.isEmpty()) + .filter(StringMultiTrie.Node::isNonEmpty) .toList(); if (nodes.isEmpty()) { return Stream.empty(); @@ -122,7 +122,7 @@ default Stream> streamNextIgnoreCase(Character key) { next.isEmpty() ? Stream.empty() : Stream.of(next.view()), StringMultiTrie.tryToggleCase(key) .map(this::next) - .filter(node -> !node.isEmpty()) + .filter(Node::isNonEmpty) .map(Node::view) .stream() ); diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java index 0f3cc40e7..68d1905f4 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/StringMultiTrie.java @@ -58,7 +58,7 @@ interface Node extends MultiTrie.Node { Node next(Character key); /** - * @return a stream of 0, 1, or 2 non-{@linkplain #isEmpty() empty} nodes associated with the sequence formed + * @return a stream of 0, 1, or 2 {@linkplain #isNonEmpty() non-empty} nodes associated with the sequence formed * by appending the passed {@code key} or its case variant to this node's sequence */ Stream> streamNextIgnoreCase(Character key); From 37e8a77162fb380346258ead5dc782ba9856c571 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 19 Nov 2025 14:55:32 -0800 Subject: [PATCH 065/124] utilize Node::previous in SearchMenusMenu --- .../gui/element/menu_bar/SearchMenusMenu.java | 88 +++++++++++++------ 1 file changed, 62 insertions(+), 26 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java index 68ac5938d..28c67ccf8 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java @@ -8,7 +8,6 @@ import org.quiltmc.enigma.gui.element.SearchableElement; import org.quiltmc.enigma.util.I18n; import org.quiltmc.enigma.util.multi_trie.CompositeStringMultiTrie; -import org.quiltmc.enigma.util.multi_trie.MultiTrie; import org.quiltmc.enigma.util.multi_trie.StringMultiTrie; import org.quiltmc.enigma.util.multi_trie.StringMultiTrie.Node; @@ -21,9 +20,6 @@ import java.awt.event.HierarchyEvent; import java.awt.event.HierarchyListener; import java.util.Arrays; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; import java.util.stream.Stream; import static com.google.common.collect.ImmutableList.toImmutableList; @@ -179,47 +175,87 @@ private class ResultManager { * @return {@code true} if there are any results, or {@code false} otherwise */ UpdateOutcome updateResultItems(String searchTerm) { - if (this.currentResults == null || !searchTerm.startsWith(this.currentResults.searchTerm)) { + if (this.currentResults == null) { return this.initializeCurrentResults(searchTerm); } else { - if (this.currentResults.searchTerm.length() == searchTerm.length()) { + final int commonPrefixLength = + getCommonPrefixLengthIgnoreCase(this.currentResults.searchTerm, searchTerm); + final int termLength = searchTerm.length(); + final int currentTermLength = this.currentResults.searchTerm.length(); + + if (commonPrefixLength == 0) { + return this.initializeCurrentResults(searchTerm); + } else if (commonPrefixLength == termLength && commonPrefixLength == currentTermLength) { return UpdateOutcome.SAME_RESULTS; } else { - List> resultNodes = this.currentResults.nodes; - for (int i = this.currentResults.searchTerm.length(); i < searchTerm.length(); i++) { - final Character key = searchTerm.charAt(i); - resultNodes = resultNodes.stream().flatMap(node -> node.streamNextIgnoreCase(key)).toList(); + final ImmutableList> commonPrefixNodes; + final int backSteps = currentTermLength - commonPrefixLength; + if (backSteps > 0) { + commonPrefixNodes = this.currentResults.nodes.stream() + .map(node -> node.previous(backSteps)) + .distinct() + .collect(toImmutableList()); + } else { + commonPrefixNodes = this.currentResults.nodes; } - if (resultNodes.isEmpty()) { - this.clearCurrent(); - - return UpdateOutcome.NO_RESULTS; - } else { - final Set newResults = resultNodes.stream() - .flatMap(MultiTrie.Node::streamValues) - .collect(Collectors.toSet()); + if (termLength > commonPrefixLength) { + ImmutableList> resultNodes = commonPrefixNodes; + for (int i = commonPrefixLength; i < termLength; i++) { + final Character key = searchTerm.charAt(i); + resultNodes = resultNodes.stream() + .flatMap(node -> node.streamNextIgnoreCase(key)) + .collect(toImmutableList()); + } - final Set excludedResults = this.currentResults.stream() - .filter(oldResult -> !newResults.contains(oldResult)) - .map(Result::getItem) - .collect(Collectors.toSet()); + if (resultNodes.isEmpty()) { + this.clearCurrent(); - if (excludedResults.isEmpty()) { - return UpdateOutcome.SAME_RESULTS; + return UpdateOutcome.NO_RESULTS; } else { - excludedResults - .forEach(SearchMenusMenu.this::remove); + this.currentResults.stream().map(Result::getItem).forEach(SearchMenusMenu.this::remove); this.currentResults = new CurrentResults(resultNodes, searchTerm); + this.currentResults.stream().map(Result::getItem).forEach(SearchMenusMenu.this::add); + return UpdateOutcome.DIFFERENT_RESULTS; } + } else { + this.currentResults = new CurrentResults(commonPrefixNodes, searchTerm); + + this.currentResults.stream().map(Result::getItem).forEach(SearchMenusMenu.this::add); + + return UpdateOutcome.DIFFERENT_RESULTS; } } } } + private static int getCommonPrefixLengthIgnoreCase(String left, String right) { + final int minLength = Math.min(left.length(), right.length()); + + for (int i = 0; i < minLength; i++) { + if (!equalsIgnoreCase(left.charAt(i), right.charAt(i))) { + return i; + } + } + + return minLength; + } + + private static boolean equalsIgnoreCase(char left, char right) { + if (left == right) { + return true; + } else if (Character.isUpperCase(right)) { + return left == Character.toLowerCase(right); + } else if (Character.isLowerCase(right)) { + return left == Character.toUpperCase(right); + } else { + return false; + } + } + UpdateOutcome initializeCurrentResults(String searchTerm) { final ImmutableList> resultNodes = this.getResultTrie() .streamIgnoreCase(searchTerm) From 04df0cda80bb75fecd6be29e56b18df8ec673a52 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 19 Nov 2025 15:15:31 -0800 Subject: [PATCH 066/124] add testViews --- .../CompositeStringMultiTrieTest.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java b/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java index 85e9c015a..c6d348473 100644 --- a/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java +++ b/enigma/src/test/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrieTest.java @@ -17,6 +17,7 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.sameInstance; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -343,6 +344,32 @@ nodes, containsInAnyOrder(invertedNodes.toArray()) ); } + @Test + void testViews() { + final CompositeStringMultiTrie trie = CompositeStringMultiTrie.createHashed(); + + assertFalse(trie.view() instanceof MutableMultiTrie, "Trie view must not be mutable!"); + + assertFalse(trie.getRoot().view() instanceof MutableMultiTrie.Node, "Trie root view must not be mutable!"); + + assertThat( + "Root view should be the same as view root", + trie.view().getRoot(), sameInstance(trie.getRoot().view()) + ); + + final char key = '1'; + + // orphan node + final Node node = trie.getRoot().next(key); + + assertFalse(node.view() instanceof MutableMultiTrie.Node, "Trie branch view must not be mutable!"); + + assertThat( + "View lookups should be the same as trie lookup views", + node.view(), sameInstance(trie.view().getRoot().next(key)) + ); + } + private static void assertNodeCount(Collection> nodes, int expected) { assertThat("Node count", nodes.size(), is(expected)); } From 086fcd7f14720f3e405207a9da4a3d07246fc2c8 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 19 Nov 2025 15:17:38 -0800 Subject: [PATCH 067/124] move SearchableElement to menu_bar --- .../gui/element/menu_bar/AbstractSearchableEnigmaMenu.java | 1 - .../quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java | 1 - .../enigma/gui/element/{ => menu_bar}/SearchableElement.java | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) rename enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/{ => menu_bar}/SearchableElement.java (96%) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/AbstractSearchableEnigmaMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/AbstractSearchableEnigmaMenu.java index 345accde1..83e7a27d0 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/AbstractSearchableEnigmaMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/AbstractSearchableEnigmaMenu.java @@ -1,7 +1,6 @@ package org.quiltmc.enigma.gui.element.menu_bar; import org.quiltmc.enigma.gui.Gui; -import org.quiltmc.enigma.gui.element.SearchableElement; public abstract class AbstractSearchableEnigmaMenu extends AbstractEnigmaMenu implements SearchableElement { protected AbstractSearchableEnigmaMenu(Gui gui) { diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java index 28c67ccf8..5f10ef00a 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java @@ -5,7 +5,6 @@ import org.quiltmc.enigma.gui.ConnectionState; import org.quiltmc.enigma.gui.Gui; import org.quiltmc.enigma.gui.element.PlaceheldTextField; -import org.quiltmc.enigma.gui.element.SearchableElement; import org.quiltmc.enigma.util.I18n; import org.quiltmc.enigma.util.multi_trie.CompositeStringMultiTrie; import org.quiltmc.enigma.util.multi_trie.StringMultiTrie; diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/SearchableElement.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchableElement.java similarity index 96% rename from enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/SearchableElement.java rename to enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchableElement.java index efdf23871..f5933ab64 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/SearchableElement.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchableElement.java @@ -1,4 +1,4 @@ -package org.quiltmc.enigma.gui.element; +package org.quiltmc.enigma.gui.element.menu_bar; import org.quiltmc.enigma.util.I18n; From dd835b99d7ca9bf6fd5605b3c4260be3cf2a1442 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 19 Nov 2025 16:38:46 -0800 Subject: [PATCH 068/124] extract Retranslatable interface make FileMenu's sub-items searchable --- CONTRIBUTING.md | 51 +++++++++------ .../gui/element/menu_bar/EnigmaMenu.java | 3 +- .../enigma/gui/element/menu_bar/HelpMenu.java | 12 ++-- .../enigma/gui/element/menu_bar/MenuBar.java | 11 +++- .../gui/element/menu_bar/Retranslatable.java | 5 ++ .../gui/element/menu_bar/SearchMenusMenu.java | 8 ++- .../menu_bar/SearchableCheckBoxItem.java | 26 ++++++++ .../gui/element/menu_bar/SearchableItem.java | 26 ++++++++ .../element/menu_bar/SimpleCheckBoxItem.java | 14 ++++ .../gui/element/menu_bar/SimpleItem.java | 14 ++++ .../gui/element/menu_bar/file/FileMenu.java | 64 +++++++++---------- 11 files changed, 175 insertions(+), 59 deletions(-) create mode 100644 enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/Retranslatable.java create mode 100644 enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchableCheckBoxItem.java create mode 100644 enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchableItem.java create mode 100644 enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SimpleCheckBoxItem.java create mode 100644 enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SimpleItem.java diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cd14258d0..6ddfd43a0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -49,21 +49,36 @@ If you'd like to add search aliases to an element that doesn't already have alia its translation file. #### Complete list of search alias translation keys -| Element | Translation Key | -|-----------------------------|-------------------------------------------| -| `Dev` menu | `"dev.menu.aliases"` | -| `Collab` menu | `"menu.collab.aliases"` | -| `Decompiler` menu | `"menu.decompiler.aliases"` | -| `Help` menu | `"menu.help.aliases"` | -| `Search` menu | `"menu.search.aliases"` | -| `Crash History` menu | `"menu.file.crash_history.aliases"` | -| `File` menu | `"menu.file.aliases"` | -| `Open Recent Project` menu | `"menu.file.open_recent_project.aliases"` | -| `Save Mappings As...` menu | `"menu.file.mappings.save_as.aliases"` | -| `View` menu | `"menu.view.aliases"` | -| `Entry Tooltips` menu | `"menu.view.entry_tooltips.aliases"` | -| `Languages` menu | `"menu.view.languages.aliases"` | -| `Server Notifications` menu | `"menu.view.notifications.aliases"` | -| `Scale` menu | `"menu.view.scale.aliases"` | -| `Stat Icons` menu | `"menu.view.stat_icons.aliases"` | -| `Themes` menu | `"menu.view.themes.aliases"` | +| Element | Translation Key | +|--------------------------------|-------------------------------------------| +| `Dev` menu | `"dev.menu.aliases"` | +| `Collab` menu | `"menu.collab.aliases"` | +| `Decompiler` menu | `"menu.decompiler.aliases"` | +| `Help` menu | `"menu.help.aliases"` | +| `Search` menu | `"menu.search.aliases"` | +| `Crash History` menu | `"menu.file.crash_history.aliases"` | +| `File` menu | `"menu.file.aliases"` | +| `File`>`Open Jar...` | `"menu.file.jar.open.aliases"` | +| `File`>`Close Jar` | `"menu.file.jar.close.aliases"` | +| `File`>`Open Mappings...` | `"menu.file.mappings.open.aliases"` | +| `File`>`Max Recent Projects` | `"menu.file.max_recent_projects.aliases"` | +| `File`>`Save Mappings` | `"menu.file.mappings.save.aliases"` | +| `File`>`Auto Save Mappings` | `"menu.file.mappings.auto_save.aliases"` | +| `File`>`Close Mappings` | `"menu.file.mappings.close.aliases"` | +| `File`>`Drop Invalid Mappings` | `"menu.file.mappings.drop.aliases"` | +| `File`>`Reload Mappings` | `"menu.file.reload_mappings.aliases"` | +| `File`>`Reload Jar/Mappings` | `"menu.file.reload_all.aliases"` | +| `File`>`Export Source...` | `"menu.file.export.source.aliases"` | +| `File`>`Export Jar...` | `"menu.file.export.jar.aliases"` | +| `File`>`Mapping Stats...` | `"menu.file.stats.aliases"` | +| `File`>`Configure Keybinds...` | `"menu.file.configure_keybinds.aliases"` | +| `File`>`Exit` | `"menu.file.exit.aliases"` | +| `Open Recent Project` menu | `"menu.file.open_recent_project.aliases"` | +| `Save Mappings As...` menu | `"menu.file.mappings.save_as.aliases"` | +| `View` menu | `"menu.view.aliases"` | +| `Entry Tooltips` menu | `"menu.view.entry_tooltips.aliases"` | +| `Languages` menu | `"menu.view.languages.aliases"` | +| `Server Notifications` menu | `"menu.view.notifications.aliases"` | +| `Scale` menu | `"menu.view.scale.aliases"` | +| `Stat Icons` menu | `"menu.view.stat_icons.aliases"` | +| `Themes` menu | `"menu.view.themes.aliases"` | diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/EnigmaMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/EnigmaMenu.java index 8c0adc649..c9c4875e8 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/EnigmaMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/EnigmaMenu.java @@ -4,10 +4,11 @@ import javax.swing.MenuElement; -public interface EnigmaMenu extends MenuElement { +public interface EnigmaMenu extends MenuElement, Retranslatable { default void setKeyBinds() { } default void updateState(boolean jarOpen, ConnectionState state) { } + @Override default void retranslate() { } } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/HelpMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/HelpMenu.java index 2a9758aa7..c53e40438 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/HelpMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/HelpMenu.java @@ -12,27 +12,31 @@ public class HelpMenu extends AbstractSearchableEnigmaMenu { private final JMenuItem aboutItem = new JMenuItem(); private final JMenuItem githubItem = new JMenuItem(); - private final SearchMenusMenu searchItem; + private final SearchMenusMenu searchMenusMenu; public HelpMenu(Gui gui) { super(gui); - this.searchItem = new SearchMenusMenu(gui); + this.searchMenusMenu = new SearchMenusMenu(gui); this.add(this.aboutItem); this.add(this.githubItem); - this.add(this.searchItem); + this.add(this.searchMenusMenu); this.aboutItem.addActionListener(e -> AboutDialog.show(this.gui.getFrame())); this.githubItem.addActionListener(e -> this.onGithubClicked()); } + public void clearSearchMenusResults() { + this.searchMenusMenu.clearResults(); + } + @Override public void retranslate() { this.setText(I18n.translate(TRANSLATION_KEY)); this.aboutItem.setText(I18n.translate("menu.help.about")); this.githubItem.setText(I18n.translate("menu.help.github")); - this.searchItem.retranslate(); + this.searchMenusMenu.retranslate(); } private void onGithubClicked() { diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/MenuBar.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/MenuBar.java index 158c911bc..bf4b5cf2f 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/MenuBar.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/MenuBar.java @@ -15,6 +15,7 @@ public class MenuBar { private final CollabMenu collabMenu; private final FileMenu fileMenu; + private final HelpMenu helpMenu; private final Gui gui; @@ -26,7 +27,7 @@ public MenuBar(Gui gui) { ViewMenu viewMenu = new ViewMenu(gui); SearchMenu searchMenu = new SearchMenu(gui); this.collabMenu = new CollabMenu(gui); - HelpMenu helpMenu = new HelpMenu(gui); + this.helpMenu = new HelpMenu(gui); // Enabled with system property "enigma.development" or "--development" flag DevMenu devMenu = new DevMenu(gui); @@ -37,7 +38,7 @@ public MenuBar(Gui gui) { this.addMenu(viewMenu); this.addMenu(searchMenu); this.addMenu(this.collabMenu); - this.addMenu(helpMenu); + this.addMenu(this.helpMenu); if (Boolean.parseBoolean(System.getProperty("enigma.development")) || Config.main().development.anyEnabled) { this.addMenu(devMenu); @@ -69,6 +70,12 @@ public void retranslateUi() { for (EnigmaMenu menu : this.menus) { menu.retranslate(); } + + this.clearSearchMenusResults(); + } + + public void clearSearchMenusResults() { + this.helpMenu.clearSearchMenusResults(); } public CollabMenu getCollabMenu() { diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/Retranslatable.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/Retranslatable.java new file mode 100644 index 000000000..5d935a826 --- /dev/null +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/Retranslatable.java @@ -0,0 +1,5 @@ +package org.quiltmc.enigma.gui.element.menu_bar; + +public interface Retranslatable { + void retranslate(); +} diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java index 5f10ef00a..deb3fc3af 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java @@ -132,14 +132,18 @@ public void changedUpdate(DocumentEvent e) { this.retranslate(); } + public void clearResults() { + this.resultManager.clear(); + } + @Override public void updateState(boolean jarOpen, ConnectionState state) { - this.resultManager.clear(); + this.clearResults(); } @Override public void retranslate() { - this.resultManager.clear(); + this.clearResults(); this.setText(I18n.translate("menu.help.search")); this.field.setPlaceholder(I18n.translate("menu.help.search.placeholder")); diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchableCheckBoxItem.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchableCheckBoxItem.java new file mode 100644 index 000000000..86213c857 --- /dev/null +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchableCheckBoxItem.java @@ -0,0 +1,26 @@ +package org.quiltmc.enigma.gui.element.menu_bar; + +import javax.swing.JCheckBoxMenuItem; + +public class SearchableCheckBoxItem extends JCheckBoxMenuItem implements SearchableElement { + private final String aliasesTranslationKeyPrefix; + + public SearchableCheckBoxItem(String aliasesTranslationKeyPrefix) { + this.aliasesTranslationKeyPrefix = aliasesTranslationKeyPrefix; + } + + @Override + public String getSearchName() { + return this.getText(); + } + + @Override + public String getAliasesTranslationKeyPrefix() { + return this.aliasesTranslationKeyPrefix; + } + + @Override + public void onSearchClicked() { + this.doClick(); + } +} diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchableItem.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchableItem.java new file mode 100644 index 000000000..3d2bf47a4 --- /dev/null +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchableItem.java @@ -0,0 +1,26 @@ +package org.quiltmc.enigma.gui.element.menu_bar; + +import javax.swing.JMenuItem; + +public class SearchableItem extends JMenuItem implements SearchableElement { + private final String aliasesTranslationKeyPrefix; + + public SearchableItem(String aliasesTranslationKeyPrefix) { + this.aliasesTranslationKeyPrefix = aliasesTranslationKeyPrefix; + } + + @Override + public String getSearchName() { + return this.getText(); + } + + @Override + public String getAliasesTranslationKeyPrefix() { + return this.aliasesTranslationKeyPrefix; + } + + @Override + public void onSearchClicked() { + this.doClick(); + } +} diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SimpleCheckBoxItem.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SimpleCheckBoxItem.java new file mode 100644 index 000000000..e8309a50c --- /dev/null +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SimpleCheckBoxItem.java @@ -0,0 +1,14 @@ +package org.quiltmc.enigma.gui.element.menu_bar; + +import org.quiltmc.enigma.util.I18n; + +public class SimpleCheckBoxItem extends SearchableCheckBoxItem implements Retranslatable { + public SimpleCheckBoxItem(String translationKey) { + super(translationKey); + } + + @Override + public void retranslate() { + this.setText(I18n.translate(this.getAliasesTranslationKeyPrefix())); + } +} diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SimpleItem.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SimpleItem.java new file mode 100644 index 000000000..64577c200 --- /dev/null +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SimpleItem.java @@ -0,0 +1,14 @@ +package org.quiltmc.enigma.gui.element.menu_bar; + +import org.quiltmc.enigma.util.I18n; + +public class SimpleItem extends SearchableItem implements Retranslatable { + public SimpleItem(String translationKey) { + super(translationKey); + } + + @Override + public void retranslate() { + this.setText(I18n.translate(this.getAliasesTranslationKeyPrefix())); + } +} diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/file/FileMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/file/FileMenu.java index 485df5ac9..ef8dccf33 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/file/FileMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/file/FileMenu.java @@ -8,12 +8,12 @@ import org.quiltmc.enigma.gui.dialog.StatsDialog; import org.quiltmc.enigma.gui.dialog.keybind.ConfigureKeyBindsDialog; import org.quiltmc.enigma.gui.element.menu_bar.AbstractSearchableEnigmaMenu; +import org.quiltmc.enigma.gui.element.menu_bar.SimpleCheckBoxItem; +import org.quiltmc.enigma.gui.element.menu_bar.SimpleItem; import org.quiltmc.enigma.gui.util.ExtensionFileFilter; import org.quiltmc.enigma.util.I18n; -import javax.swing.JCheckBoxMenuItem; import javax.swing.JFileChooser; -import javax.swing.JMenuItem; import javax.swing.JOptionPane; import java.io.File; import java.nio.file.Files; @@ -28,21 +28,21 @@ public class FileMenu extends AbstractSearchableEnigmaMenu { private final CrashHistoryMenu crashHistory; private final OpenRecentMenu openRecent; - private final JMenuItem jarOpenItem = new JMenuItem(); - private final JMenuItem jarCloseItem = new JMenuItem(); - private final JMenuItem openMappingsItem = new JMenuItem(); - private final JMenuItem maxRecentFilesItem = new JMenuItem(); - private final JMenuItem saveMappingsItem = new JMenuItem(); - private final JCheckBoxMenuItem autoSaveMappingsItem = new JCheckBoxMenuItem(); - private final JMenuItem closeMappingsItem = new JMenuItem(); - private final JMenuItem dropMappingsItem = new JMenuItem(); - private final JMenuItem reloadMappingsItem = new JMenuItem(); - private final JMenuItem reloadAllItem = new JMenuItem(); - private final JMenuItem exportSourceItem = new JMenuItem(); - private final JMenuItem exportJarItem = new JMenuItem(); - private final JMenuItem statsItem = new JMenuItem(); - private final JMenuItem configureKeyBindsItem = new JMenuItem(); - private final JMenuItem exitItem = new JMenuItem(); + private final SimpleItem jarOpenItem = new SimpleItem("menu.file.jar.open"); + private final SimpleItem jarCloseItem = new SimpleItem("menu.file.jar.close"); + private final SimpleItem openMappingsItem = new SimpleItem("menu.file.mappings.open"); + private final SimpleItem maxRecentFilesItem = new SimpleItem("menu.file.max_recent_projects"); + private final SimpleItem saveMappingsItem = new SimpleItem("menu.file.mappings.save"); + private final SimpleCheckBoxItem autoSaveMappingsItem = new SimpleCheckBoxItem("menu.file.mappings.auto_save"); + private final SimpleItem closeMappingsItem = new SimpleItem("menu.file.mappings.close"); + private final SimpleItem dropMappingsItem = new SimpleItem("menu.file.mappings.drop"); + private final SimpleItem reloadMappingsItem = new SimpleItem("menu.file.reload_mappings"); + private final SimpleItem reloadAllItem = new SimpleItem("menu.file.reload_all"); + private final SimpleItem exportSourceItem = new SimpleItem("menu.file.export.source"); + private final SimpleItem exportJarItem = new SimpleItem("menu.file.export.jar"); + private final SimpleItem statsItem = new SimpleItem("menu.file.stats"); + private final SimpleItem configureKeyBindsItem = new SimpleItem("menu.file.configure_keybinds"); + private final SimpleItem exitItem = new SimpleItem("menu.file.exit"); public FileMenu(Gui gui) { super(gui); @@ -125,24 +125,24 @@ public void updateState(boolean jarOpen, ConnectionState state) { @Override public void retranslate() { this.setText(I18n.translate(TRANSLATION_KEY)); - this.jarOpenItem.setText(I18n.translate("menu.file.jar.open")); - this.jarCloseItem.setText(I18n.translate("menu.file.jar.close")); + this.jarOpenItem.retranslate(); + this.jarCloseItem.retranslate(); this.openRecent.retranslate(); - this.maxRecentFilesItem.setText(I18n.translate("menu.file.max_recent_projects")); - this.openMappingsItem.setText(I18n.translate("menu.file.mappings.open")); - this.saveMappingsItem.setText(I18n.translate("menu.file.mappings.save")); + this.openMappingsItem.retranslate(); + this.maxRecentFilesItem.retranslate(); + this.saveMappingsItem.retranslate(); this.saveMappingsAs.retranslate(); - this.autoSaveMappingsItem.setText(I18n.translate("menu.file.mappings.auto_save")); - this.closeMappingsItem.setText(I18n.translate("menu.file.mappings.close")); - this.dropMappingsItem.setText(I18n.translate("menu.file.mappings.drop")); - this.reloadMappingsItem.setText(I18n.translate("menu.file.reload_mappings")); - this.reloadAllItem.setText(I18n.translate("menu.file.reload_all")); - this.exportSourceItem.setText(I18n.translate("menu.file.export.source")); - this.exportJarItem.setText(I18n.translate("menu.file.export.jar")); - this.statsItem.setText(I18n.translate("menu.file.stats")); - this.configureKeyBindsItem.setText(I18n.translate("menu.file.configure_keybinds")); + this.autoSaveMappingsItem.retranslate(); + this.closeMappingsItem.retranslate(); + this.dropMappingsItem.retranslate(); + this.reloadMappingsItem.retranslate(); + this.reloadAllItem.retranslate(); + this.exportSourceItem.retranslate(); + this.exportJarItem.retranslate(); + this.statsItem.retranslate(); + this.configureKeyBindsItem.retranslate(); this.crashHistory.retranslate(); - this.exitItem.setText(I18n.translate("menu.file.exit")); + this.exitItem.retranslate(); } private void onOpenJarClicked() { From aaae2f1c39d18b11d6791389de5237a6bbf5c2ec Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 19 Nov 2025 17:59:09 -0800 Subject: [PATCH 069/124] make SaveMappingsAsMenu's formats searchable --- CONTRIBUTING.md | 71 ++++++++++--------- .../menu_bar/file/SaveMappingsAsMenu.java | 8 +-- 2 files changed, 42 insertions(+), 37 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6ddfd43a0..8018a441b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -49,36 +49,41 @@ If you'd like to add search aliases to an element that doesn't already have alia its translation file. #### Complete list of search alias translation keys -| Element | Translation Key | -|--------------------------------|-------------------------------------------| -| `Dev` menu | `"dev.menu.aliases"` | -| `Collab` menu | `"menu.collab.aliases"` | -| `Decompiler` menu | `"menu.decompiler.aliases"` | -| `Help` menu | `"menu.help.aliases"` | -| `Search` menu | `"menu.search.aliases"` | -| `Crash History` menu | `"menu.file.crash_history.aliases"` | -| `File` menu | `"menu.file.aliases"` | -| `File`>`Open Jar...` | `"menu.file.jar.open.aliases"` | -| `File`>`Close Jar` | `"menu.file.jar.close.aliases"` | -| `File`>`Open Mappings...` | `"menu.file.mappings.open.aliases"` | -| `File`>`Max Recent Projects` | `"menu.file.max_recent_projects.aliases"` | -| `File`>`Save Mappings` | `"menu.file.mappings.save.aliases"` | -| `File`>`Auto Save Mappings` | `"menu.file.mappings.auto_save.aliases"` | -| `File`>`Close Mappings` | `"menu.file.mappings.close.aliases"` | -| `File`>`Drop Invalid Mappings` | `"menu.file.mappings.drop.aliases"` | -| `File`>`Reload Mappings` | `"menu.file.reload_mappings.aliases"` | -| `File`>`Reload Jar/Mappings` | `"menu.file.reload_all.aliases"` | -| `File`>`Export Source...` | `"menu.file.export.source.aliases"` | -| `File`>`Export Jar...` | `"menu.file.export.jar.aliases"` | -| `File`>`Mapping Stats...` | `"menu.file.stats.aliases"` | -| `File`>`Configure Keybinds...` | `"menu.file.configure_keybinds.aliases"` | -| `File`>`Exit` | `"menu.file.exit.aliases"` | -| `Open Recent Project` menu | `"menu.file.open_recent_project.aliases"` | -| `Save Mappings As...` menu | `"menu.file.mappings.save_as.aliases"` | -| `View` menu | `"menu.view.aliases"` | -| `Entry Tooltips` menu | `"menu.view.entry_tooltips.aliases"` | -| `Languages` menu | `"menu.view.languages.aliases"` | -| `Server Notifications` menu | `"menu.view.notifications.aliases"` | -| `Scale` menu | `"menu.view.scale.aliases"` | -| `Stat Icons` menu | `"menu.view.stat_icons.aliases"` | -| `Themes` menu | `"menu.view.themes.aliases"` | +| Element | Translation Key | +|------------------------------------------|-------------------------------------------| +| `Dev` menu | `"dev.menu.aliases"` | +| `Collab` menu | `"menu.collab.aliases"` | +| `Decompiler` menu | `"menu.decompiler.aliases"` | +| `Help` menu | `"menu.help.aliases"` | +| `Search` menu | `"menu.search.aliases"` | +| `Crash History` menu | `"menu.file.crash_history.aliases"` | +| `File` menu | `"menu.file.aliases"` | +| `File`>`Open Jar...` | `"menu.file.jar.open.aliases"` | +| `File`>`Close Jar` | `"menu.file.jar.close.aliases"` | +| `File`>`Open Mappings...` | `"menu.file.mappings.open.aliases"` | +| `File`>`Max Recent Projects` | `"menu.file.max_recent_projects.aliases"` | +| `File`>`Save Mappings` | `"menu.file.mappings.save.aliases"` | +| `File`>`Auto Save Mappings` | `"menu.file.mappings.auto_save.aliases"` | +| `File`>`Close Mappings` | `"menu.file.mappings.close.aliases"` | +| `File`>`Drop Invalid Mappings` | `"menu.file.mappings.drop.aliases"` | +| `File`>`Reload Mappings` | `"menu.file.reload_mappings.aliases"` | +| `File`>`Reload Jar/Mappings` | `"menu.file.reload_all.aliases"` | +| `File`>`Export Source...` | `"menu.file.export.source.aliases"` | +| `File`>`Export Jar...` | `"menu.file.export.jar.aliases"` | +| `File`>`Mapping Stats...` | `"menu.file.stats.aliases"` | +| `File`>`Configure Keybinds...` | `"menu.file.configure_keybinds.aliases"` | +| `File`>`Exit` | `"menu.file.exit.aliases"` | +| `Open Recent Project` menu | `"menu.file.open_recent_project.aliases"` | +| `Save Mappings As...` menu | `"menu.file.mappings.save_as.aliases"` | +| `Save Mappings As...`>`Enigma File` | `"enigma:enigma_file.aliases"` | +| `Save Mappings As...`>`Enigma Directory` | `"enigma:enigma_directory.aliases"` | +| `Save Mappings As...`>`Enigma ZIP` | `"enigma:enigma_zip.aliases"` | +| `Save Mappings As...`>`Tiny v2` | `"enigma:tiny_v2.aliases"` | +| `Save Mappings As...`>`SRG File` | `"enigma:srg_file.aliases"` | +| `View` menu | `"menu.view.aliases"` | +| `Entry Tooltips` menu | `"menu.view.entry_tooltips.aliases"` | +| `Languages` menu | `"menu.view.languages.aliases"` | +| `Server Notifications` menu | `"menu.view.notifications.aliases"` | +| `Scale` menu | `"menu.view.scale.aliases"` | +| `Stat Icons` menu | `"menu.view.stat_icons.aliases"` | +| `Themes` menu | `"menu.view.themes.aliases"` | diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/file/SaveMappingsAsMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/file/SaveMappingsAsMenu.java index 60a2c7a21..7f61e8071 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/file/SaveMappingsAsMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/file/SaveMappingsAsMenu.java @@ -5,11 +5,11 @@ import org.quiltmc.enigma.gui.Gui; import org.quiltmc.enigma.gui.config.Config; import org.quiltmc.enigma.gui.element.menu_bar.AbstractSearchableEnigmaMenu; +import org.quiltmc.enigma.gui.element.menu_bar.SimpleItem; import org.quiltmc.enigma.gui.util.ExtensionFileFilter; import org.quiltmc.enigma.util.I18n; import javax.swing.JFileChooser; -import javax.swing.JMenuItem; import java.io.File; import java.nio.file.Path; import java.util.HashMap; @@ -19,13 +19,13 @@ public class SaveMappingsAsMenu extends AbstractSearchableEnigmaMenu { private static final String TRANSLATION_KEY = "menu.file.mappings.save_as"; - private final Map items = new HashMap<>(); + private final Map items = new HashMap<>(); protected SaveMappingsAsMenu(Gui gui) { super(gui); this.forEachFormat(format -> { - JMenuItem item = new JMenuItem(); + final SimpleItem item = new SimpleItem(format.getId()); this.items.put(format, item); this.add(item); @@ -37,7 +37,7 @@ protected SaveMappingsAsMenu(Gui gui) { public void retranslate() { this.setText(I18n.translate(TRANSLATION_KEY)); - this.forEachFormat(format -> this.items.get(format).setText(I18n.translate(format.getId()))); + this.items.values().forEach(SimpleItem::retranslate); } @Override From 0c4fd4dcd9ab3b0aba9f3a69a51f45eea9b415d2 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 19 Nov 2025 18:18:07 -0800 Subject: [PATCH 070/124] use GuiUtil.syncStateWithConfig on FielMenu.autoSaveMappingsItem --- .../quiltmc/enigma/gui/element/menu_bar/file/FileMenu.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/file/FileMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/file/FileMenu.java index ef8dccf33..cac468fc5 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/file/FileMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/file/FileMenu.java @@ -11,6 +11,7 @@ import org.quiltmc.enigma.gui.element.menu_bar.SimpleCheckBoxItem; import org.quiltmc.enigma.gui.element.menu_bar.SimpleItem; import org.quiltmc.enigma.gui.util.ExtensionFileFilter; +import org.quiltmc.enigma.gui.util.GuiUtil; import org.quiltmc.enigma.util.I18n; import javax.swing.JFileChooser; @@ -51,6 +52,8 @@ public FileMenu(Gui gui) { this.crashHistory = new CrashHistoryMenu(gui); this.openRecent = new OpenRecentMenu(gui); + GuiUtil.syncStateWithConfig(this.autoSaveMappingsItem, Config.editor().autoSaveMappings); + this.add(this.jarOpenItem); this.add(this.jarCloseItem); this.addSeparator(); @@ -82,7 +85,6 @@ public FileMenu(Gui gui) { this.jarCloseItem.addActionListener(e -> this.gui.getController().closeJar()); this.maxRecentFilesItem.addActionListener(e -> this.onMaxRecentFilesClicked()); this.saveMappingsItem.addActionListener(e -> this.onSaveMappingsClicked()); - this.autoSaveMappingsItem.addActionListener(e -> Config.editor().autoSaveMappings.setValue(this.autoSaveMappingsItem.getState())); this.closeMappingsItem.addActionListener(e -> this.onCloseMappingsClicked()); this.dropMappingsItem.addActionListener(e -> this.gui.getController().dropMappings()); this.reloadMappingsItem.addActionListener(e -> this.onReloadMappingsClicked()); @@ -111,7 +113,6 @@ public void updateState(boolean jarOpen, ConnectionState state) { this.saveMappingsItem.setEnabled(jarOpen && this.gui.mappingsFileChooser.getSelectedFile() != null && this.gui.getConnectionState() != ConnectionState.CONNECTED); this.saveMappingsAs.updateState(); this.autoSaveMappingsItem.setEnabled(jarOpen); - this.autoSaveMappingsItem.setState(Config.editor().autoSaveMappings.value()); this.closeMappingsItem.setEnabled(jarOpen); this.reloadMappingsItem.setEnabled(jarOpen); this.reloadAllItem.setEnabled(jarOpen); From f095e97a7b5ad634f67cdff0d1aefefd402a52a7 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 19 Nov 2025 18:28:23 -0800 Subject: [PATCH 071/124] make EntryTooltipsMenu's sub-items searchable --- CONTRIBUTING.md | 78 ++++++++++--------- .../menu_bar/view/EntryTooltipsMenu.java | 16 ++-- 2 files changed, 49 insertions(+), 45 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8018a441b..fd9328ff3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -49,41 +49,43 @@ If you'd like to add search aliases to an element that doesn't already have alia its translation file. #### Complete list of search alias translation keys -| Element | Translation Key | -|------------------------------------------|-------------------------------------------| -| `Dev` menu | `"dev.menu.aliases"` | -| `Collab` menu | `"menu.collab.aliases"` | -| `Decompiler` menu | `"menu.decompiler.aliases"` | -| `Help` menu | `"menu.help.aliases"` | -| `Search` menu | `"menu.search.aliases"` | -| `Crash History` menu | `"menu.file.crash_history.aliases"` | -| `File` menu | `"menu.file.aliases"` | -| `File`>`Open Jar...` | `"menu.file.jar.open.aliases"` | -| `File`>`Close Jar` | `"menu.file.jar.close.aliases"` | -| `File`>`Open Mappings...` | `"menu.file.mappings.open.aliases"` | -| `File`>`Max Recent Projects` | `"menu.file.max_recent_projects.aliases"` | -| `File`>`Save Mappings` | `"menu.file.mappings.save.aliases"` | -| `File`>`Auto Save Mappings` | `"menu.file.mappings.auto_save.aliases"` | -| `File`>`Close Mappings` | `"menu.file.mappings.close.aliases"` | -| `File`>`Drop Invalid Mappings` | `"menu.file.mappings.drop.aliases"` | -| `File`>`Reload Mappings` | `"menu.file.reload_mappings.aliases"` | -| `File`>`Reload Jar/Mappings` | `"menu.file.reload_all.aliases"` | -| `File`>`Export Source...` | `"menu.file.export.source.aliases"` | -| `File`>`Export Jar...` | `"menu.file.export.jar.aliases"` | -| `File`>`Mapping Stats...` | `"menu.file.stats.aliases"` | -| `File`>`Configure Keybinds...` | `"menu.file.configure_keybinds.aliases"` | -| `File`>`Exit` | `"menu.file.exit.aliases"` | -| `Open Recent Project` menu | `"menu.file.open_recent_project.aliases"` | -| `Save Mappings As...` menu | `"menu.file.mappings.save_as.aliases"` | -| `Save Mappings As...`>`Enigma File` | `"enigma:enigma_file.aliases"` | -| `Save Mappings As...`>`Enigma Directory` | `"enigma:enigma_directory.aliases"` | -| `Save Mappings As...`>`Enigma ZIP` | `"enigma:enigma_zip.aliases"` | -| `Save Mappings As...`>`Tiny v2` | `"enigma:tiny_v2.aliases"` | -| `Save Mappings As...`>`SRG File` | `"enigma:srg_file.aliases"` | -| `View` menu | `"menu.view.aliases"` | -| `Entry Tooltips` menu | `"menu.view.entry_tooltips.aliases"` | -| `Languages` menu | `"menu.view.languages.aliases"` | -| `Server Notifications` menu | `"menu.view.notifications.aliases"` | -| `Scale` menu | `"menu.view.scale.aliases"` | -| `Stat Icons` menu | `"menu.view.stat_icons.aliases"` | -| `Themes` menu | `"menu.view.themes.aliases"` | +| Element | Translation Key | +|----------------------------------------------|---------------------------------------------------| +| `Dev` menu | `"dev.menu.aliases"` | +| `Collab` menu | `"menu.collab.aliases"` | +| `Decompiler` menu | `"menu.decompiler.aliases"` | +| `Help` menu | `"menu.help.aliases"` | +| `Search` menu | `"menu.search.aliases"` | +| `Crash History` menu | `"menu.file.crash_history.aliases"` | +| `File` menu | `"menu.file.aliases"` | +| `File`>`Open Jar...` | `"menu.file.jar.open.aliases"` | +| `File`>`Close Jar` | `"menu.file.jar.close.aliases"` | +| `File`>`Open Mappings...` | `"menu.file.mappings.open.aliases"` | +| `File`>`Max Recent Projects` | `"menu.file.max_recent_projects.aliases"` | +| `File`>`Save Mappings` | `"menu.file.mappings.save.aliases"` | +| `File`>`Auto Save Mappings` | `"menu.file.mappings.auto_save.aliases"` | +| `File`>`Close Mappings` | `"menu.file.mappings.close.aliases"` | +| `File`>`Drop Invalid Mappings` | `"menu.file.mappings.drop.aliases"` | +| `File`>`Reload Mappings` | `"menu.file.reload_mappings.aliases"` | +| `File`>`Reload Jar/Mappings` | `"menu.file.reload_all.aliases"` | +| `File`>`Export Source...` | `"menu.file.export.source.aliases"` | +| `File`>`Export Jar...` | `"menu.file.export.jar.aliases"` | +| `File`>`Mapping Stats...` | `"menu.file.stats.aliases"` | +| `File`>`Configure Keybinds...` | `"menu.file.configure_keybinds.aliases"` | +| `File`>`Exit` | `"menu.file.exit.aliases"` | +| `Open Recent Project` menu | `"menu.file.open_recent_project.aliases"` | +| `Save Mappings As...` menu | `"menu.file.mappings.save_as.aliases"` | +| `Save Mappings As...`>`Enigma File` | `"enigma:enigma_file.aliases"` | +| `Save Mappings As...`>`Enigma Directory` | `"enigma:enigma_directory.aliases"` | +| `Save Mappings As...`>`Enigma ZIP` | `"enigma:enigma_zip.aliases"` | +| `Save Mappings As...`>`Tiny v2` | `"enigma:tiny_v2.aliases"` | +| `Save Mappings As...`>`SRG File` | `"enigma:srg_file.aliases"` | +| `View` menu | `"menu.view.aliases"` | +| `Entry Tooltips` menu | `"menu.view.entry_tooltips.aliases"` | +| `Entry Tooltips`>`Enable tooltips` | `"menu.view.entry_tooltips.enable.aliases"` | +| `Entry Tooltips`>`Allow tooltip interaction` | `"menu.view.entry_tooltips.interactable.aliases"` | +| `Languages` menu | `"menu.view.languages.aliases"` | +| `Server Notifications` menu | `"menu.view.notifications.aliases"` | +| `Scale` menu | `"menu.view.scale.aliases"` | +| `Stat Icons` menu | `"menu.view.stat_icons.aliases"` | +| `Themes` menu | `"menu.view.themes.aliases"` | diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/EntryTooltipsMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/EntryTooltipsMenu.java index a3cdc3d25..1791bc041 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/EntryTooltipsMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/EntryTooltipsMenu.java @@ -3,21 +3,23 @@ import org.quiltmc.enigma.gui.Gui; import org.quiltmc.enigma.gui.config.Config; import org.quiltmc.enigma.gui.element.menu_bar.AbstractSearchableEnigmaMenu; +import org.quiltmc.enigma.gui.element.menu_bar.SimpleCheckBoxItem; import org.quiltmc.enigma.util.I18n; -import javax.swing.JCheckBoxMenuItem; - -import static org.quiltmc.enigma.gui.util.GuiUtil.createSyncedMenuCheckBox; +import static org.quiltmc.enigma.gui.util.GuiUtil.syncStateWithConfig; public class EntryTooltipsMenu extends AbstractSearchableEnigmaMenu { private static final String TRANSLATION_KEY = "menu.view.entry_tooltips"; - private final JCheckBoxMenuItem enable = createSyncedMenuCheckBox(Config.editor().entryTooltips.enable); - private final JCheckBoxMenuItem interactable = createSyncedMenuCheckBox(Config.editor().entryTooltips.interactable); + private final SimpleCheckBoxItem enable = new SimpleCheckBoxItem("menu.view.entry_tooltips.enable"); + private final SimpleCheckBoxItem interactable = new SimpleCheckBoxItem("menu.view.entry_tooltips.interactable"); protected EntryTooltipsMenu(Gui gui) { super(gui); + syncStateWithConfig(this.enable, Config.editor().entryTooltips.enable); + syncStateWithConfig(this.interactable, Config.editor().entryTooltips.interactable); + this.add(this.enable); this.add(this.interactable); @@ -27,8 +29,8 @@ protected EntryTooltipsMenu(Gui gui) { @Override public void retranslate() { this.setText(I18n.translate(TRANSLATION_KEY)); - this.enable.setText(I18n.translate("menu.view.entry_tooltips.enable")); - this.interactable.setText(I18n.translate("menu.view.entry_tooltips.interactable")); + this.enable.retranslate(); + this.interactable.retranslate(); } @Override From c738eb9f086633ea44b4d434dd69696c1eb508e0 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 19 Nov 2025 19:16:50 -0800 Subject: [PATCH 072/124] eliminate unecessary Simple...Item super classes make LanguagesMenu language items searchable add aliases for some non-english languages to english translation --- CONTRIBUTING.md | 7 +++- .../menu_bar/SearchableCheckBoxItem.java | 26 ------------- .../gui/element/menu_bar/SearchableItem.java | 26 ------------- .../element/menu_bar/SimpleCheckBoxItem.java | 25 ++++++++++-- .../gui/element/menu_bar/SimpleItem.java | 23 ++++++++++- .../element/menu_bar/view/LanguagesMenu.java | 38 ++++++++++++++++--- enigma/src/main/resources/lang/en_us.json | 4 ++ 7 files changed, 86 insertions(+), 63 deletions(-) delete mode 100644 enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchableCheckBoxItem.java delete mode 100644 enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchableItem.java diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fd9328ff3..6a70020c4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -81,10 +81,15 @@ its translation file. | `Save Mappings As...`>`Tiny v2` | `"enigma:tiny_v2.aliases"` | | `Save Mappings As...`>`SRG File` | `"enigma:srg_file.aliases"` | | `View` menu | `"menu.view.aliases"` | +| `View`>`Languages` menu | `"menu.view.languages.aliases"` | +| `View`>`Languages`>`German` | `language.de_de.aliases` | +| `View`>`Languages`>`English` | `language.en_us.aliases` | +| `View`>`Languages`>`Français` | `language.fr_fr.aliases` | +| `View`>`Languages`>`日本語` | `language.ja_jp.aliases` | +| `View`>`Languages`>`简体中文` | `language.zh_cn.aliases` | | `Entry Tooltips` menu | `"menu.view.entry_tooltips.aliases"` | | `Entry Tooltips`>`Enable tooltips` | `"menu.view.entry_tooltips.enable.aliases"` | | `Entry Tooltips`>`Allow tooltip interaction` | `"menu.view.entry_tooltips.interactable.aliases"` | -| `Languages` menu | `"menu.view.languages.aliases"` | | `Server Notifications` menu | `"menu.view.notifications.aliases"` | | `Scale` menu | `"menu.view.scale.aliases"` | | `Stat Icons` menu | `"menu.view.stat_icons.aliases"` | diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchableCheckBoxItem.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchableCheckBoxItem.java deleted file mode 100644 index 86213c857..000000000 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchableCheckBoxItem.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.quiltmc.enigma.gui.element.menu_bar; - -import javax.swing.JCheckBoxMenuItem; - -public class SearchableCheckBoxItem extends JCheckBoxMenuItem implements SearchableElement { - private final String aliasesTranslationKeyPrefix; - - public SearchableCheckBoxItem(String aliasesTranslationKeyPrefix) { - this.aliasesTranslationKeyPrefix = aliasesTranslationKeyPrefix; - } - - @Override - public String getSearchName() { - return this.getText(); - } - - @Override - public String getAliasesTranslationKeyPrefix() { - return this.aliasesTranslationKeyPrefix; - } - - @Override - public void onSearchClicked() { - this.doClick(); - } -} diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchableItem.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchableItem.java deleted file mode 100644 index 3d2bf47a4..000000000 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchableItem.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.quiltmc.enigma.gui.element.menu_bar; - -import javax.swing.JMenuItem; - -public class SearchableItem extends JMenuItem implements SearchableElement { - private final String aliasesTranslationKeyPrefix; - - public SearchableItem(String aliasesTranslationKeyPrefix) { - this.aliasesTranslationKeyPrefix = aliasesTranslationKeyPrefix; - } - - @Override - public String getSearchName() { - return this.getText(); - } - - @Override - public String getAliasesTranslationKeyPrefix() { - return this.aliasesTranslationKeyPrefix; - } - - @Override - public void onSearchClicked() { - this.doClick(); - } -} diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SimpleCheckBoxItem.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SimpleCheckBoxItem.java index e8309a50c..62e9e641e 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SimpleCheckBoxItem.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SimpleCheckBoxItem.java @@ -2,13 +2,32 @@ import org.quiltmc.enigma.util.I18n; -public class SimpleCheckBoxItem extends SearchableCheckBoxItem implements Retranslatable { +import javax.swing.JCheckBoxMenuItem; + +public class SimpleCheckBoxItem extends JCheckBoxMenuItem implements SearchableElement, Retranslatable { + private final String translationKey; + public SimpleCheckBoxItem(String translationKey) { - super(translationKey); + this.translationKey = translationKey; } @Override public void retranslate() { - this.setText(I18n.translate(this.getAliasesTranslationKeyPrefix())); + this.setText(I18n.translate(this.translationKey)); + } + + @Override + public String getSearchName() { + return this.getText(); + } + + @Override + public String getAliasesTranslationKeyPrefix() { + return this.translationKey; + } + + @Override + public void onSearchClicked() { + this.doClick(); } } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SimpleItem.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SimpleItem.java index 64577c200..bc82b1cf4 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SimpleItem.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SimpleItem.java @@ -2,13 +2,32 @@ import org.quiltmc.enigma.util.I18n; -public class SimpleItem extends SearchableItem implements Retranslatable { +import javax.swing.JMenuItem; + +public class SimpleItem extends JMenuItem implements SearchableElement, Retranslatable { + private final String translationKey; + public SimpleItem(String translationKey) { - super(translationKey); + this.translationKey = translationKey; } @Override public void retranslate() { this.setText(I18n.translate(this.getAliasesTranslationKeyPrefix())); } + + @Override + public String getSearchName() { + return this.getText(); + } + + @Override + public String getAliasesTranslationKeyPrefix() { + return this.translationKey; + } + + @Override + public void onSearchClicked() { + this.doClick(); + } } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/LanguagesMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/LanguagesMenu.java index c7a2bd2a7..f9b98b15e 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/LanguagesMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/LanguagesMenu.java @@ -4,6 +4,8 @@ import org.quiltmc.enigma.gui.Gui; import org.quiltmc.enigma.gui.config.Config; import org.quiltmc.enigma.gui.element.menu_bar.AbstractSearchableEnigmaMenu; +import org.quiltmc.enigma.gui.element.menu_bar.Retranslatable; +import org.quiltmc.enigma.gui.element.menu_bar.SearchableElement; import org.quiltmc.enigma.gui.util.LanguageUtil; import org.quiltmc.enigma.util.I18n; @@ -15,14 +17,14 @@ public class LanguagesMenu extends AbstractSearchableEnigmaMenu { private static final String TRANSLATION_KEY = "menu.view.languages"; - private final Map languages = new HashMap<>(); + private final Map languages = new HashMap<>(); protected LanguagesMenu(Gui gui) { super(gui); ButtonGroup languageButtons = new ButtonGroup(); for (String lang : I18n.getAvailableLanguages()) { - JRadioButtonMenuItem languageButton = new JRadioButtonMenuItem(I18n.getLanguageName(lang)); + LanguageItem languageButton = new LanguageItem(lang); this.languages.put(lang, languageButton); languageButtons.add(languageButton); this.add(languageButton); @@ -35,9 +37,7 @@ protected LanguagesMenu(Gui gui) { public void retranslate() { this.setText(I18n.translate(TRANSLATION_KEY)); - for (String lang : I18n.getAvailableLanguages()) { - this.languages.get(lang).setText(I18n.getLanguageName(lang)); - } + this.languages.values().forEach(LanguageItem::retranslate); } @Override @@ -59,4 +59,32 @@ private void onLanguageButtonClicked(String lang) { public String getAliasesTranslationKeyPrefix() { return TRANSLATION_KEY; } + + private static final class LanguageItem extends JRadioButtonMenuItem implements SearchableElement, Retranslatable { + private final String language; + + LanguageItem(String language) { + this.language = language; + } + + @Override + public void retranslate() { + this.setText(I18n.getLanguageName(this.language)); + } + + @Override + public String getSearchName() { + return this.getText(); + } + + @Override + public String getAliasesTranslationKeyPrefix() { + return "language." + this.language; + } + + @Override + public void onSearchClicked() { + this.doClick(); + } + } } diff --git a/enigma/src/main/resources/lang/en_us.json b/enigma/src/main/resources/lang/en_us.json index 3cdbfcd50..b31f457b7 100644 --- a/enigma/src/main/resources/lang/en_us.json +++ b/enigma/src/main/resources/lang/en_us.json @@ -1,6 +1,10 @@ { "language": "English", + "language.fr_fr.aliases": "French", + "language.ja_jp.aliases": "Japanese", + "language.zh_cn.aliases": "Chinese;Simplified Chinese", + "enigma:enigma_file": "Enigma File", "enigma:enigma_directory": "Enigma Directory", "enigma:enigma_zip": "Enigma ZIP", From 7ce1371310676327dff2186fbe732f484b903af9 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 19 Nov 2025 19:33:02 -0800 Subject: [PATCH 073/124] make NotificationsMenu level items searchable --- CONTRIBUTING.md | 93 ++++++++++--------- .../enigma/gui/NotificationManager.java | 4 +- .../gui/element/menu_bar/SimpleRadioItem.java | 33 +++++++ .../menu_bar/view/NotificationsMenu.java | 10 +- 4 files changed, 87 insertions(+), 53 deletions(-) create mode 100644 enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SimpleRadioItem.java diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6a70020c4..de9a54701 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -49,48 +49,51 @@ If you'd like to add search aliases to an element that doesn't already have alia its translation file. #### Complete list of search alias translation keys -| Element | Translation Key | -|----------------------------------------------|---------------------------------------------------| -| `Dev` menu | `"dev.menu.aliases"` | -| `Collab` menu | `"menu.collab.aliases"` | -| `Decompiler` menu | `"menu.decompiler.aliases"` | -| `Help` menu | `"menu.help.aliases"` | -| `Search` menu | `"menu.search.aliases"` | -| `Crash History` menu | `"menu.file.crash_history.aliases"` | -| `File` menu | `"menu.file.aliases"` | -| `File`>`Open Jar...` | `"menu.file.jar.open.aliases"` | -| `File`>`Close Jar` | `"menu.file.jar.close.aliases"` | -| `File`>`Open Mappings...` | `"menu.file.mappings.open.aliases"` | -| `File`>`Max Recent Projects` | `"menu.file.max_recent_projects.aliases"` | -| `File`>`Save Mappings` | `"menu.file.mappings.save.aliases"` | -| `File`>`Auto Save Mappings` | `"menu.file.mappings.auto_save.aliases"` | -| `File`>`Close Mappings` | `"menu.file.mappings.close.aliases"` | -| `File`>`Drop Invalid Mappings` | `"menu.file.mappings.drop.aliases"` | -| `File`>`Reload Mappings` | `"menu.file.reload_mappings.aliases"` | -| `File`>`Reload Jar/Mappings` | `"menu.file.reload_all.aliases"` | -| `File`>`Export Source...` | `"menu.file.export.source.aliases"` | -| `File`>`Export Jar...` | `"menu.file.export.jar.aliases"` | -| `File`>`Mapping Stats...` | `"menu.file.stats.aliases"` | -| `File`>`Configure Keybinds...` | `"menu.file.configure_keybinds.aliases"` | -| `File`>`Exit` | `"menu.file.exit.aliases"` | -| `Open Recent Project` menu | `"menu.file.open_recent_project.aliases"` | -| `Save Mappings As...` menu | `"menu.file.mappings.save_as.aliases"` | -| `Save Mappings As...`>`Enigma File` | `"enigma:enigma_file.aliases"` | -| `Save Mappings As...`>`Enigma Directory` | `"enigma:enigma_directory.aliases"` | -| `Save Mappings As...`>`Enigma ZIP` | `"enigma:enigma_zip.aliases"` | -| `Save Mappings As...`>`Tiny v2` | `"enigma:tiny_v2.aliases"` | -| `Save Mappings As...`>`SRG File` | `"enigma:srg_file.aliases"` | -| `View` menu | `"menu.view.aliases"` | -| `View`>`Languages` menu | `"menu.view.languages.aliases"` | -| `View`>`Languages`>`German` | `language.de_de.aliases` | -| `View`>`Languages`>`English` | `language.en_us.aliases` | -| `View`>`Languages`>`Français` | `language.fr_fr.aliases` | -| `View`>`Languages`>`日本語` | `language.ja_jp.aliases` | -| `View`>`Languages`>`简体中文` | `language.zh_cn.aliases` | -| `Entry Tooltips` menu | `"menu.view.entry_tooltips.aliases"` | -| `Entry Tooltips`>`Enable tooltips` | `"menu.view.entry_tooltips.enable.aliases"` | -| `Entry Tooltips`>`Allow tooltip interaction` | `"menu.view.entry_tooltips.interactable.aliases"` | -| `Server Notifications` menu | `"menu.view.notifications.aliases"` | -| `Scale` menu | `"menu.view.scale.aliases"` | -| `Stat Icons` menu | `"menu.view.stat_icons.aliases"` | -| `Themes` menu | `"menu.view.themes.aliases"` | +| Element | Translation Key | +|----------------------------------------------------------|---------------------------------------------------| +| `Dev` menu | `"dev.menu.aliases"` | +| `Collab` menu | `"menu.collab.aliases"` | +| `Decompiler` menu | `"menu.decompiler.aliases"` | +| `Help` menu | `"menu.help.aliases"` | +| `Search` menu | `"menu.search.aliases"` | +| `Crash History` menu | `"menu.file.crash_history.aliases"` | +| `File` menu | `"menu.file.aliases"` | +| `File`>`Open Jar...` | `"menu.file.jar.open.aliases"` | +| `File`>`Close Jar` | `"menu.file.jar.close.aliases"` | +| `File`>`Open Mappings...` | `"menu.file.mappings.open.aliases"` | +| `File`>`Max Recent Projects` | `"menu.file.max_recent_projects.aliases"` | +| `File`>`Save Mappings` | `"menu.file.mappings.save.aliases"` | +| `File`>`Auto Save Mappings` | `"menu.file.mappings.auto_save.aliases"` | +| `File`>`Close Mappings` | `"menu.file.mappings.close.aliases"` | +| `File`>`Drop Invalid Mappings` | `"menu.file.mappings.drop.aliases"` | +| `File`>`Reload Mappings` | `"menu.file.reload_mappings.aliases"` | +| `File`>`Reload Jar/Mappings` | `"menu.file.reload_all.aliases"` | +| `File`>`Export Source...` | `"menu.file.export.source.aliases"` | +| `File`>`Export Jar...` | `"menu.file.export.jar.aliases"` | +| `File`>`Mapping Stats...` | `"menu.file.stats.aliases"` | +| `File`>`Configure Keybinds...` | `"menu.file.configure_keybinds.aliases"` | +| `File`>`Exit` | `"menu.file.exit.aliases"` | +| `Open Recent Project` menu | `"menu.file.open_recent_project.aliases"` | +| `Save Mappings As...` menu | `"menu.file.mappings.save_as.aliases"` | +| `Save Mappings As...`>`Enigma File` | `"enigma:enigma_file.aliases"` | +| `Save Mappings As...`>`Enigma Directory` | `"enigma:enigma_directory.aliases"` | +| `Save Mappings As...`>`Enigma ZIP` | `"enigma:enigma_zip.aliases"` | +| `Save Mappings As...`>`Tiny v2` | `"enigma:tiny_v2.aliases"` | +| `Save Mappings As...`>`SRG File` | `"enigma:srg_file.aliases"` | +| `View` menu | `"menu.view.aliases"` | +| `View`>`Languages` menu | `"menu.view.languages.aliases"` | +| `View`>`Languages`>`German` | `language.de_de.aliases` | +| `View`>`Languages`>`English` | `language.en_us.aliases` | +| `View`>`Languages`>`Français` | `language.fr_fr.aliases` | +| `View`>`Languages`>`日本語` | `language.ja_jp.aliases` | +| `View`>`Languages`>`简体中文` | `language.zh_cn.aliases` | +| `View`>`Server Notifications` menu | `"menu.view.notifications.aliases"` | +| `View`>`Server Notifications`>`No server notifications` | `"notification.level.none.aliases"` | +| `View`>`Server Notifications`>`No chat messages` | `"notification.level.no_chat.aliases"` | +| `View`>`Server Notifications`>`All server notifications` | `"notification.level.full.aliases"` | +| `Entry Tooltips` menu | `"menu.view.entry_tooltips.aliases"` | +| `Entry Tooltips`>`Enable tooltips` | `"menu.view.entry_tooltips.enable.aliases"` | +| `Entry Tooltips`>`Allow tooltip interaction` | `"menu.view.entry_tooltips.interactable.aliases"` | +| `Scale` menu | `"menu.view.scale.aliases"` | +| `Stat Icons` menu | `"menu.view.stat_icons.aliases"` | +| `Themes` menu | `"menu.view.themes.aliases"` | diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/NotificationManager.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/NotificationManager.java index 2c67b53cd..d9b511d25 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/NotificationManager.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/NotificationManager.java @@ -139,8 +139,8 @@ public enum ServerNotificationLevel { NO_CHAT, FULL; - public String getText() { - return I18n.translate("notification.level." + this.name().toLowerCase()); + public String getTranslationKey() { + return "notification.level." + this.name().toLowerCase(); } } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SimpleRadioItem.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SimpleRadioItem.java new file mode 100644 index 000000000..871cf0a0e --- /dev/null +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SimpleRadioItem.java @@ -0,0 +1,33 @@ +package org.quiltmc.enigma.gui.element.menu_bar; + +import org.quiltmc.enigma.util.I18n; + +import javax.swing.JRadioButtonMenuItem; + +public class SimpleRadioItem extends JRadioButtonMenuItem implements SearchableElement, Retranslatable { + private final String translationKey; + + public SimpleRadioItem(String translationKey) { + this.translationKey = translationKey; + } + + @Override + public String getSearchName() { + return this.getText(); + } + + @Override + public String getAliasesTranslationKeyPrefix() { + return this.translationKey; + } + + @Override + public void onSearchClicked() { + this.doClick(); + } + + @Override + public void retranslate() { + this.setText(I18n.translate(this.translationKey)); + } +} diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/NotificationsMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/NotificationsMenu.java index e8e11c330..4562f15f5 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/NotificationsMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/NotificationsMenu.java @@ -4,10 +4,10 @@ import org.quiltmc.enigma.gui.Gui; import org.quiltmc.enigma.gui.config.Config; import org.quiltmc.enigma.gui.element.menu_bar.AbstractSearchableEnigmaMenu; +import org.quiltmc.enigma.gui.element.menu_bar.SimpleRadioItem; import org.quiltmc.enigma.util.I18n; import javax.swing.ButtonGroup; -import javax.swing.JRadioButtonMenuItem; import java.util.HashMap; import java.util.Map; @@ -16,14 +16,14 @@ public class NotificationsMenu extends AbstractSearchableEnigmaMenu { private static final String TRANSLATION_KEY = "menu.view.notifications"; - private final Map buttons = new HashMap<>(); + private final Map buttons = new HashMap<>(); public NotificationsMenu(Gui gui) { super(gui); ButtonGroup buttonGroup = new ButtonGroup(); for (ServerNotificationLevel level : ServerNotificationLevel.values()) { - JRadioButtonMenuItem notificationsButton = new JRadioButtonMenuItem(); + SimpleRadioItem notificationsButton = new SimpleRadioItem(level.getTranslationKey()); buttonGroup.add(notificationsButton); this.buttons.put(level, notificationsButton); notificationsButton.addActionListener(event -> Config.main().serverNotificationLevel.setValue(level, true)); @@ -35,9 +35,7 @@ public NotificationsMenu(Gui gui) { public void retranslate() { this.setText(I18n.translate(TRANSLATION_KEY)); - for (ServerNotificationLevel level : ServerNotificationLevel.values()) { - this.buttons.get(level).setText(level.getText()); - } + this.buttons.values().forEach(SimpleRadioItem::retranslate); } @Override From 4e59262edc94b146e736d5c7a14ee77b9dd3cea7 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 19 Nov 2025 20:00:42 -0800 Subject: [PATCH 074/124] make StatsMenu and TypeMenu sub-itmes searchable --- CONTRIBUTING.md | 104 ++++++++++-------- .../gui/element/menu_bar/view/StatsMenu.java | 99 ++++++++++------- 2 files changed, 117 insertions(+), 86 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index de9a54701..79b5f2932 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -49,51 +49,59 @@ If you'd like to add search aliases to an element that doesn't already have alia its translation file. #### Complete list of search alias translation keys -| Element | Translation Key | -|----------------------------------------------------------|---------------------------------------------------| -| `Dev` menu | `"dev.menu.aliases"` | -| `Collab` menu | `"menu.collab.aliases"` | -| `Decompiler` menu | `"menu.decompiler.aliases"` | -| `Help` menu | `"menu.help.aliases"` | -| `Search` menu | `"menu.search.aliases"` | -| `Crash History` menu | `"menu.file.crash_history.aliases"` | -| `File` menu | `"menu.file.aliases"` | -| `File`>`Open Jar...` | `"menu.file.jar.open.aliases"` | -| `File`>`Close Jar` | `"menu.file.jar.close.aliases"` | -| `File`>`Open Mappings...` | `"menu.file.mappings.open.aliases"` | -| `File`>`Max Recent Projects` | `"menu.file.max_recent_projects.aliases"` | -| `File`>`Save Mappings` | `"menu.file.mappings.save.aliases"` | -| `File`>`Auto Save Mappings` | `"menu.file.mappings.auto_save.aliases"` | -| `File`>`Close Mappings` | `"menu.file.mappings.close.aliases"` | -| `File`>`Drop Invalid Mappings` | `"menu.file.mappings.drop.aliases"` | -| `File`>`Reload Mappings` | `"menu.file.reload_mappings.aliases"` | -| `File`>`Reload Jar/Mappings` | `"menu.file.reload_all.aliases"` | -| `File`>`Export Source...` | `"menu.file.export.source.aliases"` | -| `File`>`Export Jar...` | `"menu.file.export.jar.aliases"` | -| `File`>`Mapping Stats...` | `"menu.file.stats.aliases"` | -| `File`>`Configure Keybinds...` | `"menu.file.configure_keybinds.aliases"` | -| `File`>`Exit` | `"menu.file.exit.aliases"` | -| `Open Recent Project` menu | `"menu.file.open_recent_project.aliases"` | -| `Save Mappings As...` menu | `"menu.file.mappings.save_as.aliases"` | -| `Save Mappings As...`>`Enigma File` | `"enigma:enigma_file.aliases"` | -| `Save Mappings As...`>`Enigma Directory` | `"enigma:enigma_directory.aliases"` | -| `Save Mappings As...`>`Enigma ZIP` | `"enigma:enigma_zip.aliases"` | -| `Save Mappings As...`>`Tiny v2` | `"enigma:tiny_v2.aliases"` | -| `Save Mappings As...`>`SRG File` | `"enigma:srg_file.aliases"` | -| `View` menu | `"menu.view.aliases"` | -| `View`>`Languages` menu | `"menu.view.languages.aliases"` | -| `View`>`Languages`>`German` | `language.de_de.aliases` | -| `View`>`Languages`>`English` | `language.en_us.aliases` | -| `View`>`Languages`>`Français` | `language.fr_fr.aliases` | -| `View`>`Languages`>`日本語` | `language.ja_jp.aliases` | -| `View`>`Languages`>`简体中文` | `language.zh_cn.aliases` | -| `View`>`Server Notifications` menu | `"menu.view.notifications.aliases"` | -| `View`>`Server Notifications`>`No server notifications` | `"notification.level.none.aliases"` | -| `View`>`Server Notifications`>`No chat messages` | `"notification.level.no_chat.aliases"` | -| `View`>`Server Notifications`>`All server notifications` | `"notification.level.full.aliases"` | -| `Entry Tooltips` menu | `"menu.view.entry_tooltips.aliases"` | -| `Entry Tooltips`>`Enable tooltips` | `"menu.view.entry_tooltips.enable.aliases"` | -| `Entry Tooltips`>`Allow tooltip interaction` | `"menu.view.entry_tooltips.interactable.aliases"` | -| `Scale` menu | `"menu.view.scale.aliases"` | -| `Stat Icons` menu | `"menu.view.stat_icons.aliases"` | -| `Themes` menu | `"menu.view.themes.aliases"` | +| Element | Translation Key | +|----------------------------------------------------------|----------------------------------------------------| +| `Dev` menu | `"dev.menu.aliases"` | +| `Collab` menu | `"menu.collab.aliases"` | +| `Decompiler` menu | `"menu.decompiler.aliases"` | +| `Help` menu | `"menu.help.aliases"` | +| `Search` menu | `"menu.search.aliases"` | +| `Crash History` menu | `"menu.file.crash_history.aliases"` | +| `File` menu | `"menu.file.aliases"` | +| `File`>`Open Jar...` | `"menu.file.jar.open.aliases"` | +| `File`>`Close Jar` | `"menu.file.jar.close.aliases"` | +| `File`>`Open Mappings...` | `"menu.file.mappings.open.aliases"` | +| `File`>`Max Recent Projects` | `"menu.file.max_recent_projects.aliases"` | +| `File`>`Save Mappings` | `"menu.file.mappings.save.aliases"` | +| `File`>`Auto Save Mappings` | `"menu.file.mappings.auto_save.aliases"` | +| `File`>`Close Mappings` | `"menu.file.mappings.close.aliases"` | +| `File`>`Drop Invalid Mappings` | `"menu.file.mappings.drop.aliases"` | +| `File`>`Reload Mappings` | `"menu.file.reload_mappings.aliases"` | +| `File`>`Reload Jar/Mappings` | `"menu.file.reload_all.aliases"` | +| `File`>`Export Source...` | `"menu.file.export.source.aliases"` | +| `File`>`Export Jar...` | `"menu.file.export.jar.aliases"` | +| `File`>`Mapping Stats...` | `"menu.file.stats.aliases"` | +| `File`>`Configure Keybinds...` | `"menu.file.configure_keybinds.aliases"` | +| `File`>`Exit` | `"menu.file.exit.aliases"` | +| `Open Recent Project` menu | `"menu.file.open_recent_project.aliases"` | +| `Save Mappings As...` menu | `"menu.file.mappings.save_as.aliases"` | +| `Save Mappings As...`>`Enigma File` | `"enigma:enigma_file.aliases"` | +| `Save Mappings As...`>`Enigma Directory` | `"enigma:enigma_directory.aliases"` | +| `Save Mappings As...`>`Enigma ZIP` | `"enigma:enigma_zip.aliases"` | +| `Save Mappings As...`>`Tiny v2` | `"enigma:tiny_v2.aliases"` | +| `Save Mappings As...`>`SRG File` | `"enigma:srg_file.aliases"` | +| `View` menu | `"menu.view.aliases"` | +| `View`>`Languages` menu | `"menu.view.languages.aliases"` | +| `View`>`Languages`>`German` | `language.de_de.aliases` | +| `View`>`Languages`>`English` | `language.en_us.aliases` | +| `View`>`Languages`>`Français` | `language.fr_fr.aliases` | +| `View`>`Languages`>`日本語` | `language.ja_jp.aliases` | +| `View`>`Languages`>`简体中文` | `language.zh_cn.aliases` | +| `View`>`Server Notifications` menu | `"menu.view.notifications.aliases"` | +| `View`>`Server Notifications`>`No server notifications` | `"notification.level.none.aliases"` | +| `View`>`Server Notifications`>`No chat messages` | `"notification.level.no_chat.aliases"` | +| `View`>`Server Notifications`>`All server notifications` | `"notification.level.full.aliases"` | +| `View`>`Stat Icons` menu | `"menu.view.stat_icons.aliases"` | +| `View`>`Stat Icons`>`Include synthetic parameters` | `"menu.view.stat_icons.include_synthetic.aliases"` | +| `View`>`Stat Icons`>`Count fallback-proposed names` | `"menu.view.stat_icons.count_fallback.aliases"` | +| `View`>`Stat Icons`>`Enable icons` | `"menu.view.stat_icons.enable_icons.aliases"` | +| `View`>`Stat Icons`>`Included types` menu | `"menu.view.stat_icons.included_types.aliases"` | +| `View`>`Stat Icons`>`Included types`>`Methods` | `"type.methods"` | +| `View`>`Stat Icons`>`Included types`>`Fields` | `"type.fields"` | +| `View`>`Stat Icons`>`Included types`>`Parameters` | `"type.parameters"` | +| `View`>`Stat Icons`>`Included types`>`Classes` | `"type.classes"` | +| `Entry Tooltips` menu | `"menu.view.entry_tooltips.aliases"` | +| `Entry Tooltips`>`Enable tooltips` | `"menu.view.entry_tooltips.enable.aliases"` | +| `Entry Tooltips`>`Allow tooltip interaction` | `"menu.view.entry_tooltips.interactable.aliases"` | +| `Scale` menu | `"menu.view.scale.aliases"` | +| `Themes` menu | `"menu.view.themes.aliases"` | diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/StatsMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/StatsMenu.java index 13ba9bbdf..c94610e27 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/StatsMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/StatsMenu.java @@ -5,10 +5,10 @@ import org.quiltmc.enigma.gui.Gui; import org.quiltmc.enigma.gui.config.Config; import org.quiltmc.enigma.gui.element.menu_bar.AbstractSearchableEnigmaMenu; +import org.quiltmc.enigma.gui.element.menu_bar.SimpleCheckBoxItem; import org.quiltmc.enigma.util.I18n; import javax.swing.JCheckBoxMenuItem; -import javax.swing.JMenu; import javax.swing.SwingUtilities; import java.util.HashMap; import java.util.Map; @@ -18,11 +18,10 @@ public class StatsMenu extends AbstractSearchableEnigmaMenu { private static final String TRANSLATION_KEY = "menu.view.stat_icons"; - private final JCheckBoxMenuItem enableIcons = new JCheckBoxMenuItem(); - private final JCheckBoxMenuItem includeSynthetic = new JCheckBoxMenuItem(); - private final JCheckBoxMenuItem countFallback = new JCheckBoxMenuItem(); - private final JMenu statTypes = new JMenu(); - private final Map statTypeItems = new HashMap<>(); + private final SimpleCheckBoxItem enableIcons = new SimpleCheckBoxItem("menu.view.stat_icons.enable_icons"); + private final SimpleCheckBoxItem includeSynthetic = new SimpleCheckBoxItem("menu.view.stat_icons.include_synthetic"); + private final SimpleCheckBoxItem countFallback = new SimpleCheckBoxItem("menu.view.stat_icons.count_fallback"); + private final TypeMenu typeMenu = new TypeMenu(); public StatsMenu(Gui gui) { super(gui); @@ -30,32 +29,21 @@ public StatsMenu(Gui gui) { this.add(this.enableIcons); this.add(this.includeSynthetic); this.add(this.countFallback); - this.add(this.statTypes); + this.add(this.typeMenu); this.enableIcons.addActionListener(e -> this.onEnableIconsClicked()); this.includeSynthetic.addActionListener(e -> this.onIncludeSyntheticClicked()); this.countFallback.addActionListener(e -> this.onCountFallbackClicked()); - for (StatType statType : StatType.values()) { - JCheckBoxMenuItem checkbox = new JCheckBoxMenuItem(statType.getName()); - checkbox.addActionListener(event -> this.onCheckboxClicked(statType)); - - this.statTypeItems.put(statType, checkbox); - this.statTypes.add(checkbox); - } } @Override public void retranslate() { this.setText(I18n.translate(TRANSLATION_KEY)); - this.enableIcons.setText(I18n.translate("menu.view.stat_icons.enable_icons")); - this.includeSynthetic.setText(I18n.translate("menu.view.stat_icons.include_synthetic")); - this.countFallback.setText(I18n.translate("menu.view.stat_icons.count_fallback")); - this.statTypes.setText(I18n.translate("menu.view.stat_icons.included_types")); - - for (StatType statType : StatType.values()) { - this.statTypeItems.get(statType).setText(statType.getName()); - } + this.enableIcons.retranslate(); + this.includeSynthetic.retranslate(); + this.countFallback.retranslate(); + this.typeMenu.retranslate(); } @Override @@ -64,10 +52,7 @@ public void updateState(boolean jarOpen, ConnectionState state) { this.includeSynthetic.setSelected(Config.main().stats.shouldIncludeSyntheticParameters.value()); this.countFallback.setSelected(Config.main().stats.shouldCountFallbackNames.value()); - for (StatType type : StatType.values()) { - JCheckBoxMenuItem checkbox = this.statTypeItems.get(type); - checkbox.setSelected(Config.main().stats.includedStatTypes.value().contains(type)); - } + this.typeMenu.updateState(jarOpen, state); } private void onEnableIconsClicked() { @@ -85,18 +70,6 @@ private void onCountFallbackClicked() { this.updateIconsLater(); } - private void onCheckboxClicked(StatType type) { - JCheckBoxMenuItem checkbox = this.statTypeItems.get(type); - - if (checkbox.isSelected() && !Config.stats().includedStatTypes.value().contains(type)) { - Config.stats().includedStatTypes.value().add(type); - } else { - Config.stats().includedStatTypes.value().remove(type); - } - - this.updateIconsLater(); - } - private void updateIconsLater() { SwingUtilities.invokeLater(() -> runAsync(() -> this.gui.getController().regenerateAndUpdateStatIcons())); } @@ -105,4 +78,54 @@ private void updateIconsLater() { public String getAliasesTranslationKeyPrefix() { return TRANSLATION_KEY; } + + private final class TypeMenu extends AbstractSearchableEnigmaMenu { + static final String TRANSLATION_KEY = "menu.view.stat_icons.included_types"; + + private final Map items = new HashMap<>(); + + TypeMenu() { + super(StatsMenu.this.gui); + + for (StatType statType : StatType.values()) { + SimpleCheckBoxItem checkbox = new SimpleCheckBoxItem(statType.getTranslationKey()); + checkbox.addActionListener(event -> this.onTypeClicked(statType)); + + this.items.put(statType, checkbox); + this.add(checkbox); + } + } + + @Override + public String getAliasesTranslationKeyPrefix() { + return TRANSLATION_KEY; + } + + @Override + public void retranslate() { + this.setText(I18n.translate(TRANSLATION_KEY)); + + this.items.values().forEach(SimpleCheckBoxItem::retranslate); + } + + @Override + public void updateState(boolean jarOpen, ConnectionState state) { + for (StatType type : StatType.values()) { + JCheckBoxMenuItem checkbox = this.items.get(type); + checkbox.setSelected(Config.main().stats.includedStatTypes.value().contains(type)); + } + } + + void onTypeClicked(StatType type) { + JCheckBoxMenuItem checkbox = this.items.get(type); + + if (checkbox.isSelected() && !Config.stats().includedStatTypes.value().contains(type)) { + Config.stats().includedStatTypes.value().add(type); + } else { + Config.stats().includedStatTypes.value().remove(type); + } + + StatsMenu.this.updateIconsLater(); + } + } } From 8df388cf09cc8b2c74e53e464c9fae0665cd7285 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 19 Nov 2025 20:18:38 -0800 Subject: [PATCH 075/124] make ThemesMenu themes searchable --- CONTRIBUTING.md | 16 +++++-- .../gui/element/menu_bar/view/ThemesMenu.java | 48 +++++++++++++++---- enigma/src/main/resources/lang/en_us.json | 5 ++ 3 files changed, 55 insertions(+), 14 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 79b5f2932..f20bda0b7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -100,8 +100,14 @@ its translation file. | `View`>`Stat Icons`>`Included types`>`Fields` | `"type.fields"` | | `View`>`Stat Icons`>`Included types`>`Parameters` | `"type.parameters"` | | `View`>`Stat Icons`>`Included types`>`Classes` | `"type.classes"` | -| `Entry Tooltips` menu | `"menu.view.entry_tooltips.aliases"` | -| `Entry Tooltips`>`Enable tooltips` | `"menu.view.entry_tooltips.enable.aliases"` | -| `Entry Tooltips`>`Allow tooltip interaction` | `"menu.view.entry_tooltips.interactable.aliases"` | -| `Scale` menu | `"menu.view.scale.aliases"` | -| `Themes` menu | `"menu.view.themes.aliases"` | +| `View`>`Entry Tooltips` menu | `"menu.view.entry_tooltips.aliases"` | +| `View`>`Entry Tooltips`>`Enable tooltips` | `"menu.view.entry_tooltips.enable.aliases"` | +| `View`>`Entry Tooltips`>`Allow tooltip interaction` | `"menu.view.entry_tooltips.interactable.aliases"` | +| `View`>`Themes` menu | `"menu.view.themes.aliases"` | +| `View`>`Themes`>`Default` | `"menu.view.themes.default.aliases"` | +| `View`>`Themes`>`Darcula` | `"menu.view.themes.darcula.aliases"` | +| `View`>`Themes`>`Darcerula` | `"menu.view.themes.darcerula.aliases"` | +| `View`>`Themes`>`Metal` | `"menu.view.themes.metal.aliases"` | +| `View`>`Themes`>`System` | `"menu.view.themes.system.aliases"` | +| `View`>`Themes`>`None (JVM Default)` | `"menu.view.themes.none.aliases"` | +| `View`>`Scale` menu | `"menu.view.scale.aliases"` | diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/ThemesMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/ThemesMenu.java index fd17c2b04..0c921ec03 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/ThemesMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/ThemesMenu.java @@ -5,6 +5,8 @@ import org.quiltmc.enigma.gui.config.Config; import org.quiltmc.enigma.gui.dialog.ChangeDialog; import org.quiltmc.enigma.gui.element.menu_bar.AbstractSearchableEnigmaMenu; +import org.quiltmc.enigma.gui.element.menu_bar.Retranslatable; +import org.quiltmc.enigma.gui.element.menu_bar.SearchableElement; import org.quiltmc.enigma.util.I18n; import javax.swing.ButtonGroup; @@ -18,19 +20,19 @@ public class ThemesMenu extends AbstractSearchableEnigmaMenu { private static final String TRANSLATION_KEY = "menu.view.themes"; - private final Map themes = new HashMap<>(); + private final Map themes = new HashMap<>(); protected ThemesMenu(Gui gui) { super(gui); ButtonGroup themeGroup = new ButtonGroup(); for (ThemeChoice theme : ThemeChoice.values()) { - JRadioButtonMenuItem themeButton = new JRadioButtonMenuItem(); - themeGroup.add(themeButton); - this.themes.put(theme, themeButton); + ThemeItem themeItem = new ThemeItem(theme); + themeGroup.add(themeItem); + this.themes.put(theme, themeItem); - this.add(themeButton); - themeButton.addActionListener(e -> this.onThemeClicked(theme)); + this.add(themeItem); + themeItem.addActionListener(e -> this.onThemeClicked(theme)); } } @@ -38,9 +40,7 @@ protected ThemesMenu(Gui gui) { public void retranslate() { this.setText(I18n.translate(TRANSLATION_KEY)); - for (ThemeChoice theme : ThemeChoice.values()) { - this.themes.get(theme).setText(I18n.translate("menu.view.themes." + theme.name().toLowerCase(Locale.ROOT))); - } + this.themes.values().forEach(ThemeItem::retranslate); } @Override @@ -61,4 +61,34 @@ private void onThemeClicked(ThemeChoice theme) { public String getAliasesTranslationKeyPrefix() { return TRANSLATION_KEY; } + + private static final class ThemeItem extends JRadioButtonMenuItem implements SearchableElement, Retranslatable { + final ThemeChoice theme; + final String translationKey; + + private ThemeItem(ThemeChoice theme) { + this.theme = theme; + this.translationKey = ThemesMenu.TRANSLATION_KEY + "." + theme.name().toLowerCase(Locale.ROOT); + } + + @Override + public void retranslate() { + this.setText(I18n.translate(this.translationKey)); + } + + @Override + public String getSearchName() { + return this.getText(); + } + + @Override + public String getAliasesTranslationKeyPrefix() { + return this.translationKey; + } + + @Override + public void onSearchClicked() { + this.doClick(); + } + } } diff --git a/enigma/src/main/resources/lang/en_us.json b/enigma/src/main/resources/lang/en_us.json index b31f457b7..33876b666 100644 --- a/enigma/src/main/resources/lang/en_us.json +++ b/enigma/src/main/resources/lang/en_us.json @@ -73,11 +73,16 @@ "menu.view.themes": "Themes", "menu.view.themes.aliases": "Skins", "menu.view.themes.default": "Default", + "menu.view.themes.default.aliases": "Light", "menu.view.themes.darcula": "Darcula", + "menu.view.themes.darcula.aliases": "Dark;Dracula", "menu.view.themes.darcerula": "Darcerula", + "menu.view.themes.darcerula.aliases": "Darker;Dracula", "menu.view.themes.metal": "Metal", "menu.view.themes.system": "System", + "menu.view.themes.system.aliases": "Os;Operating System", "menu.view.themes.none": "None (JVM Default)", + "menu.view.themes.none.aliases": "Java", "menu.view.languages": "Languages", "menu.view.scale": "Scale", "menu.view.scale.custom": "Custom...", From 0af1142e9d83be5e981b21e8b07f3be4cddef3aa Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 19 Nov 2025 20:22:33 -0800 Subject: [PATCH 076/124] make ViewMenu.fontItem searchable --- CONTRIBUTING.md | 1 + .../quiltmc/enigma/gui/element/menu_bar/view/ViewMenu.java | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f20bda0b7..9419b6422 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -111,3 +111,4 @@ its translation file. | `View`>`Themes`>`System` | `"menu.view.themes.system.aliases"` | | `View`>`Themes`>`None (JVM Default)` | `"menu.view.themes.none.aliases"` | | `View`>`Scale` menu | `"menu.view.scale.aliases"` | +| `View`>`Fonts...` | `"menu.view.font"` | diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/ViewMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/ViewMenu.java index c85abddeb..78c740e46 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/ViewMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/ViewMenu.java @@ -4,6 +4,7 @@ import org.quiltmc.enigma.gui.Gui; import org.quiltmc.enigma.gui.dialog.FontDialog; import org.quiltmc.enigma.gui.element.menu_bar.AbstractSearchableEnigmaMenu; +import org.quiltmc.enigma.gui.element.menu_bar.SimpleItem; import org.quiltmc.enigma.util.I18n; import javax.swing.JMenuItem; @@ -18,7 +19,7 @@ public class ViewMenu extends AbstractSearchableEnigmaMenu { private final ScaleMenu scale; private final EntryTooltipsMenu entryTooltips; - private final JMenuItem fontItem = new JMenuItem(); + private final SimpleItem fontItem = new SimpleItem("menu.view.font"); public ViewMenu(Gui gui) { super(gui); @@ -50,7 +51,7 @@ public void retranslate() { this.scale.retranslate(); this.stats.retranslate(); this.entryTooltips.retranslate(); - this.fontItem.setText(I18n.translate("menu.view.font")); + this.fontItem.retranslate(); } @Override From 3b18bac0321e1ca8d5b48795f69d1dd8e399892c Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 19 Nov 2025 21:09:17 -0800 Subject: [PATCH 077/124] split SearchableElement and ConventionalSearchableElement make CommabMenu items searchable --- CONTRIBUTING.md | 4 ++ .../enigma/gui/docker/CollabDocker.java | 4 +- .../AbstractSearchableEnigmaMenu.java | 2 +- .../gui/element/menu_bar/CollabMenu.java | 69 +++++++++++++++---- .../ConventionalSearchableElement.java | 27 ++++++++ .../element/menu_bar/SearchableElement.java | 25 ++----- .../element/menu_bar/SimpleCheckBoxItem.java | 2 +- .../gui/element/menu_bar/SimpleItem.java | 4 +- .../gui/element/menu_bar/SimpleRadioItem.java | 2 +- .../element/menu_bar/view/LanguagesMenu.java | 4 +- .../gui/element/menu_bar/view/ThemesMenu.java | 4 +- 11 files changed, 104 insertions(+), 43 deletions(-) create mode 100644 enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/ConventionalSearchableElement.java diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9419b6422..d6bc58d00 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -53,6 +53,10 @@ its translation file. |----------------------------------------------------------|----------------------------------------------------| | `Dev` menu | `"dev.menu.aliases"` | | `Collab` menu | `"menu.collab.aliases"` | +| `Collab`>`Connect to Server` | `"menu.collab.connect.aliases"` | +| `Collab`>`Disconnect` | `"menu.collab.disconnect"` | +| `Collab`>`Start Server` | `"menu.collab.server.start.aliases"` | +| `Collab`>`Stop Server` | `"menu.collab.server.stop"` | | `Decompiler` menu | `"menu.decompiler.aliases"` | | `Help` menu | `"menu.help.aliases"` | | `Search` menu | `"menu.search.aliases"` | diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/docker/CollabDocker.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/docker/CollabDocker.java index eda5a880d..7f51b8844 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/docker/CollabDocker.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/docker/CollabDocker.java @@ -54,8 +54,8 @@ public CollabDocker(Gui gui) { connectionButtonPanel.add(this.startServerButton, BorderLayout.NORTH); connectionButtonPanel.add(this.connectToServerButton, BorderLayout.SOUTH); - this.startServerButton.addActionListener(e -> this.gui.getMenuBar().getCollabMenu().onStartServerClicked()); - this.connectToServerButton.addActionListener(e -> this.gui.getMenuBar().getCollabMenu().onConnectClicked()); + this.startServerButton.addActionListener(e -> this.gui.getMenuBar().getCollabMenu().onHostClicked()); + this.connectToServerButton.addActionListener(e -> this.gui.getMenuBar().getCollabMenu().onConnectionClicked()); // we make a copy of the title bar to avoid having to shuffle it around both panels this.titleCopy = new DockerTitleBar(gui, this, this.titleSupplier); diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/AbstractSearchableEnigmaMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/AbstractSearchableEnigmaMenu.java index 83e7a27d0..a40ba9606 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/AbstractSearchableEnigmaMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/AbstractSearchableEnigmaMenu.java @@ -2,7 +2,7 @@ import org.quiltmc.enigma.gui.Gui; -public abstract class AbstractSearchableEnigmaMenu extends AbstractEnigmaMenu implements SearchableElement { +public abstract class AbstractSearchableEnigmaMenu extends AbstractEnigmaMenu implements ConventionalSearchableElement { protected AbstractSearchableEnigmaMenu(Gui gui) { super(gui); } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/CollabMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/CollabMenu.java index 4b7a30783..327661285 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/CollabMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/CollabMenu.java @@ -1,5 +1,6 @@ package org.quiltmc.enigma.gui.element.menu_bar; +import org.jspecify.annotations.Nullable; import org.quiltmc.enigma.gui.ConnectionState; import org.quiltmc.enigma.gui.Gui; import org.quiltmc.enigma.gui.NotificationManager; @@ -14,21 +15,30 @@ import javax.swing.JOptionPane; import java.io.IOException; import java.util.Arrays; +import java.util.function.Function; +import java.util.stream.Stream; public class CollabMenu extends AbstractSearchableEnigmaMenu { private static final String TRANSLATION_KEY = "menu.collab"; - private final JMenuItem connectItem = new JMenuItem(); - private final JMenuItem startServerItem = new JMenuItem(); + private final StatefulItem connectionItem = new StatefulItem(state -> state != ConnectionState.CONNECTED + ? "menu.collab.connect" + : "menu.collab.disconnect" + ); + + private final StatefulItem hostItem = new StatefulItem(state -> state != ConnectionState.HOSTING + ? "menu.collab.server.start" + : "menu.collab.server.stop" + ); public CollabMenu(Gui gui) { super(gui); - this.add(this.connectItem); - this.add(this.startServerItem); + this.add(this.connectionItem); + this.add(this.hostItem); - this.connectItem.addActionListener(e -> this.onConnectClicked()); - this.startServerItem.addActionListener(e -> this.onStartServerClicked()); + this.connectionItem.addActionListener(e -> this.onConnectionClicked()); + this.hostItem.addActionListener(e -> this.onHostClicked()); } @Override @@ -38,18 +48,18 @@ public void retranslate() { } private void retranslate(ConnectionState state) { - this.connectItem.setText(I18n.translate(state != ConnectionState.CONNECTED ? "menu.collab.connect" : "menu.collab.disconnect")); - this.startServerItem.setText(I18n.translate(state != ConnectionState.HOSTING ? "menu.collab.server.start" : "menu.collab.server.stop")); + this.connectionItem.retranslate(state); + this.hostItem.retranslate(state); } @Override public void updateState(boolean jarOpen, ConnectionState state) { - this.connectItem.setEnabled(jarOpen && state != ConnectionState.HOSTING); - this.startServerItem.setEnabled(jarOpen && state != ConnectionState.CONNECTED); + this.connectionItem.setEnabled(jarOpen && state != ConnectionState.HOSTING); + this.hostItem.setEnabled(jarOpen && state != ConnectionState.CONNECTED); this.retranslate(state); } - public void onConnectClicked() { + public void onConnectionClicked() { if (this.gui.getController().getClient() != null) { this.gui.getController().disconnectIfConnected(null); return; @@ -78,7 +88,7 @@ public void onConnectClicked() { Arrays.fill(result.password(), (char) 0); } - public void onStartServerClicked() { + public void onHostClicked() { if (this.gui.getController().getServer() != null) { this.gui.getController().disconnectIfConnected(null); return; @@ -109,4 +119,39 @@ public void onStartServerClicked() { public String getAliasesTranslationKeyPrefix() { return TRANSLATION_KEY; } + + private static final class StatefulItem extends JMenuItem implements SearchableElement { + final Function updateTranslationKey; + + @Nullable + String translationKey; + + StatefulItem(Function updateTranslationKey) { + this.updateTranslationKey = updateTranslationKey; + } + + @Override + public Stream streamSearchAliases() { + return this.translationKey == null ? Stream.empty() : Stream.concat( + Stream.of(this.getSearchName()), + SearchableElement.translateExtraAliases(this.translationKey) + ); + } + + void retranslate(ConnectionState state) { + this.translationKey = this.updateTranslationKey.apply(state); + + this.setText(I18n.translate(this.translationKey)); + } + + @Override + public String getSearchName() { + return this.getText(); + } + + @Override + public void onSearchClicked() { + this.doClick(); + } + } } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/ConventionalSearchableElement.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/ConventionalSearchableElement.java new file mode 100644 index 000000000..13a54e177 --- /dev/null +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/ConventionalSearchableElement.java @@ -0,0 +1,27 @@ +package org.quiltmc.enigma.gui.element.menu_bar; + +import java.util.stream.Stream; + +public interface ConventionalSearchableElement extends SearchableElement { + String ALIASES_SUFFIX = ".aliases"; + + @Override + default Stream streamSearchAliases() { + return Stream.concat( + Stream.of(this.getSearchName()), + SearchableElement.translateExtraAliases(this.getAliasesTranslationKeyPrefix() + ALIASES_SUFFIX) + ); + } + + /** + * Returns a translation key prefix used to retrieve translatable search aliases.
    + * Usually the prefix is the translation key of the translatable element. + * + *

    {@value ALIASES_SUFFIX} is appended to create the complete translation key.
    + * Alias translations hold multiple aliases separated by {@value ALIAS_DELIMITER}. + * + *

    All alias translation key prefixes should be documented in {@code CONTRIBUTING.md} under
    + * {@code Translating > Search Aliases > Complete list of search alias translation keys} + */ + String getAliasesTranslationKeyPrefix(); +} diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchableElement.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchableElement.java index f5933ab64..93ce73968 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchableElement.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchableElement.java @@ -7,32 +7,17 @@ import java.util.stream.Stream; public interface SearchableElement extends MenuElement { - String ALIASES_SUFFIX = ".aliases"; String ALIAS_DELIMITER = ";"; - default Stream streamSearchAliases() { - final String aliases = I18n - .translateOrNull(this.getAliasesTranslationKeyPrefix() + ALIASES_SUFFIX); + static Stream translateExtraAliases(String translationKey) { + final String aliases = I18n.translateOrNull(translationKey); - return Stream.concat( - Stream.of(this.getSearchName()), - aliases == null ? Stream.empty() : Arrays.stream(aliases.split(ALIAS_DELIMITER)) - ); + return aliases == null ? Stream.empty() : Arrays.stream(aliases.split(ALIAS_DELIMITER)); } - String getSearchName(); + Stream streamSearchAliases(); - /** - * Returns a translation key prefix used to retrieve translatable search aliases.
    - * Usually the prefix is the translation key of the translatable element. - * - *

    {@value ALIASES_SUFFIX} is appended to create the complete translation key.
    - * Alias translations hold multiple aliases separated by {@value ALIAS_DELIMITER}. - * - *

    All alias translation key prefixes should be documented in {@code CONTRIBUTING.md} under
    - * {@code Translating > Search Aliases > Complete list of search alias translation keys} - */ - String getAliasesTranslationKeyPrefix(); + String getSearchName(); void onSearchClicked(); } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SimpleCheckBoxItem.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SimpleCheckBoxItem.java index 62e9e641e..ea2b81293 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SimpleCheckBoxItem.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SimpleCheckBoxItem.java @@ -4,7 +4,7 @@ import javax.swing.JCheckBoxMenuItem; -public class SimpleCheckBoxItem extends JCheckBoxMenuItem implements SearchableElement, Retranslatable { +public class SimpleCheckBoxItem extends JCheckBoxMenuItem implements ConventionalSearchableElement, Retranslatable { private final String translationKey; public SimpleCheckBoxItem(String translationKey) { diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SimpleItem.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SimpleItem.java index bc82b1cf4..018838841 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SimpleItem.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SimpleItem.java @@ -4,7 +4,7 @@ import javax.swing.JMenuItem; -public class SimpleItem extends JMenuItem implements SearchableElement, Retranslatable { +public class SimpleItem extends JMenuItem implements ConventionalSearchableElement, Retranslatable { private final String translationKey; public SimpleItem(String translationKey) { @@ -13,7 +13,7 @@ public SimpleItem(String translationKey) { @Override public void retranslate() { - this.setText(I18n.translate(this.getAliasesTranslationKeyPrefix())); + this.setText(I18n.translate(this.translationKey)); } @Override diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SimpleRadioItem.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SimpleRadioItem.java index 871cf0a0e..b527843f9 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SimpleRadioItem.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SimpleRadioItem.java @@ -4,7 +4,7 @@ import javax.swing.JRadioButtonMenuItem; -public class SimpleRadioItem extends JRadioButtonMenuItem implements SearchableElement, Retranslatable { +public class SimpleRadioItem extends JRadioButtonMenuItem implements ConventionalSearchableElement, Retranslatable { private final String translationKey; public SimpleRadioItem(String translationKey) { diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/LanguagesMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/LanguagesMenu.java index f9b98b15e..192b01814 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/LanguagesMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/LanguagesMenu.java @@ -4,8 +4,8 @@ import org.quiltmc.enigma.gui.Gui; import org.quiltmc.enigma.gui.config.Config; import org.quiltmc.enigma.gui.element.menu_bar.AbstractSearchableEnigmaMenu; +import org.quiltmc.enigma.gui.element.menu_bar.ConventionalSearchableElement; import org.quiltmc.enigma.gui.element.menu_bar.Retranslatable; -import org.quiltmc.enigma.gui.element.menu_bar.SearchableElement; import org.quiltmc.enigma.gui.util.LanguageUtil; import org.quiltmc.enigma.util.I18n; @@ -60,7 +60,7 @@ public String getAliasesTranslationKeyPrefix() { return TRANSLATION_KEY; } - private static final class LanguageItem extends JRadioButtonMenuItem implements SearchableElement, Retranslatable { + private static final class LanguageItem extends JRadioButtonMenuItem implements ConventionalSearchableElement, Retranslatable { private final String language; LanguageItem(String language) { diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/ThemesMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/ThemesMenu.java index 0c921ec03..9a2d23f64 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/ThemesMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/ThemesMenu.java @@ -5,8 +5,8 @@ import org.quiltmc.enigma.gui.config.Config; import org.quiltmc.enigma.gui.dialog.ChangeDialog; import org.quiltmc.enigma.gui.element.menu_bar.AbstractSearchableEnigmaMenu; +import org.quiltmc.enigma.gui.element.menu_bar.ConventionalSearchableElement; import org.quiltmc.enigma.gui.element.menu_bar.Retranslatable; -import org.quiltmc.enigma.gui.element.menu_bar.SearchableElement; import org.quiltmc.enigma.util.I18n; import javax.swing.ButtonGroup; @@ -62,7 +62,7 @@ public String getAliasesTranslationKeyPrefix() { return TRANSLATION_KEY; } - private static final class ThemeItem extends JRadioButtonMenuItem implements SearchableElement, Retranslatable { + private static final class ThemeItem extends JRadioButtonMenuItem implements ConventionalSearchableElement, Retranslatable { final ThemeChoice theme; final String translationKey; From 86f7cde8c556f5ed6910428542ae68306270edee Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 19 Nov 2025 21:21:40 -0800 Subject: [PATCH 078/124] make DecompilerMenu sub-items searchable --- CONTRIBUTING.md | 1 + .../gui/element/menu_bar/DecompilerMenu.java | 29 ++++++++++++++++--- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d6bc58d00..6d7136b22 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -58,6 +58,7 @@ its translation file. | `Collab`>`Start Server` | `"menu.collab.server.start.aliases"` | | `Collab`>`Stop Server` | `"menu.collab.server.stop"` | | `Decompiler` menu | `"menu.decompiler.aliases"` | +| `Decompiler`>`Decompiler Settings` | `"menu.decompiler.settings"` | | `Help` menu | `"menu.help.aliases"` | | `Search` menu | `"menu.search.aliases"` | | `Crash History` menu | `"menu.file.crash_history.aliases"` | diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/DecompilerMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/DecompilerMenu.java index f34e7b98e..2bfb7292e 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/DecompilerMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/DecompilerMenu.java @@ -7,13 +7,13 @@ import org.quiltmc.enigma.util.I18n; import javax.swing.ButtonGroup; -import javax.swing.JMenuItem; import javax.swing.JRadioButtonMenuItem; +import java.util.stream.Stream; public class DecompilerMenu extends AbstractSearchableEnigmaMenu { private static final String TRANSLATION_KEY = "menu.decompiler"; - private final JMenuItem decompilerSettingsItem = new JMenuItem(); + private final SimpleItem decompilerSettingsItem = new SimpleItem("menu.decompiler.settings"); public DecompilerMenu(Gui gui) { super(gui); @@ -21,7 +21,7 @@ public DecompilerMenu(Gui gui) { ButtonGroup decompilerGroup = new ButtonGroup(); for (Decompiler decompiler : Decompiler.values()) { - JRadioButtonMenuItem decompilerButton = new JRadioButtonMenuItem(decompiler.name); + DecompilerItem decompilerButton = new DecompilerItem(decompiler.name); decompilerGroup.add(decompilerButton); if (decompiler.equals(Config.decompiler().activeDecompiler.value())) { decompilerButton.setSelected(true); @@ -44,11 +44,32 @@ public DecompilerMenu(Gui gui) { @Override public void retranslate() { this.setText(I18n.translate(TRANSLATION_KEY)); - this.decompilerSettingsItem.setText(I18n.translate("menu.decompiler.settings")); + this.decompilerSettingsItem.retranslate(); } @Override public String getAliasesTranslationKeyPrefix() { return TRANSLATION_KEY; } + + private static final class DecompilerItem extends JRadioButtonMenuItem implements SearchableElement { + DecompilerItem(String name) { + super(name); + } + + @Override + public Stream streamSearchAliases() { + return Stream.of(this.getText()); + } + + @Override + public String getSearchName() { + return this.getText(); + } + + @Override + public void onSearchClicked() { + this.doClick(); + } + } } From 2f0413cc630bb6ba7d829fe3c9f59a8ca1af5e90 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 19 Nov 2025 21:31:15 -0800 Subject: [PATCH 079/124] make DevMenu sub-items searchable --- CONTRIBUTING.md | 4 +++ .../enigma/gui/element/menu_bar/DevMenu.java | 30 ++++++++++--------- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6d7136b22..566003fe6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -52,6 +52,10 @@ its translation file. | Element | Translation Key | |----------------------------------------------------------|----------------------------------------------------| | `Dev` menu | `"dev.menu.aliases"` | +| `Dev`>`Show mapping source plugin` | "dev.menu.show_mapping_source_plugin"` | +| `Dev`>`Debug token highlights` | "dev.menu.debug_token_highlights"` | +| `Dev`>`Log client packets` | "dev.menu.log_client_packets"` | +| `Dev`>`Print mapping tree` | "dev.menu.print_mapping_tree"` | | `Collab` menu | `"menu.collab.aliases"` | | `Collab`>`Connect to Server` | `"menu.collab.connect.aliases"` | | `Collab`>`Disconnect` | `"menu.collab.disconnect"` | diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/DevMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/DevMenu.java index 9dff80ff4..75c86d326 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/DevMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/DevMenu.java @@ -10,10 +10,8 @@ import org.tinylog.Logger; import javax.swing.JButton; -import javax.swing.JCheckBoxMenuItem; import javax.swing.JFileChooser; import javax.swing.JFrame; -import javax.swing.JMenuItem; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTextArea; @@ -26,13 +24,17 @@ import java.io.StringWriter; import java.nio.file.Files; +import static org.quiltmc.enigma.gui.util.GuiUtil.syncStateWithConfig; + public class DevMenu extends AbstractSearchableEnigmaMenu { private static final String TRANSLATION_KEY = "dev.menu"; - private final JCheckBoxMenuItem showMappingSourcePluginItem = new JCheckBoxMenuItem(); - private final JCheckBoxMenuItem debugTokenHighlightsItem = new JCheckBoxMenuItem(); - private final JCheckBoxMenuItem logClientPacketsItem = new JCheckBoxMenuItem(); - private final JMenuItem printMappingTreeItem = new JMenuItem(); + private final SimpleCheckBoxItem showMappingSourcePluginItem = + new SimpleCheckBoxItem("dev.menu.show_mapping_source_plugin"); + private final SimpleCheckBoxItem debugTokenHighlightsItem = + new SimpleCheckBoxItem("dev.menu.debug_token_highlights"); + private final SimpleCheckBoxItem logClientPacketsItem = new SimpleCheckBoxItem("dev.menu.log_client_packets"); + private final SimpleItem printMappingTreeItem = new SimpleItem("dev.menu.print_mapping_tree"); public DevMenu(Gui gui) { super(gui); @@ -42,6 +44,10 @@ public DevMenu(Gui gui) { this.add(this.logClientPacketsItem); this.add(this.printMappingTreeItem); + syncStateWithConfig(this.showMappingSourcePluginItem, Config.main().development.showMappingSourcePlugin); + syncStateWithConfig(this.debugTokenHighlightsItem, Config.main().development.debugTokenHighlights); + syncStateWithConfig(this.logClientPacketsItem, Config.main().development.logClientPackets); + this.showMappingSourcePluginItem.addActionListener(e -> this.onShowMappingSourcePluginClicked()); this.debugTokenHighlightsItem.addActionListener(e -> this.onDebugTokenHighlightsClicked()); this.logClientPacketsItem.addActionListener(e -> this.onLogClientPacketsClicked()); @@ -52,19 +58,15 @@ public DevMenu(Gui gui) { public void retranslate() { this.setText(I18n.translate(TRANSLATION_KEY)); - this.showMappingSourcePluginItem.setText(I18n.translate("dev.menu.show_mapping_source_plugin")); - this.debugTokenHighlightsItem.setText(I18n.translate("dev.menu.debug_token_highlights")); - this.logClientPacketsItem.setText(I18n.translate("dev.menu.log_client_packets")); - this.printMappingTreeItem.setText(I18n.translate("dev.menu.print_mapping_tree")); + this.showMappingSourcePluginItem.retranslate(); + this.debugTokenHighlightsItem.retranslate(); + this.logClientPacketsItem.retranslate(); + this.printMappingTreeItem.retranslate(); } @Override public void updateState(boolean jarOpen, ConnectionState state) { this.printMappingTreeItem.setEnabled(jarOpen); - - this.showMappingSourcePluginItem.setState(Config.main().development.showMappingSourcePlugin.value()); - this.debugTokenHighlightsItem.setState(Config.main().development.debugTokenHighlights.value()); - this.logClientPacketsItem.setState(Config.main().development.logClientPackets.value()); } private void showSavableTextAreaDialog(String title, String text, @Nullable String fileName) { From 11917e26af45d55566bb100df335e99f38b2388e Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 19 Nov 2025 21:33:03 -0800 Subject: [PATCH 080/124] make HelpMenu not searchable --- CONTRIBUTING.md | 1 - .../org/quiltmc/enigma/gui/element/menu_bar/HelpMenu.java | 7 +------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 566003fe6..e300884d4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -63,7 +63,6 @@ its translation file. | `Collab`>`Stop Server` | `"menu.collab.server.stop"` | | `Decompiler` menu | `"menu.decompiler.aliases"` | | `Decompiler`>`Decompiler Settings` | `"menu.decompiler.settings"` | -| `Help` menu | `"menu.help.aliases"` | | `Search` menu | `"menu.search.aliases"` | | `Crash History` menu | `"menu.file.crash_history.aliases"` | | `File` menu | `"menu.file.aliases"` | diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/HelpMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/HelpMenu.java index c53e40438..0370fdf4f 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/HelpMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/HelpMenu.java @@ -7,7 +7,7 @@ import javax.swing.JMenuItem; -public class HelpMenu extends AbstractSearchableEnigmaMenu { +public class HelpMenu extends AbstractEnigmaMenu { private static final String TRANSLATION_KEY = "menu.help"; private final JMenuItem aboutItem = new JMenuItem(); @@ -42,9 +42,4 @@ public void retranslate() { private void onGithubClicked() { GuiUtil.openUrl("https://github.com/QuiltMC/Enigma"); } - - @Override - public String getAliasesTranslationKeyPrefix() { - return TRANSLATION_KEY; - } } From 6d30f65a2dc541a1b08caec3c34dbfc582ba9e40 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 19 Nov 2025 21:41:45 -0800 Subject: [PATCH 081/124] make SearchMenu items searchable --- CONTRIBUTING.md | 28 +++++++++++-------- .../gui/element/menu_bar/SearchMenu.java | 27 +++++++++++------- 2 files changed, 33 insertions(+), 22 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e300884d4..d2cf89b62 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -52,18 +52,22 @@ its translation file. | Element | Translation Key | |----------------------------------------------------------|----------------------------------------------------| | `Dev` menu | `"dev.menu.aliases"` | -| `Dev`>`Show mapping source plugin` | "dev.menu.show_mapping_source_plugin"` | -| `Dev`>`Debug token highlights` | "dev.menu.debug_token_highlights"` | -| `Dev`>`Log client packets` | "dev.menu.log_client_packets"` | -| `Dev`>`Print mapping tree` | "dev.menu.print_mapping_tree"` | +| `Dev`>`Show mapping source plugin` | `"dev.menu.show_mapping_source_plugin.aliases"` | +| `Dev`>`Debug token highlights` | `"dev.menu.debug_token_highlights.aliases"` | +| `Dev`>`Log client packets` | `"dev.menu.log_client_packets.aliases"` | +| `Dev`>`Print mapping tree` | `"dev.menu.print_mapping_tree.aliases"` | | `Collab` menu | `"menu.collab.aliases"` | | `Collab`>`Connect to Server` | `"menu.collab.connect.aliases"` | -| `Collab`>`Disconnect` | `"menu.collab.disconnect"` | +| `Collab`>`Disconnect` | `"menu.collab.disconnect.aliases"` | | `Collab`>`Start Server` | `"menu.collab.server.start.aliases"` | -| `Collab`>`Stop Server` | `"menu.collab.server.stop"` | +| `Collab`>`Stop Server` | `"menu.collab.server.stop.aliases"` | | `Decompiler` menu | `"menu.decompiler.aliases"` | -| `Decompiler`>`Decompiler Settings` | `"menu.decompiler.settings"` | +| `Decompiler`>`Decompiler Settings` | `"menu.decompiler.settings.aliases"` | | `Search` menu | `"menu.search.aliases"` | +| `Search`>`Search All` | `"menu.search.all.aliases"` | +| `Search`>`Search Classes` | `"menu.search.class.aliases"` | +| `Search`>`Search Methods` | `"menu.search.method.aliases"` | +| `Search`>`Search Fields` | `"menu.search.field.aliases"` | | `Crash History` menu | `"menu.file.crash_history.aliases"` | | `File` menu | `"menu.file.aliases"` | | `File`>`Open Jar...` | `"menu.file.jar.open.aliases"` | @@ -104,10 +108,10 @@ its translation file. | `View`>`Stat Icons`>`Count fallback-proposed names` | `"menu.view.stat_icons.count_fallback.aliases"` | | `View`>`Stat Icons`>`Enable icons` | `"menu.view.stat_icons.enable_icons.aliases"` | | `View`>`Stat Icons`>`Included types` menu | `"menu.view.stat_icons.included_types.aliases"` | -| `View`>`Stat Icons`>`Included types`>`Methods` | `"type.methods"` | -| `View`>`Stat Icons`>`Included types`>`Fields` | `"type.fields"` | -| `View`>`Stat Icons`>`Included types`>`Parameters` | `"type.parameters"` | -| `View`>`Stat Icons`>`Included types`>`Classes` | `"type.classes"` | +| `View`>`Stat Icons`>`Included types`>`Methods` | `"type.methods.aliases"` | +| `View`>`Stat Icons`>`Included types`>`Fields` | `"type.fields.aliases"` | +| `View`>`Stat Icons`>`Included types`>`Parameters` | `"type.parameters.aliases"` | +| `View`>`Stat Icons`>`Included types`>`Classes` | `"type.classes.aliases"` | | `View`>`Entry Tooltips` menu | `"menu.view.entry_tooltips.aliases"` | | `View`>`Entry Tooltips`>`Enable tooltips` | `"menu.view.entry_tooltips.enable.aliases"` | | `View`>`Entry Tooltips`>`Allow tooltip interaction` | `"menu.view.entry_tooltips.interactable.aliases"` | @@ -119,4 +123,4 @@ its translation file. | `View`>`Themes`>`System` | `"menu.view.themes.system.aliases"` | | `View`>`Themes`>`None (JVM Default)` | `"menu.view.themes.none.aliases"` | | `View`>`Scale` menu | `"menu.view.scale.aliases"` | -| `View`>`Fonts...` | `"menu.view.font"` | +| `View`>`Fonts...` | `"menu.view.font.aliases"` | diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenu.java index 76de2301e..6c645d046 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenu.java @@ -11,15 +11,21 @@ public class SearchMenu extends AbstractSearchableEnigmaMenu { private static final String TRANSLATION_KEY = "menu.search"; - private final JMenuItem searchItem = new JMenuItem(GuiUtil.DEOBFUSCATED_ICON); - private final JMenuItem searchAllItem = new JMenuItem(GuiUtil.DEOBFUSCATED_ICON); - private final JMenuItem searchClassItem = new JMenuItem(GuiUtil.CLASS_ICON); - private final JMenuItem searchMethodItem = new JMenuItem(GuiUtil.METHOD_ICON); - private final JMenuItem searchFieldItem = new JMenuItem(GuiUtil.FIELD_ICON); + private final SimpleItem searchItem = new SimpleItem("menu.search"); + private final SimpleItem searchAllItem = new SimpleItem("menu.search.all"); + private final SimpleItem searchClassItem = new SimpleItem("menu.search.class"); + private final SimpleItem searchMethodItem = new SimpleItem("menu.search.method"); + private final SimpleItem searchFieldItem = new SimpleItem("menu.search.field"); public SearchMenu(Gui gui) { super(gui); + this.searchItem.setIcon(GuiUtil.DEOBFUSCATED_ICON); + this.searchAllItem.setIcon(GuiUtil.DEOBFUSCATED_ICON); + this.searchClassItem.setIcon(GuiUtil.CLASS_ICON); + this.searchMethodItem.setIcon(GuiUtil.METHOD_ICON); + this.searchFieldItem.setIcon(GuiUtil.FIELD_ICON); + this.add(this.searchItem); this.add(this.searchAllItem); this.add(this.searchClassItem); @@ -36,11 +42,12 @@ public SearchMenu(Gui gui) { @Override public void retranslate() { this.setText(I18n.translate(TRANSLATION_KEY)); - this.searchItem.setText(I18n.translate("menu.search")); - this.searchAllItem.setText(I18n.translate("menu.search.all")); - this.searchClassItem.setText(I18n.translate("menu.search.class")); - this.searchMethodItem.setText(I18n.translate("menu.search.method")); - this.searchFieldItem.setText(I18n.translate("menu.search.field")); + + this.searchItem.retranslate(); + this.searchAllItem.retranslate(); + this.searchClassItem.retranslate(); + this.searchMethodItem.retranslate(); + this.searchFieldItem.retranslate(); } @Override From 3197b035ed7322af2e8b354890a6133cb80a47f3 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 19 Nov 2025 21:44:12 -0800 Subject: [PATCH 082/124] remove unused imports --- .../org/quiltmc/enigma/gui/element/menu_bar/SearchMenu.java | 2 -- .../org/quiltmc/enigma/gui/element/menu_bar/view/ViewMenu.java | 2 -- 2 files changed, 4 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenu.java index 6c645d046..f588308e2 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenu.java @@ -6,8 +6,6 @@ import org.quiltmc.enigma.gui.util.GuiUtil; import org.quiltmc.enigma.util.I18n; -import javax.swing.JMenuItem; - public class SearchMenu extends AbstractSearchableEnigmaMenu { private static final String TRANSLATION_KEY = "menu.search"; diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/ViewMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/ViewMenu.java index 78c740e46..5a74c5cd7 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/ViewMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/ViewMenu.java @@ -7,8 +7,6 @@ import org.quiltmc.enigma.gui.element.menu_bar.SimpleItem; import org.quiltmc.enigma.util.I18n; -import javax.swing.JMenuItem; - public class ViewMenu extends AbstractSearchableEnigmaMenu { private static final String TRANSLATION_KEY = "menu.view"; From 57991b02c0e9d79af6f6157616bfa657ba7cb6ce Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 19 Nov 2025 22:02:39 -0800 Subject: [PATCH 083/124] clearSearchMenusResults in MenuBar::updateUiState --- .../java/org/quiltmc/enigma/gui/element/menu_bar/MenuBar.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/MenuBar.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/MenuBar.java index bf4b5cf2f..1badafe0e 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/MenuBar.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/MenuBar.java @@ -64,6 +64,8 @@ public void updateUiState() { for (EnigmaMenu menu : this.menus) { menu.updateState(jarOpen, connectionState); } + + this.clearSearchMenusResults(); } public void retranslateUi() { From 497b758c8a1de309c6aee0ec4bfbccda31641e34 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Thu, 20 Nov 2025 19:55:11 -0800 Subject: [PATCH 084/124] implement contains search --- .../enigma/gui/element/menu_bar/HelpMenu.java | 2 +- .../gui/element/menu_bar/SearchMenusMenu.java | 429 +++++++++++------- 2 files changed, 258 insertions(+), 173 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/HelpMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/HelpMenu.java index 0370fdf4f..106434be4 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/HelpMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/HelpMenu.java @@ -28,7 +28,7 @@ public HelpMenu(Gui gui) { } public void clearSearchMenusResults() { - this.searchMenusMenu.clearResults(); + this.searchMenusMenu.clearLookup(); } @Override diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java index deb3fc3af..1c664136e 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java @@ -1,12 +1,14 @@ package org.quiltmc.enigma.gui.element.menu_bar; import com.google.common.collect.ImmutableList; +import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; import org.quiltmc.enigma.gui.ConnectionState; import org.quiltmc.enigma.gui.Gui; import org.quiltmc.enigma.gui.element.PlaceheldTextField; import org.quiltmc.enigma.util.I18n; import org.quiltmc.enigma.util.multi_trie.CompositeStringMultiTrie; +import org.quiltmc.enigma.util.multi_trie.MutableStringMultiTrie; import org.quiltmc.enigma.util.multi_trie.StringMultiTrie; import org.quiltmc.enigma.util.multi_trie.StringMultiTrie.Node; @@ -16,9 +18,14 @@ import javax.swing.event.DocumentListener; import javax.swing.event.MenuEvent; import javax.swing.event.MenuListener; +import java.awt.Component; +import java.awt.Point; import java.awt.event.HierarchyEvent; import java.awt.event.HierarchyListener; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; +import java.util.Map; import java.util.stream.Stream; import static com.google.common.collect.ImmutableList.toImmutableList; @@ -38,7 +45,8 @@ private static Stream streamElementTree(MenuElement root) { private final PlaceheldTextField field = new PlaceheldTextField(); private final JMenuItem noResults = new JMenuItem(); - private final ResultManager resultManager = new ResultManager(); + @Nullable + private Lookup lookup; protected SearchMenusMenu(Gui gui) { super(gui); @@ -46,8 +54,7 @@ protected SearchMenusMenu(Gui gui) { this.noResults.setEnabled(false); this.noResults.setVisible(false); - this.add(this.field); - this.add(this.noResults); + this.addPermanentChildren(); // Always focus field, but don't always select its text, because it loses focus when packing new search results. this.field.addHierarchyListener(e -> { @@ -65,6 +72,8 @@ public void hierarchyChanged(HierarchyEvent e) { SearchMenusMenu.this.field.removeHierarchyListener(this); SearchMenusMenu.this.field.selectAll(); + + SearchMenusMenu.this.updateResultItems(); } } }; @@ -86,249 +95,325 @@ public void menuCanceled(MenuEvent e) { }); this.field.getDocument().addDocumentListener(new DocumentListener() { - void updateResultItems() { - final String searchTerm = SearchMenusMenu.this.field.getText(); - - if (searchTerm.isEmpty()) { - SearchMenusMenu.this.noResults.setVisible(false); - SearchMenusMenu.this.resultManager.clearCurrent(); - - SearchMenusMenu.this.getPopupMenu().pack(); - } else { - switch (SearchMenusMenu.this.resultManager.updateResultItems(searchTerm)) { - case NO_RESULTS -> { - SearchMenusMenu.this.noResults.setVisible(true); - - SearchMenusMenu.this.getPopupMenu().pack(); - } - case SAME_RESULTS -> { } - case DIFFERENT_RESULTS -> { - SearchMenusMenu.this.noResults.setVisible(false); - - SearchMenusMenu.this.getPopupMenu().pack(); - } - } - } - } - @Override public void insertUpdate(DocumentEvent e) { - this.updateResultItems(); + SearchMenusMenu.this.updateResultItems(); } @Override public void removeUpdate(DocumentEvent e) { - this.updateResultItems(); + SearchMenusMenu.this.updateResultItems(); } @Override public void changedUpdate(DocumentEvent e) { - this.updateResultItems(); + SearchMenusMenu.this.updateResultItems(); } }); - // TODO KeyBinds: up/down -> prev/next result, enter -> doClick on selected result + // TODO KeyBinds: enter -> doClick on selected result this.retranslate(); } - public void clearResults() { - this.resultManager.clear(); + private void updateResultItems() { + final String searchTerm = this.field.getText(); + + final Results results = this.getLookup().search(searchTerm); + + if (results instanceof Results.None) { + this.keepOnlyPermanentChildren(); + + this.noResults.setVisible(!searchTerm.isEmpty()); + + this.positionAndPackPopup(); + } else if (results instanceof Results.Different different) { + this.keepOnlyPermanentChildren(); + + this.noResults.setVisible(different.results.isEmpty()); + + different.results.forEach(this::add); + + this.positionAndPackPopup(); + } // else Results.Same + } + + private void positionAndPackPopup() { + this.getPopupMenu().pack(); + + // TODO the position is still off a bit (search "i" then delete it) + // if packing the popup previously forced it to move to stay on screen, it won't shift itself back + // reset it to default position to fix this + final Point popupMenuOrigin = this.getPopupMenuOrigin(); + this.getPopupMenu().show(this, popupMenuOrigin.x, popupMenuOrigin.y); + } + + private void addPermanentChildren() { + this.add(this.field); + this.add(this.noResults); + } + + private void keepOnlyPermanentChildren() { + this.removeAll(); + this.addPermanentChildren(); + } + + private Lookup getLookup() { + if (this.lookup == null) { + this.lookup = Lookup.build(this.gui); + } + + return this.lookup; + } + + public void clearLookup() { + this.lookup = null; } @Override public void updateState(boolean jarOpen, ConnectionState state) { - this.clearResults(); + this.clearLookup(); } @Override public void retranslate() { - this.clearResults(); + this.clearLookup(); this.setText(I18n.translate("menu.help.search")); this.field.setPlaceholder(I18n.translate("menu.help.search.placeholder")); this.noResults.setText(I18n.translate("menu.help.search.no_results")); } - private static class Result { - final SearchableElement element; + private static final class Lookup { + static final int MAX_SUBSTRING_LENGTH = 2; - @Nullable JMenuItem item; + final ResultCache emptyCache = new ResultCache( + "", + CompositeStringMultiTrie.of(Map::of, List::of).getRoot().view(), + ImmutableList.of() + ); - Result(SearchableElement element) { - this.element = element; - } + static int getCommonPrefixLength(String left, String right) { + final int minLength = Math.min(left.length(), right.length()); - JMenuItem getItem() { - if (this.item == null) { - this.item = new JMenuItem(this.element.getSearchName()); + for (int i = 0; i < minLength; i++) { + if (left.charAt(i) != right.charAt(i)) { + return i; + } } - return this.item; + return minLength; } - } - - private class ResultManager { - @Nullable - StringMultiTrie resultTrie; - @Nullable - CurrentResults currentResults; - - /** - * @return {@code true} if there are any results, or {@code false} otherwise - */ - UpdateOutcome updateResultItems(String searchTerm) { - if (this.currentResults == null) { - return this.initializeCurrentResults(searchTerm); - } else { - final int commonPrefixLength = - getCommonPrefixLengthIgnoreCase(this.currentResults.searchTerm, searchTerm); - final int termLength = searchTerm.length(); - final int currentTermLength = this.currentResults.searchTerm.length(); - - if (commonPrefixLength == 0) { - return this.initializeCurrentResults(searchTerm); - } else if (commonPrefixLength == termLength && commonPrefixLength == currentTermLength) { - return UpdateOutcome.SAME_RESULTS; - } else { - final ImmutableList> commonPrefixNodes; - final int backSteps = currentTermLength - commonPrefixLength; - if (backSteps > 0) { - commonPrefixNodes = this.currentResults.nodes.stream() - .map(node -> node.previous(backSteps)) - .distinct() - .collect(toImmutableList()); - } else { - commonPrefixNodes = this.currentResults.nodes; - } - if (termLength > commonPrefixLength) { - ImmutableList> resultNodes = commonPrefixNodes; - for (int i = commonPrefixLength; i < termLength; i++) { - final Character key = searchTerm.charAt(i); - resultNodes = resultNodes.stream() - .flatMap(node -> node.streamNextIgnoreCase(key)) - .collect(toImmutableList()); + static Lookup build(Gui gui) { + final CompositeStringMultiTrie prefixBuilder = CompositeStringMultiTrie.createHashed(); + final CompositeStringMultiTrie substringBuilder = CompositeStringMultiTrie.createHashed(); + gui.getMenuBar() + .streamMenus() + .flatMap(SearchMenusMenu::streamElementTree) + .mapMulti((element, keep) -> { + if (element instanceof SearchableElement searchable) { + keep.accept(searchable); } + }) + .map(Result::new) + .forEach(result -> result.lowerCaseAliases.forEach(alias -> { + prefixBuilder.put(alias, result); + + final int aliasLength = alias.length(); + for (int start = 1; start < aliasLength; start++) { + final int end = Math.min(start + MAX_SUBSTRING_LENGTH, aliasLength); + MutableStringMultiTrie.Node node = substringBuilder.getRoot(); + for (int i = start; i < end; i++) { + node = node.next(alias.charAt(i)); + } + + node.put(result); + } + })); - if (resultNodes.isEmpty()) { - this.clearCurrent(); + return new Lookup(prefixBuilder.view(), substringBuilder.view()); + } - return UpdateOutcome.NO_RESULTS; - } else { - this.currentResults.stream().map(Result::getItem).forEach(SearchMenusMenu.this::remove); + // maps complete search aliases to their corresponding results + final StringMultiTrie prefixResults; + // maps all non-prefix MAX_SUBSTRING_LENGTH-length (or less) substrings of search + // aliases to their corresponding result; used to narrow down the search scope for substring matches + final StringMultiTrie substringResults; - this.currentResults = new CurrentResults(resultNodes, searchTerm); + @NonNull + ResultCache resultCache = this.emptyCache; - this.currentResults.stream().map(Result::getItem).forEach(SearchMenusMenu.this::add); + Lookup(StringMultiTrie prefixResults, StringMultiTrie substringResults) { + this.prefixResults = prefixResults; + this.substringResults = substringResults; + } - return UpdateOutcome.DIFFERENT_RESULTS; - } - } else { - this.currentResults = new CurrentResults(commonPrefixNodes, searchTerm); + Results search(String term) { + if (term.isEmpty()) { + final boolean wasEmpty = !this.resultCache.hasResults(); - this.currentResults.stream().map(Result::getItem).forEach(SearchMenusMenu.this::add); + this.resultCache = this.emptyCache; - return UpdateOutcome.DIFFERENT_RESULTS; - } - } + return wasEmpty ? Results.Same.INSTANCE : Results.None.INSTANCE; } - } - private static int getCommonPrefixLengthIgnoreCase(String left, String right) { - final int minLength = Math.min(left.length(), right.length()); + final ResultCache oldCache = this.resultCache; + this.resultCache = this.resultCache.updated(term.toLowerCase()); - for (int i = 0; i < minLength; i++) { - if (!equalsIgnoreCase(left.charAt(i), right.charAt(i))) { - return i; + if (this.resultCache.hasResults()) { + if (this.resultCache.hasSameResults(oldCache)) { + return Results.Same.INSTANCE; + } else { + return Results.Different.of(this.resultCache); } + } else { + return Results.None.INSTANCE; } - - return minLength; } - private static boolean equalsIgnoreCase(char left, char right) { - if (left == right) { - return true; - } else if (Character.isUpperCase(right)) { - return left == Character.toLowerCase(right); - } else if (Character.isLowerCase(right)) { - return left == Character.toUpperCase(right); - } else { - return false; + final class ResultCache { + final String term; + final Node prefixNode; + final ImmutableList containingResults; + + ResultCache( + String term, Node prefixNode, ImmutableList containingResults + ) { + this.term = term; + this.prefixNode = prefixNode; + this.containingResults = containingResults; } - } - UpdateOutcome initializeCurrentResults(String searchTerm) { - final ImmutableList> resultNodes = this.getResultTrie() - .streamIgnoreCase(searchTerm) - .collect(toImmutableList()); - if (resultNodes.isEmpty()) { - this.clearCurrent(); + boolean hasResults() { + return !this.prefixNode.isEmpty() || !this.containingResults.isEmpty(); + } - return UpdateOutcome.NO_RESULTS; - } else { - this.currentResults = new CurrentResults(resultNodes, searchTerm); - this.currentResults.stream().map(Result::getItem).forEach(SearchMenusMenu.this::add); + boolean hasSameResults(ResultCache other) { + return this == other + || this.prefixNode == other.prefixNode + && this.containingResults.equals(other.containingResults); + } + + ResultCache updated(String term) { + if (this.term.isEmpty()) { + return this.createFresh(term); + } else { + final int commonPrefixLength = getCommonPrefixLength(this.term, term); + final int termLength = term.length(); + final int thisTermLength = this.term.length(); + + if (commonPrefixLength == 0) { + return this.createFresh(term); + } else if (commonPrefixLength == termLength && commonPrefixLength == thisTermLength) { + return this; + } else { + Node prefixNode = this.prefixNode.previous(thisTermLength - commonPrefixLength); + if (termLength > commonPrefixLength) { + for (int i = commonPrefixLength; i < termLength; i++) { + prefixNode = prefixNode.next(term.charAt(i)); + + if (prefixNode.isEmpty()) { + break; + } + } + } + + final ImmutableList containingResults; + if (termLength > thisTermLength && termLength > MAX_SUBSTRING_LENGTH) { + containingResults = this.containingResults.stream() + .filter(result -> result.anyLowerCaseAliasContains(term)) + .collect(toImmutableList()); + } else { + containingResults = this.buildContaining(term); + } - return UpdateOutcome.DIFFERENT_RESULTS; + return new ResultCache(term, prefixNode, containingResults); + } + } } - } - StringMultiTrie getResultTrie() { - if (this.resultTrie == null) { - this.resultTrie = this.buildResultTrie(); + ResultCache createFresh(String term) { + return new ResultCache(term, Lookup.this.prefixResults.get(term), this.buildContaining(term)); } - return this.resultTrie; - } + ImmutableList buildContaining(String term) { + final int termLength = term.length(); + final List possibleResults = new ArrayList<>(); + // start at 1 because prefixes are handled by prefixTrie + for (int start = 1; start <= termLength; start++) { + final int end = Math.min(start + MAX_SUBSTRING_LENGTH, termLength); + Node node = Lookup.this.substringResults.getRoot(); + for (int i = start; i < end; i++) { + node = node.next(term.charAt(i)); + + if (node.isEmpty()) { + break; + } + } + + node.streamValues().forEach(possibleResults::add); + } - void clear() { - this.resultTrie = null; - this.clearCurrent(); + return possibleResults.stream() + .distinct() + .filter(result -> result.anyLowerCaseAliasContains(term)) + .collect(toImmutableList()); + } } + } + + private static class Result implements Comparable { + final SearchableElement element; - void clearCurrent() { - if (this.currentResults != null) { - this.currentResults.stream() - .map(Result::getItem) - .forEach(SearchMenusMenu.this::remove); + final ImmutableList lowerCaseAliases; - this.currentResults = null; + // TODO display alias in item (if not search name) + @Nullable JMenuItem item; + + Result(SearchableElement element) { + this.element = element; + this.lowerCaseAliases = this.element.streamSearchAliases() + .filter(alias -> !alias.isEmpty()) + .map(String::toLowerCase) + .collect(toImmutableList()); + } + + JMenuItem getItem() { + if (this.item == null) { + this.item = new JMenuItem(this.element.getSearchName()); } + + return this.item; } - StringMultiTrie buildResultTrie() { - final CompositeStringMultiTrie elementsBuilder = CompositeStringMultiTrie.createHashed(); - SearchMenusMenu.this.gui.getMenuBar() - .streamMenus() - .flatMap(SearchMenusMenu::streamElementTree) - .mapMulti((element, keep) -> { - if (element instanceof SearchableElement searchable) { - keep.accept(searchable); - } - }) - .map(Result::new) - .forEach(result -> result - .element.streamSearchAliases() - .forEach(alias -> elementsBuilder.put(alias, result)) - ); + boolean anyLowerCaseAliasContains(String term) { + return this.lowerCaseAliases.stream().anyMatch(alias -> alias.contains(term)); + } - return elementsBuilder.view(); + @Override + public int compareTo(@NonNull Result other) { + return this.element.getSearchName().compareTo(other.element.getSearchName()); } + } - record CurrentResults(ImmutableList> nodes, String searchTerm) { - CurrentResults(Iterable> nodes, String searchTerm) { - this(ImmutableList.copyOf(nodes), searchTerm); - } + private sealed interface Results { + final class None implements Results { + static final None INSTANCE = new None(); + } - Stream stream() { - return this.nodes.stream().flatMap(StringMultiTrie.Node::streamValues); - } + final class Same implements Results { + static final Same INSTANCE = new Same(); } - enum UpdateOutcome { - NO_RESULTS, SAME_RESULTS, DIFFERENT_RESULTS + record Different(ImmutableList results) implements Results { + static Different of(Lookup.ResultCache cache) { + return new Different(Stream + .concat(cache.prefixNode.streamValues().sorted(), cache.containingResults.stream().sorted()) + .map(Result::getItem) + .collect(toImmutableList()) + ); + } } } } From 8b5466d97b195cbec1e2995409c0bd2b4836c146 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Fri, 21 Nov 2025 11:41:05 -0800 Subject: [PATCH 085/124] CompositeStringMultiTrie: decrease branch map initial capacity with depth --- .../gui/element/menu_bar/SearchMenusMenu.java | 2 +- .../multi_trie/CompositeStringMultiTrie.java | 44 +++++++++++++------ 2 files changed, 31 insertions(+), 15 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java index 1c664136e..de31804a4 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java @@ -189,7 +189,7 @@ private static final class Lookup { final ResultCache emptyCache = new ResultCache( "", - CompositeStringMultiTrie.of(Map::of, List::of).getRoot().view(), + CompositeStringMultiTrie.of(List::of, ignored -> Map.of()).getRoot().view(), ImmutableList.of() ); diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java index dfe6ce3b9..4794fcd48 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java @@ -6,6 +6,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Map; +import java.util.function.IntFunction; import java.util.function.Supplier; import java.util.stream.Stream; @@ -14,10 +15,16 @@ * * @param the type of values * - * @see #of(Supplier, Supplier) + * @see #of(Supplier, IntFunction) * @see #createHashed() */ public final class CompositeStringMultiTrie implements MutableStringMultiTrie { + private static final int HASHED_NODE_MIN_INITIAL_CAPACITY = 2; + private static final int HASHED_ROOT_INITIAL_CAPACITY_POWER = 4; + // 32 + private static final int HASHED_ROOT_INITIAL_CAPACITY = + HASHED_NODE_MIN_INITIAL_CAPACITY << HASHED_ROOT_INITIAL_CAPACITY_POWER; + private final Root root; private final View view = new View(); @@ -27,37 +34,42 @@ public final class CompositeStringMultiTrie implements MutableStringMultiTrie * * @param the type of values stored in the created trie * - * @see #of(Supplier, Supplier) + * @see #of(Supplier, IntFunction) */ public static CompositeStringMultiTrie createHashed() { - return of(HashMap::new, HashSet::new); + // decrease minimum capacity by a factor of 2 at each depth + return of(HashSet::new, depth -> new HashMap<>(depth >= HASHED_ROOT_INITIAL_CAPACITY_POWER + ? HASHED_NODE_MIN_INITIAL_CAPACITY + : HASHED_ROOT_INITIAL_CAPACITY >> depth + )); } /** * Creates a trie with nodes whose branches are held in maps created by the passed {@code branchesFactory} * and whose leaves are held in collections created by the passed {@code leavesFactory}. * - * @param branchesFactory a pure method that creates a new, empty {@link Map} in which to hold branch nodes * @param leavesFactory a pure method that create a new, empty {@link Collection} in which to hold leaf values + * @param branchesFactory a pure method that creates a new, empty {@link Map} in which to hold branch nodes; + * it receives the depth of the node which may be used to calculate an initial capacity * * @param the type of values stored in the created trie * * @see #createHashed() */ public static CompositeStringMultiTrie of( - Supplier>> branchesFactory, - Supplier> leavesFactory + Supplier> leavesFactory, + IntFunction>> branchesFactory ) { - return new CompositeStringMultiTrie<>(branchesFactory, leavesFactory); + return new CompositeStringMultiTrie<>(leavesFactory, branchesFactory); } private CompositeStringMultiTrie( - Supplier>> childrenFactory, - Supplier> leavesFactory + Supplier> leavesFactory, + IntFunction>> branchesFactory ) { this.root = new Root<>( - childrenFactory.get(), leavesFactory.get(), - new Branch.Factory<>(leavesFactory, childrenFactory) + branchesFactory.apply(Root.DEPTH), leavesFactory.get(), + new Branch.Factory<>(leavesFactory, branchesFactory) ); } @@ -75,6 +87,8 @@ public StringMultiTrie view() { static final class Root extends MutableMapNode> implements Node { + private static final int DEPTH = 0; + private final Collection leaves; private final Map> branches; @@ -103,7 +117,7 @@ public Node previous(int steps) { @Override public int getDepth() { - return 0; + return DEPTH; } @Override @@ -206,12 +220,14 @@ public StringMultiTrie.Node view() { private record Factory( Supplier> leavesFactory, - Supplier>> branchesFactory + IntFunction>> branchesFactory ) {

    > & Node> CompositeStringMultiTrie.Branch create(char key, P parent) { + final int depth = parent.getDepth() + 1; return new CompositeStringMultiTrie.Branch<>( - parent, key, parent.getDepth() + 1, this.leavesFactory.get(), this.branchesFactory.get(), + parent, key, depth, + this.leavesFactory.get(), this.branchesFactory.apply(depth), this ); } From 8deebdb85fff73e3149af22222c7f3473716df5a Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Fri, 21 Nov 2025 11:58:50 -0800 Subject: [PATCH 086/124] add EmptyStringMultiTrie --- .../gui/element/menu_bar/SearchMenusMenu.java | 8 +- .../util/multi_trie/EmptyStringMultiTrie.java | 79 +++++++++++++++++++ 2 files changed, 81 insertions(+), 6 deletions(-) create mode 100644 enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/EmptyStringMultiTrie.java diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java index de31804a4..83b8a36ba 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java @@ -8,6 +8,7 @@ import org.quiltmc.enigma.gui.element.PlaceheldTextField; import org.quiltmc.enigma.util.I18n; import org.quiltmc.enigma.util.multi_trie.CompositeStringMultiTrie; +import org.quiltmc.enigma.util.multi_trie.EmptyStringMultiTrie; import org.quiltmc.enigma.util.multi_trie.MutableStringMultiTrie; import org.quiltmc.enigma.util.multi_trie.StringMultiTrie; import org.quiltmc.enigma.util.multi_trie.StringMultiTrie.Node; @@ -25,7 +26,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.Map; import java.util.stream.Stream; import static com.google.common.collect.ImmutableList.toImmutableList; @@ -187,11 +187,7 @@ public void retranslate() { private static final class Lookup { static final int MAX_SUBSTRING_LENGTH = 2; - final ResultCache emptyCache = new ResultCache( - "", - CompositeStringMultiTrie.of(List::of, ignored -> Map.of()).getRoot().view(), - ImmutableList.of() - ); + final ResultCache emptyCache = new ResultCache("", EmptyStringMultiTrie.Node.get(), ImmutableList.of()); static int getCommonPrefixLength(String left, String right) { final int minLength = Math.min(left.length(), right.length()); diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/EmptyStringMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/EmptyStringMultiTrie.java new file mode 100644 index 000000000..333106bd2 --- /dev/null +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/EmptyStringMultiTrie.java @@ -0,0 +1,79 @@ +package org.quiltmc.enigma.util.multi_trie; + +import java.util.stream.Stream; + +/** + * An empty, immutable singleton {@link StringMultiTrie}. + */ +public final class EmptyStringMultiTrie implements StringMultiTrie { + private static final EmptyStringMultiTrie INSTANCE = new EmptyStringMultiTrie<>(); + + @SuppressWarnings("unchecked") + public static EmptyStringMultiTrie get() { + return (EmptyStringMultiTrie) INSTANCE; + } + + @Override + public StringMultiTrie.Node getRoot() { + return Node.get(); + } + + @Override + public StringMultiTrie.Node get(String prefix) { + return Node.get(); + } + + @Override + public Stream> streamIgnoreCase(String prefix) { + return Stream.empty(); + } + + /** + * An empty, immutable singleton {@link StringMultiTrie.Node}. + */ + public static final class Node implements StringMultiTrie.Node { + private static final Node INSTANCE = new Node<>(); + + @SuppressWarnings("unchecked") + public static Node get() { + return (Node) INSTANCE; + } + + private Node() { } + + @Override + public Stream streamLeaves() { + return Stream.empty(); + } + + @Override + public Stream streamStems() { + return Stream.empty(); + } + + @Override + public StringMultiTrie.Node next(Character key) { + return this; + } + + @Override + public Stream> streamNextIgnoreCase(Character key) { + return Stream.empty(); + } + + @Override + public StringMultiTrie.Node previous() { + return this; + } + + @Override + public StringMultiTrie.Node previous(int steps) { + return this; + } + + @Override + public int getDepth() { + return 0; + } + } +} From f42b19c2c6c6d97d569bbe403636d1dd2366f678 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Fri, 21 Nov 2025 12:03:40 -0800 Subject: [PATCH 087/124] tweak SearchMenusMenu::streamElementTree --- .../quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java index 83b8a36ba..cf6999764 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java @@ -33,10 +33,11 @@ public class SearchMenusMenu extends AbstractEnigmaMenu { /** * @return a breadth-first stream of the passed {@code root} element and all of its sub-elements, - * excluding {@link SearchMenusMenu}s and their sub-elements + * excluding the {@link HelpMenu} and its sub-elements; the help menu is not searchable because it must be open + * to start searching in the first place */ private static Stream streamElementTree(MenuElement root) { - return root instanceof SearchMenusMenu ? Stream.empty() : Stream.concat( + return root instanceof HelpMenu ? Stream.empty() : Stream.concat( Stream.of(root), Arrays.stream(root.getSubElements()).flatMap(SearchMenusMenu::streamElementTree) ); From 791766a49b9e6027045e12188bbfbdc05602c48f Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Fri, 21 Nov 2025 13:53:02 -0800 Subject: [PATCH 088/124] fix SearchMenusMenu's popup's positioning+border --- .../gui/element/menu_bar/SearchMenusMenu.java | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java index cf6999764..db6768b6e 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java @@ -15,6 +15,7 @@ import javax.swing.JMenuItem; import javax.swing.MenuElement; +import javax.swing.border.Border; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import javax.swing.event.MenuEvent; @@ -31,6 +32,9 @@ import static com.google.common.collect.ImmutableList.toImmutableList; public class SearchMenusMenu extends AbstractEnigmaMenu { + @Nullable + private final Border defaultPopupBorder; + /** * @return a breadth-first stream of the passed {@code root} element and all of its sub-elements, * excluding the {@link HelpMenu} and its sub-elements; the help menu is not searchable because it must be open @@ -52,6 +56,8 @@ private static Stream streamElementTree(MenuElement root) { protected SearchMenusMenu(Gui gui) { super(gui); + this.defaultPopupBorder = this.getPopupMenu().getBorder(); + this.noResults.setEnabled(false); this.noResults.setVisible(false); @@ -64,6 +70,7 @@ protected SearchMenusMenu(Gui gui) { } }); + // TODO try PopupMenuListener instead // Only select field text when the menu is selected, so text isn't selected when packing new search results. this.addMenuListener(new MenuListener() { final HierarchyListener fieldTextSelector = new HierarchyListener() { @@ -127,7 +134,7 @@ private void updateResultItems() { this.noResults.setVisible(!searchTerm.isEmpty()); - this.positionAndPackPopup(); + this.refreshPopup(); } else if (results instanceof Results.Different different) { this.keepOnlyPermanentChildren(); @@ -135,16 +142,16 @@ private void updateResultItems() { different.results.forEach(this::add); - this.positionAndPackPopup(); + this.refreshPopup(); } // else Results.Same } - private void positionAndPackPopup() { + private void refreshPopup() { + // HACK: When popups are resizing in limited space, they may remove their borders. + // The border won't be restored when re-packing or showing, so manually restore the original border here. + this.getPopupMenu().setBorder(this.defaultPopupBorder); this.getPopupMenu().pack(); - // TODO the position is still off a bit (search "i" then delete it) - // if packing the popup previously forced it to move to stay on screen, it won't shift itself back - // reset it to default position to fix this final Point popupMenuOrigin = this.getPopupMenuOrigin(); this.getPopupMenu().show(this, popupMenuOrigin.x, popupMenuOrigin.y); } From f8fed778707e348f1da3f43c53451fbc5a8db4ab Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Fri, 21 Nov 2025 14:10:06 -0800 Subject: [PATCH 089/124] SearchMenusMenu: replace hacky MenuListener with much cleaner PopupMenuListener --- .../gui/element/menu_bar/SearchMenusMenu.java | 37 +++++-------------- 1 file changed, 9 insertions(+), 28 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java index db6768b6e..488550f1b 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java @@ -18,12 +18,10 @@ import javax.swing.border.Border; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; -import javax.swing.event.MenuEvent; -import javax.swing.event.MenuListener; +import javax.swing.event.PopupMenuEvent; +import javax.swing.event.PopupMenuListener; import java.awt.Component; import java.awt.Point; -import java.awt.event.HierarchyEvent; -import java.awt.event.HierarchyListener; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -70,36 +68,19 @@ protected SearchMenusMenu(Gui gui) { } }); - // TODO try PopupMenuListener instead - // Only select field text when the menu is selected, so text isn't selected when packing new search results. - this.addMenuListener(new MenuListener() { - final HierarchyListener fieldTextSelector = new HierarchyListener() { - @Override - public void hierarchyChanged(HierarchyEvent e) { - if (SearchMenusMenu.this.field.isShowing()) { - SearchMenusMenu.this.field.removeHierarchyListener(this); - - SearchMenusMenu.this.field.selectAll(); - - SearchMenusMenu.this.updateResultItems(); - } - } - }; - + this.getPopupMenu().addPopupMenuListener(new PopupMenuListener() { @Override - public void menuSelected(MenuEvent e) { - SearchMenusMenu.this.field.addHierarchyListener(this.fieldTextSelector); + public void popupMenuWillBecomeVisible(PopupMenuEvent e) { + SearchMenusMenu.this.field.selectAll(); + + SearchMenusMenu.this.updateResultItems(); } @Override - public void menuDeselected(MenuEvent e) { - SearchMenusMenu.this.field.removeHierarchyListener(this.fieldTextSelector); - } + public void popupMenuWillBecomeInvisible(PopupMenuEvent e) { } @Override - public void menuCanceled(MenuEvent e) { - SearchMenusMenu.this.field.removeHierarchyListener(this.fieldTextSelector); - } + public void popupMenuCanceled(PopupMenuEvent e) { } }); this.field.getDocument().addDocumentListener(new DocumentListener() { From efaecbb02dc8c1b96809906ae9c9bd4327fb9a8b Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Fri, 21 Nov 2025 16:10:45 -0800 Subject: [PATCH 090/124] show search alias of search item when it doesn't match name --- .../gui/element/menu_bar/SearchMenusMenu.java | 194 +++++++++++++----- 1 file changed, 137 insertions(+), 57 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java index 488550f1b..b2a2d8f31 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java @@ -1,6 +1,8 @@ package org.quiltmc.enigma.gui.element.menu_bar; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; import org.quiltmc.enigma.gui.ConnectionState; @@ -24,10 +26,16 @@ import java.awt.Point; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; import java.util.stream.Stream; import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.common.collect.ImmutableMap.toImmutableMap; +import static com.google.common.collect.ImmutableSet.toImmutableSet; public class SearchMenusMenu extends AbstractEnigmaMenu { @Nullable @@ -176,7 +184,7 @@ public void retranslate() { private static final class Lookup { static final int MAX_SUBSTRING_LENGTH = 2; - final ResultCache emptyCache = new ResultCache("", EmptyStringMultiTrie.Node.get(), ImmutableList.of()); + final ResultCache emptyCache = new ResultCache("", EmptyStringMultiTrie.Node.get(), ImmutableSet.of()); static int getCommonPrefixLength(String left, String right) { final int minLength = Math.min(left.length(), right.length()); @@ -192,7 +200,8 @@ static int getCommonPrefixLength(String left, String right) { static Lookup build(Gui gui) { final CompositeStringMultiTrie prefixBuilder = CompositeStringMultiTrie.createHashed(); - final CompositeStringMultiTrie substringBuilder = CompositeStringMultiTrie.createHashed(); + final CompositeStringMultiTrie containingBuilder = + CompositeStringMultiTrie.createHashed(); gui.getMenuBar() .streamMenus() .flatMap(SearchMenusMenu::streamElementTree) @@ -201,37 +210,49 @@ static Lookup build(Gui gui) { keep.accept(searchable); } }) - .map(Result::new) - .forEach(result -> result.lowerCaseAliases.forEach(alias -> { - prefixBuilder.put(alias, result); - - final int aliasLength = alias.length(); - for (int start = 1; start < aliasLength; start++) { - final int end = Math.min(start + MAX_SUBSTRING_LENGTH, aliasLength); - MutableStringMultiTrie.Node node = substringBuilder.getRoot(); - for (int i = start; i < end; i++) { - node = node.next(alias.charAt(i)); - } + .forEach(element -> { + final ImmutableMap aliasesByLowercase = element.streamSearchAliases() + .filter(alias -> !alias.isEmpty()) + .collect(toImmutableMap( + String::toLowerCase, + Function.identity(), + // ignore case-insensitive duplicate aliases + (left, right) -> left + )); + + final Result result = new Result(element, aliasesByLowercase); + + aliasesByLowercase.keySet().forEach(alias -> { + prefixBuilder.put(alias, result); + + final int aliasLength = alias.length(); + for (int start = 1; start < aliasLength; start++) { + final int end = Math.min(start + MAX_SUBSTRING_LENGTH, aliasLength); + MutableStringMultiTrie.Node node = containingBuilder.getRoot(); + for (int i = start; i < end; i++) { + node = node.next(alias.charAt(i)); + } - node.put(result); - } - })); + node.put(result); + } + }); + }); - return new Lookup(prefixBuilder.view(), substringBuilder.view()); + return new Lookup(prefixBuilder.view(), containingBuilder.view()); } // maps complete search aliases to their corresponding results final StringMultiTrie prefixResults; // maps all non-prefix MAX_SUBSTRING_LENGTH-length (or less) substrings of search // aliases to their corresponding result; used to narrow down the search scope for substring matches - final StringMultiTrie substringResults; + final StringMultiTrie containingResults; @NonNull ResultCache resultCache = this.emptyCache; - Lookup(StringMultiTrie prefixResults, StringMultiTrie substringResults) { + Lookup(StringMultiTrie prefixResults, StringMultiTrie containingResults) { this.prefixResults = prefixResults; - this.substringResults = substringResults; + this.containingResults = containingResults; } Results search(String term) { @@ -260,24 +281,24 @@ Results search(String term) { final class ResultCache { final String term; final Node prefixNode; - final ImmutableList containingResults; + final ImmutableSet containingItems; ResultCache( - String term, Node prefixNode, ImmutableList containingResults + String term, Node prefixNode, ImmutableSet containingItems ) { this.term = term; this.prefixNode = prefixNode; - this.containingResults = containingResults; + this.containingItems = containingItems; } boolean hasResults() { - return !this.prefixNode.isEmpty() || !this.containingResults.isEmpty(); + return !this.prefixNode.isEmpty() || !this.containingItems.isEmpty(); } boolean hasSameResults(ResultCache other) { return this == other || this.prefixNode == other.prefixNode - && this.containingResults.equals(other.containingResults); + && this.containingItems.equals(other.containingItems); } ResultCache updated(String term) { @@ -304,16 +325,17 @@ ResultCache updated(String term) { } } - final ImmutableList containingResults; + final ImmutableSet containingItems; if (termLength > thisTermLength && termLength > MAX_SUBSTRING_LENGTH) { - containingResults = this.containingResults.stream() - .filter(result -> result.anyLowerCaseAliasContains(term)) - .collect(toImmutableList()); + containingItems = this.containingItems.stream() + .map(item -> item.getOwner().findContainingItem(term)) + .flatMap(Optional::stream) + .collect(toImmutableSet()); } else { - containingResults = this.buildContaining(term); + containingItems = this.buildContaining(term); } - return new ResultCache(term, prefixNode, containingResults); + return new ResultCache(term, prefixNode, containingItems); } } } @@ -322,13 +344,13 @@ ResultCache createFresh(String term) { return new ResultCache(term, Lookup.this.prefixResults.get(term), this.buildContaining(term)); } - ImmutableList buildContaining(String term) { + ImmutableSet buildContaining(String term) { final int termLength = term.length(); final List possibleResults = new ArrayList<>(); // start at 1 because prefixes are handled by prefixTrie for (int start = 1; start <= termLength; start++) { final int end = Math.min(start + MAX_SUBSTRING_LENGTH, termLength); - Node node = Lookup.this.substringResults.getRoot(); + Node node = Lookup.this.containingResults.getRoot(); for (int i = start; i < end; i++) { node = node.next(term.charAt(i)); @@ -342,43 +364,89 @@ ImmutableList buildContaining(String term) { return possibleResults.stream() .distinct() - .filter(result -> result.anyLowerCaseAliasContains(term)) - .collect(toImmutableList()); + .map(result -> result.findContainingItem(term)) + .flatMap(Optional::stream) + .collect(toImmutableSet()); } } } private static class Result implements Comparable { - final SearchableElement element; + final SearchableElement searchable; - final ImmutableList lowerCaseAliases; + final ImmutableMap aliasesByLowercase; + final Map holdersByAlias; - // TODO display alias in item (if not search name) - @Nullable JMenuItem item; + Result(SearchableElement searchable, ImmutableMap aliasesByLowercase) { + this.searchable = searchable; - Result(SearchableElement element) { - this.element = element; - this.lowerCaseAliases = this.element.streamSearchAliases() - .filter(alias -> !alias.isEmpty()) - .map(String::toLowerCase) - .collect(toImmutableList()); + this.aliasesByLowercase = aliasesByLowercase; + this.holdersByAlias = new HashMap<>(aliasesByLowercase.size()); } - JMenuItem getItem() { - if (this.item == null) { - this.item = new JMenuItem(this.element.getSearchName()); - } + @Override + public int compareTo(@NonNull Result other) { + return this.searchable.getSearchName().compareTo(other.searchable.getSearchName()); + } - return this.item; + SearchableElement getSearchable() { + return this.searchable; } - boolean anyLowerCaseAliasContains(String term) { - return this.lowerCaseAliases.stream().anyMatch(alias -> alias.contains(term)); + Optional findContainingItem(String term) { + return this.aliasesByLowercase.entrySet().stream() + .filter(entry -> entry.getKey().contains(term)) + .findFirst() + .map(Map.Entry::getValue) + .map(this::getItemHolderForAlias); } - @Override - public int compareTo(@NonNull Result other) { - return this.element.getSearchName().compareTo(other.element.getSearchName()); + /** + * @return the {@link ItemHolder} representing the alias prefixed with the passed {@code term} + * + * @throws IllegalArgumentException if no lowercase alias starts with the passed {@code term} + */ + ItemHolder getPrefixedItemOrThrow(String term) { + return this.aliasesByLowercase.entrySet() + .stream() + .filter(entry -> entry.getKey().startsWith(term)) + .findFirst() + .map(Map.Entry::getValue) + .map(this::getItemHolderForAlias) + .orElseThrow(() -> new IllegalArgumentException( + """ + No lowercase alias starts with "%s"! + \tlowercase aliases: %s + """.formatted(term, this.aliasesByLowercase.keySet())) + ); + } + + ItemHolder getItemHolderForAlias(String alias) { + return this.holdersByAlias.computeIfAbsent(alias, ItemHolder::new); + } + + class ItemHolder implements Comparable { + final JMenuItem item; + + ItemHolder(String matchedAlias) { + final String searchName = Result.this.searchable.getSearchName(); + this.item = new JMenuItem( + matchedAlias.equals(searchName) ? searchName : "%s (%s)".formatted(searchName, matchedAlias) + ); + } + + public Component getItem() { + return this.item; + } + + Result getOwner() { + return Result.this; + } + + @Override + public int compareTo(@NonNull ItemHolder other) { + return this.getOwner().compareTo(other.getOwner()); + } } } @@ -393,9 +461,21 @@ final class Same implements Results { record Different(ImmutableList results) implements Results { static Different of(Lookup.ResultCache cache) { + final ImmutableMap prefixedItemsByElement = cache.prefixNode + .streamValues() + .sorted() + .distinct() + .collect(toImmutableMap( + Result::getSearchable, + result -> result.getPrefixedItemOrThrow(cache.term).getItem() + )); + return new Different(Stream - .concat(cache.prefixNode.streamValues().sorted(), cache.containingResults.stream().sorted()) - .map(Result::getItem) + .concat(prefixedItemsByElement.values().stream(), cache.containingItems.stream() + .sorted() + .filter(holder -> !prefixedItemsByElement.containsKey(holder.getOwner().searchable)) + .map(Result.ItemHolder::getItem) + ) .collect(toImmutableList()) ); } From cd4134d9763ef381eba14dc7d49ca111e8620a3a Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Fri, 21 Nov 2025 16:11:23 -0800 Subject: [PATCH 091/124] allow CompositeStringMultiTrie branch maps to have an initial capacity of 1 --- .../enigma/util/multi_trie/CompositeStringMultiTrie.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java index 4794fcd48..5bb1ea3c7 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/multi_trie/CompositeStringMultiTrie.java @@ -19,8 +19,8 @@ * @see #createHashed() */ public final class CompositeStringMultiTrie implements MutableStringMultiTrie { - private static final int HASHED_NODE_MIN_INITIAL_CAPACITY = 2; - private static final int HASHED_ROOT_INITIAL_CAPACITY_POWER = 4; + private static final int HASHED_NODE_MIN_INITIAL_CAPACITY = 1; + private static final int HASHED_ROOT_INITIAL_CAPACITY_POWER = 5; // 32 private static final int HASHED_ROOT_INITIAL_CAPACITY = HASHED_NODE_MIN_INITIAL_CAPACITY << HASHED_ROOT_INITIAL_CAPACITY_POWER; From 1c6de81fafa00986273278a41c992ff5cae9aaf0 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sat, 22 Nov 2025 13:33:56 -0800 Subject: [PATCH 092/124] optimize containing results update in some cases improve result item alias appearance --- .../gui/element/PlaceheldTextField.java | 72 +++++---- .../gui/element/menu_bar/SearchMenusMenu.java | 146 +++++++++++++++--- .../org/quiltmc/enigma/gui/util/GuiUtil.java | 26 ++++ 3 files changed, 191 insertions(+), 53 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/PlaceheldTextField.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/PlaceheldTextField.java index 4ff77013d..0cda5f483 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/PlaceheldTextField.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/PlaceheldTextField.java @@ -1,6 +1,7 @@ package org.quiltmc.enigma.gui.element; import org.jspecify.annotations.Nullable; +import org.quiltmc.enigma.gui.util.GuiUtil; import org.quiltmc.enigma.util.Utils; import javax.swing.JTextField; @@ -10,33 +11,18 @@ import java.awt.Color; import java.awt.Component; import java.awt.Dimension; +import java.awt.Font; import java.awt.Graphics; -import java.awt.Graphics2D; import java.awt.Insets; -import java.awt.RenderingHints; -import java.awt.Toolkit; import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; -import java.util.Map; /** * A text field that displays placeholder text when it's empty. */ public class PlaceheldTextField extends JTextField implements MenuElement { - private static final String DESKTOP_FONT_HINTS_KEY = "awt.font.desktophints"; - @Nullable - private static Map desktopFontHints = Toolkit.getDefaultToolkit().getDesktopProperty(DESKTOP_FONT_HINTS_KEY) - instanceof Map map ? map : null; - - static { - Toolkit.getDefaultToolkit().addPropertyChangeListener(DESKTOP_FONT_HINTS_KEY, e -> { - desktopFontHints = e.getNewValue() instanceof Map map ? map : null; - }); - } - - @Nullable - private String placeholder; + private Placeholder placeholder; @Nullable private Color placeholderColor; @@ -74,7 +60,7 @@ public PlaceheldTextField( ) { super(doc, text, columns); - this.placeholder = placeholder; + this.placeholder = new Placeholder(placeholder); } @Override @@ -84,9 +70,7 @@ public Dimension getPreferredSize() { if (this.placeholder != null) { final Insets insets = this.getInsets(); - final int placeholderWidth = this.getFontMetrics(this.getFont()).stringWidth(this.placeholder); - - size.width = Math.max(insets.left + placeholderWidth + insets.right, size.width); + size.width = Math.max(insets.left + this.placeholder.getWidth() + insets.right, size.width); } return size; @@ -97,13 +81,7 @@ protected void paintComponent(Graphics graphics) { super.paintComponent(graphics); if (this.placeholder != null && this.getText().isEmpty()) { - if (graphics instanceof Graphics2D graphics2D) { - if (desktopFontHints == null) { - graphics2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - } else { - graphics2D.setRenderingHints(desktopFontHints); - } - } + GuiUtil.trySetRenderingHints(graphics); Utils.findFirstNonNull(this.placeholderColor, this.getDisabledTextColor(), this.getForeground()) .ifPresent(graphics::setColor); @@ -111,7 +89,7 @@ protected void paintComponent(Graphics graphics) { final Insets insets = this.getInsets(); final int baseY = graphics.getFontMetrics().getMaxAscent() + insets.top; - graphics.drawString(this.placeholder, insets.left, baseY); + graphics.drawString(this.placeholder.text, insets.left, baseY); } } @@ -119,7 +97,7 @@ protected void paintComponent(Graphics graphics) { * @param placeholder the placeholder text for this field; if {@code null}, no placeholder will be shown */ public void setPlaceholder(@Nullable String placeholder) { - this.placeholder = placeholder; + this.placeholder = new Placeholder(placeholder); } /** @@ -148,4 +126,38 @@ public MenuElement[] getSubElements() { public Component getComponent() { return this; } + + @Override + public void setFont(Font f) { + super.setFont(f); + + if (this.placeholder != null) { + this.placeholder.clearWidth(); + } + } + + private class Placeholder { + static final int UNSET_WIDTH = -1; + + final String text; + + int width = UNSET_WIDTH; + + Placeholder(String text) { + this.text = text; + } + + int getWidth() { + if (this.width < 0) { + this.width = PlaceheldTextField.this + .getFontMetrics(PlaceheldTextField.this.getFont()).stringWidth(this.text); + } + + return this.width; + } + + void clearWidth() { + this.width = UNSET_WIDTH; + } + } } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java index b2a2d8f31..18dd5ac64 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java @@ -8,6 +8,7 @@ import org.quiltmc.enigma.gui.ConnectionState; import org.quiltmc.enigma.gui.Gui; import org.quiltmc.enigma.gui.element.PlaceheldTextField; +import org.quiltmc.enigma.gui.util.GuiUtil; import org.quiltmc.enigma.util.I18n; import org.quiltmc.enigma.util.multi_trie.CompositeStringMultiTrie; import org.quiltmc.enigma.util.multi_trie.EmptyStringMultiTrie; @@ -22,14 +23,19 @@ import javax.swing.event.DocumentListener; import javax.swing.event.PopupMenuEvent; import javax.swing.event.PopupMenuListener; +import java.awt.Color; import java.awt.Component; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.Graphics; +import java.awt.Insets; import java.awt.Point; -import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; -import java.util.List; +import java.util.HashSet; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.function.Function; import java.util.stream.Stream; @@ -182,6 +188,7 @@ public void retranslate() { } private static final class Lookup { + static final int NON_PREFIX_START = 1; static final int MAX_SUBSTRING_LENGTH = 2; final ResultCache emptyCache = new ResultCache("", EmptyStringMultiTrie.Node.get(), ImmutableSet.of()); @@ -226,7 +233,7 @@ static Lookup build(Gui gui) { prefixBuilder.put(alias, result); final int aliasLength = alias.length(); - for (int start = 1; start < aliasLength; start++) { + for (int start = NON_PREFIX_START; start < aliasLength; start++) { final int end = Math.min(start + MAX_SUBSTRING_LENGTH, aliasLength); MutableStringMultiTrie.Node node = containingBuilder.getRoot(); for (int i = start; i < end; i++) { @@ -326,11 +333,20 @@ ResultCache updated(String term) { } final ImmutableSet containingItems; - if (termLength > thisTermLength && termLength > MAX_SUBSTRING_LENGTH) { - containingItems = this.containingItems.stream() - .map(item -> item.getOwner().findContainingItem(term)) - .flatMap(Optional::stream) - .collect(toImmutableSet()); + if (termLength > thisTermLength) { + if (termLength > MAX_SUBSTRING_LENGTH) { + containingItems = this.narrowedContainingItemsOf(term); + } else { + final Set containingPossibilities = this.getContainingPossibilities(term); + if (containingPossibilities.size() <= this.containingItems.size()) { + containingItems = containingPossibilities.stream() + .map(result -> result.findContainingItem(term)) + .flatMap(Optional::stream) + .collect(toImmutableSet()); + } else { + containingItems = this.narrowedContainingItemsOf(term); + } + } } else { containingItems = this.buildContaining(term); } @@ -344,11 +360,25 @@ ResultCache createFresh(String term) { return new ResultCache(term, Lookup.this.prefixResults.get(term), this.buildContaining(term)); } + private ImmutableSet narrowedContainingItemsOf(String term) { + return this.containingItems.stream() + .map(item -> item.getOwner().findContainingItem(term)) + .flatMap(Optional::stream) + .collect(toImmutableSet()); + } + ImmutableSet buildContaining(String term) { + return this.getContainingPossibilities(term) + .stream() + .map(result -> result.findContainingItem(term)) + .flatMap(Optional::stream) + .collect(toImmutableSet()); + } + + Set getContainingPossibilities(String term) { final int termLength = term.length(); - final List possibleResults = new ArrayList<>(); - // start at 1 because prefixes are handled by prefixTrie - for (int start = 1; start <= termLength; start++) { + final Set possibilities = new HashSet<>(); + for (int start = NON_PREFIX_START; start <= termLength; start++) { final int end = Math.min(start + MAX_SUBSTRING_LENGTH, termLength); Node node = Lookup.this.containingResults.getRoot(); for (int i = start; i < end; i++) { @@ -359,14 +389,10 @@ ImmutableSet buildContaining(String term) { } } - node.streamValues().forEach(possibleResults::add); + node.streamValues().forEach(possibilities::add); } - return possibleResults.stream() - .distinct() - .map(result -> result.findContainingItem(term)) - .flatMap(Optional::stream) - .collect(toImmutableSet()); + return possibilities; } } } @@ -416,9 +442,9 @@ ItemHolder getPrefixedItemOrThrow(String term) { .orElseThrow(() -> new IllegalArgumentException( """ No lowercase alias starts with "%s"! - \tlowercase aliases: %s - """.formatted(term, this.aliasesByLowercase.keySet())) - ); + \tlowercase aliases: %s\ + """.formatted(term, this.aliasesByLowercase.keySet()) + )); } ItemHolder getItemHolderForAlias(String alias) { @@ -430,9 +456,10 @@ class ItemHolder implements Comparable { ItemHolder(String matchedAlias) { final String searchName = Result.this.searchable.getSearchName(); - this.item = new JMenuItem( - matchedAlias.equals(searchName) ? searchName : "%s (%s)".formatted(searchName, matchedAlias) - ); + + this.item = matchedAlias.equals(searchName) + ? new JMenuItem(searchName) + : new AliasedItem(searchName, matchedAlias); } public Component getItem() { @@ -447,6 +474,79 @@ Result getOwner() { public int compareTo(@NonNull ItemHolder other) { return this.getOwner().compareTo(other.getOwner()); } + + static class AliasedItem extends JMenuItem { + static final int UNSET_WIDTH = -1; + + final String alias; + + int aliasWidth = UNSET_WIDTH; + @Nullable + Font aliasFont; + + AliasedItem(String searchName, String alias) { + super(searchName); + + this.alias = alias; + } + + @Override + public void setFont(Font font) { + super.setFont(font); + + this.aliasWidth = UNSET_WIDTH; + this.aliasFont = null; + } + + @Nullable + Font getAliasFont() { + if (this.aliasFont == null) { + final Font font = this.getFont(); + if (font != null) { + this.aliasFont = font.deriveFont(Font.ITALIC); + } + } + + return this.aliasFont; + } + + @Override + public Dimension getPreferredSize() { + final Dimension size = super.getPreferredSize(); + + size.width += this.getAliasWidth(); + + return size; + } + + @Override + public void paint(Graphics graphics) { + super.paint(graphics); + + GuiUtil.trySetRenderingHints(graphics); + final Color color = this.getForeground(); + if (color != null) { + graphics.setColor(color); + } + + final Font aliasFont = this.getAliasFont(); + if (aliasFont != null) { + graphics.setFont(aliasFont); + } + + final Insets insets = this.getInsets(); + final int baseY = graphics.getFontMetrics().getMaxAscent() + insets.top; + graphics.drawString(this.alias, this.getWidth() - insets.right - this.getAliasWidth(), baseY); + } + + int getAliasWidth() { + if (this.aliasWidth < 0) { + this.aliasWidth = this.getFontMetrics(this.getAliasFont()).stringWidth(this.alias); + } + + return this.aliasWidth; + } + } } } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/GuiUtil.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/GuiUtil.java index f9e9a973a..4a754d972 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/GuiUtil.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/GuiUtil.java @@ -2,6 +2,7 @@ import com.formdev.flatlaf.extras.FlatSVGIcon; import com.google.common.collect.ImmutableList; +import org.jspecify.annotations.Nullable; import org.quiltmc.config.api.values.TrackedValue; import org.quiltmc.enigma.api.analysis.index.jar.EntryIndex; import org.quiltmc.enigma.api.service.JarIndexerService; @@ -41,8 +42,11 @@ import java.awt.Cursor; import java.awt.Desktop; import java.awt.Font; +import java.awt.Graphics; +import java.awt.Graphics2D; import java.awt.MouseInfo; import java.awt.Point; +import java.awt.RenderingHints; import java.awt.Toolkit; import java.awt.datatransfer.DataFlavor; import java.awt.datatransfer.StringSelection; @@ -97,6 +101,28 @@ private GuiUtil() { public static final Icon CHEVRON_UP_WHITE = loadIcon("chevron-up-white"); public static final Icon CHEVRON_DOWN_WHITE = loadIcon("chevron-down-white"); + private static final String DESKTOP_FONT_HINTS_KEY = "awt.font.desktophints"; + + @Nullable + private static Map desktopFontHints = Toolkit.getDefaultToolkit().getDesktopProperty(DESKTOP_FONT_HINTS_KEY) + instanceof Map map ? map : null; + + static { + Toolkit.getDefaultToolkit().addPropertyChangeListener(DESKTOP_FONT_HINTS_KEY, e -> { + desktopFontHints = e.getNewValue() instanceof Map map ? map : null; + }); + } + + public static void trySetRenderingHints(Graphics graphics) { + if (graphics instanceof Graphics2D graphics2D) { + if (desktopFontHints == null) { + graphics2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + } else { + graphics2D.setRenderingHints(desktopFontHints); + } + } + } + public static void openUrl(String url) { try { if (Objects.requireNonNull(Os.getOs()) == Os.LINUX) { From c01a8012ab3d9f506466c74af85e31ef02885bd0 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sat, 22 Nov 2025 14:51:20 -0800 Subject: [PATCH 093/124] fix case where some cached containing results could be erroniously kept cache prefixedItemsBySearchable --- .../gui/element/menu_bar/SearchMenusMenu.java | 106 ++++++++++++------ 1 file changed, 73 insertions(+), 33 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java index 18dd5ac64..f6c7681ff 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java @@ -191,7 +191,10 @@ private static final class Lookup { static final int NON_PREFIX_START = 1; static final int MAX_SUBSTRING_LENGTH = 2; - final ResultCache emptyCache = new ResultCache("", EmptyStringMultiTrie.Node.get(), ImmutableSet.of()); + final ResultCache emptyCache = new ResultCache( + "", EmptyStringMultiTrie.Node.get(), + ImmutableMap.of(), ImmutableSet.of() + ); static int getCommonPrefixLength(String left, String right) { final int minLength = Math.min(left.length(), right.length()); @@ -286,15 +289,32 @@ Results search(String term) { } final class ResultCache { + static ImmutableSet buildContaining( + String term, Set possibilities, + Set excluded + ) { + return possibilities + .stream() + .filter(result -> !excluded.contains(result.searchable)) + .map(result -> result.findContainingItem(term)) + .flatMap(Optional::stream) + .sorted() + .collect(toImmutableSet()); + } + final String term; final Node prefixNode; + final ImmutableMap prefixedItemsBySearchable; final ImmutableSet containingItems; ResultCache( - String term, Node prefixNode, ImmutableSet containingItems + String term, Node prefixNode, + ImmutableMap prefixedItemsBySearchable, + ImmutableSet containingItems ) { this.term = term; this.prefixNode = prefixNode; + this.prefixedItemsBySearchable = prefixedItemsBySearchable; this.containingItems = containingItems; } @@ -314,15 +334,20 @@ ResultCache updated(String term) { } else { final int commonPrefixLength = getCommonPrefixLength(this.term, term); final int termLength = term.length(); - final int thisTermLength = this.term.length(); + final int cachedTermLength = this.term.length(); if (commonPrefixLength == 0) { return this.createFresh(term); - } else if (commonPrefixLength == termLength && commonPrefixLength == thisTermLength) { + } else if (commonPrefixLength == termLength && commonPrefixLength == cachedTermLength) { return this; } else { - Node prefixNode = this.prefixNode.previous(thisTermLength - commonPrefixLength); + final int backSteps = cachedTermLength - commonPrefixLength; + Node prefixNode = this.prefixNode.previous(backSteps); + // true iff this.term is a prefix of term or vice versa + final boolean oneTermIsPrefix; if (termLength > commonPrefixLength) { + oneTermIsPrefix = backSteps == 0; + for (int i = commonPrefixLength; i < termLength; i++) { prefixNode = prefixNode.next(term.charAt(i)); @@ -330,49 +355,74 @@ ResultCache updated(String term) { break; } } + } else { + oneTermIsPrefix = true; + } + + final ImmutableMap prefixedItemsBySearchable; + if (oneTermIsPrefix && this.prefixNode.getSize() == prefixNode.getSize()) { + prefixedItemsBySearchable = this.prefixedItemsBySearchable; + } else { + prefixedItemsBySearchable = buildPrefixedItemsBySearchable(term, prefixNode); } final ImmutableSet containingItems; - if (termLength > thisTermLength) { + if (cachedTermLength == commonPrefixLength) { if (termLength > MAX_SUBSTRING_LENGTH) { containingItems = this.narrowedContainingItemsOf(term); } else { final Set containingPossibilities = this.getContainingPossibilities(term); if (containingPossibilities.size() <= this.containingItems.size()) { - containingItems = containingPossibilities.stream() - .map(result -> result.findContainingItem(term)) - .flatMap(Optional::stream) - .collect(toImmutableSet()); + containingItems = buildContaining( + term, containingPossibilities, + prefixedItemsBySearchable.keySet() + ); } else { containingItems = this.narrowedContainingItemsOf(term); } } } else { - containingItems = this.buildContaining(term); + containingItems = this.buildContaining(term, prefixedItemsBySearchable.keySet()); } - return new ResultCache(term, prefixNode, containingItems); + return new ResultCache(term, prefixNode, prefixedItemsBySearchable, containingItems); } } } ResultCache createFresh(String term) { - return new ResultCache(term, Lookup.this.prefixResults.get(term), this.buildContaining(term)); + final Node prefixNode = Lookup.this.prefixResults.get(term); + final ImmutableMap prefixedItemsByElement = + buildPrefixedItemsBySearchable(term, prefixNode); + return new ResultCache( + term, prefixNode, + prefixedItemsByElement, + this.buildContaining(term, prefixedItemsByElement.keySet()) + ); + } + + static ImmutableMap buildPrefixedItemsBySearchable( + String term, Node prefixNode + ) { + return prefixNode + .streamValues() + .sorted() + .distinct() + .collect(toImmutableMap( + Result::getSearchable, + result -> result.getPrefixedItemOrThrow(term).getItem() + )); } - private ImmutableSet narrowedContainingItemsOf(String term) { + ImmutableSet narrowedContainingItemsOf(String term) { return this.containingItems.stream() .map(item -> item.getOwner().findContainingItem(term)) .flatMap(Optional::stream) .collect(toImmutableSet()); } - ImmutableSet buildContaining(String term) { - return this.getContainingPossibilities(term) - .stream() - .map(result -> result.findContainingItem(term)) - .flatMap(Optional::stream) - .collect(toImmutableSet()); + ImmutableSet buildContaining(String term, Set excluded) { + return buildContaining(term, this.getContainingPossibilities(term), excluded); } Set getContainingPossibilities(String term) { @@ -561,20 +611,10 @@ final class Same implements Results { record Different(ImmutableList results) implements Results { static Different of(Lookup.ResultCache cache) { - final ImmutableMap prefixedItemsByElement = cache.prefixNode - .streamValues() - .sorted() - .distinct() - .collect(toImmutableMap( - Result::getSearchable, - result -> result.getPrefixedItemOrThrow(cache.term).getItem() - )); - return new Different(Stream - .concat(prefixedItemsByElement.values().stream(), cache.containingItems.stream() - .sorted() - .filter(holder -> !prefixedItemsByElement.containsKey(holder.getOwner().searchable)) - .map(Result.ItemHolder::getItem) + .concat( + cache.prefixedItemsBySearchable.values().stream(), + cache.containingItems.stream().map(Result.ItemHolder::getItem) ) .collect(toImmutableList()) ); From 37cfbaaf222c904ca82488087f217be34e8a152e Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sat, 22 Nov 2025 15:31:35 -0800 Subject: [PATCH 094/124] implement basic searchable item click functionality --- .../gui/element/menu_bar/AbstractSearchableEnigmaMenu.java | 1 - .../quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java | 4 ++++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/AbstractSearchableEnigmaMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/AbstractSearchableEnigmaMenu.java index a40ba9606..bae8cd3d0 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/AbstractSearchableEnigmaMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/AbstractSearchableEnigmaMenu.java @@ -14,7 +14,6 @@ public String getSearchName() { @Override public void onSearchClicked() { - this.setSelected(true); this.doClick(); } } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java index f6c7681ff..dcb10ef1e 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java @@ -510,6 +510,10 @@ class ItemHolder implements Comparable { this.item = matchedAlias.equals(searchName) ? new JMenuItem(searchName) : new AliasedItem(searchName, matchedAlias); + + this.item.addActionListener(e -> { + Result.this.searchable.onSearchClicked(); + }); } public Component getItem() { From e87012df634b260fe5364f98edcabc6ab7466fc0 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sat, 22 Nov 2025 19:29:15 -0800 Subject: [PATCH 095/124] move ItemHolder functionality into new Item extends JMenuItem select search item while holding shift --- .../gui/element/menu_bar/SearchMenusMenu.java | 256 ++++++++++++------ 1 file changed, 173 insertions(+), 83 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java index dcb10ef1e..abcf20ca4 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java @@ -16,13 +16,18 @@ import org.quiltmc.enigma.util.multi_trie.StringMultiTrie; import org.quiltmc.enigma.util.multi_trie.StringMultiTrie.Node; +import javax.swing.JMenu; +import javax.swing.JMenuBar; import javax.swing.JMenuItem; +import javax.swing.JPopupMenu; import javax.swing.MenuElement; +import javax.swing.MenuSelectionManager; import javax.swing.border.Border; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import javax.swing.event.PopupMenuEvent; import javax.swing.event.PopupMenuListener; +import java.awt.AWTEvent; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; @@ -30,9 +35,15 @@ import java.awt.Graphics; import java.awt.Insets; import java.awt.Point; +import java.awt.Toolkit; +import java.awt.event.AWTEventListener; +import java.awt.event.InputEvent; +import java.awt.event.KeyEvent; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -68,6 +79,10 @@ private static Stream streamElementTree(MenuElement root) { protected SearchMenusMenu(Gui gui) { super(gui); + // global listener because menu/item key listeners didn't fire + // also more reliably clears restorablePath + Toolkit.getDefaultToolkit().addAWTEventListener(SearchPathPreviewer.INSTANCE, AWTEvent.KEY_EVENT_MASK); + this.defaultPopupBorder = this.getPopupMenu().getBorder(); this.noResults.setEnabled(false); @@ -289,7 +304,7 @@ Results search(String term) { } final class ResultCache { - static ImmutableSet buildContaining( + static ImmutableSet buildContaining( String term, Set possibilities, Set excluded ) { @@ -305,12 +320,12 @@ static ImmutableSet buildContaining( final String term; final Node prefixNode; final ImmutableMap prefixedItemsBySearchable; - final ImmutableSet containingItems; + final ImmutableSet containingItems; ResultCache( String term, Node prefixNode, ImmutableMap prefixedItemsBySearchable, - ImmutableSet containingItems + ImmutableSet containingItems ) { this.term = term; this.prefixNode = prefixNode; @@ -366,7 +381,7 @@ ResultCache updated(String term) { prefixedItemsBySearchable = buildPrefixedItemsBySearchable(term, prefixNode); } - final ImmutableSet containingItems; + final ImmutableSet containingItems; if (cachedTermLength == commonPrefixLength) { if (termLength > MAX_SUBSTRING_LENGTH) { containingItems = this.narrowedContainingItemsOf(term); @@ -410,18 +425,18 @@ static ImmutableMap buildPrefixedItemsBySearchable .distinct() .collect(toImmutableMap( Result::getSearchable, - result -> result.getPrefixedItemOrThrow(term).getItem() + result -> result.getPrefixedItemOrThrow(term) )); } - ImmutableSet narrowedContainingItemsOf(String term) { + ImmutableSet narrowedContainingItemsOf(String term) { return this.containingItems.stream() .map(item -> item.getOwner().findContainingItem(term)) .flatMap(Optional::stream) .collect(toImmutableSet()); } - ImmutableSet buildContaining(String term, Set excluded) { + ImmutableSet buildContaining(String term, Set excluded) { return buildContaining(term, this.getContainingPossibilities(term), excluded); } @@ -451,13 +466,13 @@ private static class Result implements Comparable { final SearchableElement searchable; final ImmutableMap aliasesByLowercase; - final Map holdersByAlias; + final Map itemsByAlias; Result(SearchableElement searchable, ImmutableMap aliasesByLowercase) { this.searchable = searchable; this.aliasesByLowercase = aliasesByLowercase; - this.holdersByAlias = new HashMap<>(aliasesByLowercase.size()); + this.itemsByAlias = new HashMap<>(aliasesByLowercase.size()); } @Override @@ -469,26 +484,26 @@ SearchableElement getSearchable() { return this.searchable; } - Optional findContainingItem(String term) { + Optional findContainingItem(String term) { return this.aliasesByLowercase.entrySet().stream() .filter(entry -> entry.getKey().contains(term)) .findFirst() .map(Map.Entry::getValue) - .map(this::getItemHolderForAlias); + .map(this::getItemForAlias); } /** - * @return the {@link ItemHolder} representing the alias prefixed with the passed {@code term} + * @return the {@link Item} representing the alias prefixed with the passed {@code term} * * @throws IllegalArgumentException if no lowercase alias starts with the passed {@code term} */ - ItemHolder getPrefixedItemOrThrow(String term) { + Item getPrefixedItemOrThrow(String term) { return this.aliasesByLowercase.entrySet() .stream() .filter(entry -> entry.getKey().startsWith(term)) .findFirst() .map(Map.Entry::getValue) - .map(this::getItemHolderForAlias) + .map(this::getItemForAlias) .orElseThrow(() -> new IllegalArgumentException( """ No lowercase alias starts with "%s"! @@ -497,109 +512,129 @@ ItemHolder getPrefixedItemOrThrow(String term) { )); } - ItemHolder getItemHolderForAlias(String alias) { - return this.holdersByAlias.computeIfAbsent(alias, ItemHolder::new); - } - - class ItemHolder implements Comparable { - final JMenuItem item; - - ItemHolder(String matchedAlias) { + Item getItemForAlias(String alias) { + return this.itemsByAlias.computeIfAbsent(alias, itemAlias -> { final String searchName = Result.this.searchable.getSearchName(); - this.item = matchedAlias.equals(searchName) - ? new JMenuItem(searchName) - : new AliasedItem(searchName, matchedAlias); + return itemAlias.equals(searchName) + ? new Item(searchName) + : new AliasedItem(searchName, itemAlias); + }); + } + + class Item extends JMenuItem implements Comparable { + Item(String searchName) { + super(searchName); - this.item.addActionListener(e -> { + this.addActionListener(e -> { Result.this.searchable.onSearchClicked(); }); } - public Component getItem() { - return this.item; - } - Result getOwner() { return Result.this; } @Override - public int compareTo(@NonNull ItemHolder other) { + public int compareTo(@NonNull Item other) { return this.getOwner().compareTo(other.getOwner()); } - static class AliasedItem extends JMenuItem { - static final int UNSET_WIDTH = -1; + void selectSearchable() { + final MenuSelectionManager manager = MenuSelectionManager.defaultManager(); + final List pathBuilder = new LinkedList<>(); + pathBuilder.add(this.getOwner().searchable); + Component element = this.getOwner().searchable.getComponent().getParent(); + while (true) { + if (element instanceof JMenu menu) { + pathBuilder.add(0, menu); + element = menu.getParent(); + } else if (element instanceof JPopupMenu popup) { + pathBuilder.add(0, popup); + element = popup.getInvoker(); + } else { + break; + } + } - final String alias; + if (element instanceof JMenuBar bar) { + pathBuilder.add(0, bar); - int aliasWidth = UNSET_WIDTH; - @Nullable - Font aliasFont; + manager.setSelectedPath(pathBuilder.toArray(pathBuilder.toArray(new MenuElement[0]))); + } + } + } - AliasedItem(String searchName, String alias) { - super(searchName); + class AliasedItem extends Item { + static final int UNSET_WIDTH = -1; - this.alias = alias; - } + final String alias; - @Override - public void setFont(Font font) { - super.setFont(font); + int aliasWidth = UNSET_WIDTH; + @Nullable + Font aliasFont; - this.aliasWidth = UNSET_WIDTH; - this.aliasFont = null; - } + AliasedItem(String searchName, String alias) { + super(searchName); - @Nullable - Font getAliasFont() { - if (this.aliasFont == null) { - final Font font = this.getFont(); - if (font != null) { - this.aliasFont = font.deriveFont(Font.ITALIC); - } - } + this.alias = alias; + } + + @Override + public void setFont(Font font) { + super.setFont(font); - return this.aliasFont; + this.aliasWidth = UNSET_WIDTH; + this.aliasFont = null; + } + + @Nullable + Font getAliasFont() { + if (this.aliasFont == null) { + final Font font = this.getFont(); + if (font != null) { + this.aliasFont = font.deriveFont(Font.ITALIC); + } } - @Override - public Dimension getPreferredSize() { - final Dimension size = super.getPreferredSize(); + return this.aliasFont; + } - size.width += this.getAliasWidth(); + @Override + public Dimension getPreferredSize() { + final Dimension size = super.getPreferredSize(); - return size; - } + size.width += this.getAliasWidth(); - @Override - public void paint(Graphics graphics) { - super.paint(graphics); + return size; + } - GuiUtil.trySetRenderingHints(graphics); - final Color color = this.getForeground(); - if (color != null) { - graphics.setColor(color); - } + @Override + public void paint(Graphics graphics) { + super.paint(graphics); - final Font aliasFont = this.getAliasFont(); - if (aliasFont != null) { - graphics.setFont(aliasFont); - } + GuiUtil.trySetRenderingHints(graphics); + final Color color = this.getForeground(); + if (color != null) { + graphics.setColor(color); + } - final Insets insets = this.getInsets(); - final int baseY = graphics.getFontMetrics().getMaxAscent() + insets.top; - graphics.drawString(this.alias, this.getWidth() - insets.right - this.getAliasWidth(), baseY); + final Font aliasFont = this.getAliasFont(); + if (aliasFont != null) { + graphics.setFont(aliasFont); } - int getAliasWidth() { - if (this.aliasWidth < 0) { - this.aliasWidth = this.getFontMetrics(this.getAliasFont()).stringWidth(this.alias); - } + final Insets insets = this.getInsets(); + final int baseY = graphics.getFontMetrics().getMaxAscent() + insets.top; + graphics.drawString(this.alias, this.getWidth() - insets.right - this.getAliasWidth(), baseY); + } - return this.aliasWidth; + int getAliasWidth() { + if (this.aliasWidth < 0) { + this.aliasWidth = this.getFontMetrics(this.getAliasFont()).stringWidth(this.alias); } + + return this.aliasWidth; } } } @@ -618,11 +653,66 @@ static Different of(Lookup.ResultCache cache) { return new Different(Stream .concat( cache.prefixedItemsBySearchable.values().stream(), - cache.containingItems.stream().map(Result.ItemHolder::getItem) + cache.containingItems.stream() ) .collect(toImmutableList()) ); } } } + + private static class SearchPathPreviewer implements AWTEventListener { + static final int PREVIEW_MODIFIER_DOWN_MASK = InputEvent.SHIFT_DOWN_MASK; + static final int PREVIEW_MODIFIER_KEY = KeyEvent.VK_SHIFT; + + static final SearchPathPreviewer INSTANCE = new SearchPathPreviewer(); + + @Nullable + RestorablePath restorablePath; + + @Override + public void eventDispatched(AWTEvent e) { + if (e instanceof KeyEvent keyEvent) { + if (keyEvent.getID() == KeyEvent.KEY_PRESSED) { + if ( + keyEvent.getKeyCode() == PREVIEW_MODIFIER_KEY + && (keyEvent.getModifiersEx() & PREVIEW_MODIFIER_DOWN_MASK) != 0 + ) { + final MenuElement[] selectedPath = MenuSelectionManager.defaultManager() + .getSelectedPath(); + + if (selectedPath.length > 0) { + final MenuElement selectedElement = selectedPath[selectedPath.length - 1]; + if (selectedElement instanceof Result.Item item) { + this.restorablePath = + new RestorablePath(item.getOwner().searchable, selectedPath); + item.selectSearchable(); + + return; + } else if ( + this.restorablePath != null + && this.restorablePath.searched == selectedElement + ) { + return; + } + } + } + + this.restorablePath = null; + } else if (keyEvent.getID() == KeyEvent.KEY_RELEASED) { + if (this.restorablePath != null) { + if (keyEvent.getKeyCode() == PREVIEW_MODIFIER_KEY) { + if (keyEvent.getModifiersEx() == 0) { + MenuSelectionManager.defaultManager().setSelectedPath(this.restorablePath.helpPath); + } + } else { + this.restorablePath = null; + } + } + } + } + } + + record RestorablePath(SearchableElement searched, MenuElement[] helpPath) { } + } } From dd60c3a4d65b20c7608a61efcb90c3ae7d13729c Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sun, 23 Nov 2025 08:11:40 -0800 Subject: [PATCH 096/124] fix SearchMenusMenu.field sometimes being unfocussable --- .../enigma/gui/element/menu_bar/SearchMenusMenu.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java index abcf20ca4..2ad192100 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java @@ -22,6 +22,7 @@ import javax.swing.JPopupMenu; import javax.swing.MenuElement; import javax.swing.MenuSelectionManager; +import javax.swing.SwingUtilities; import javax.swing.border.Border; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; @@ -36,6 +37,7 @@ import java.awt.Insets; import java.awt.Point; import java.awt.Toolkit; +import java.awt.Window; import java.awt.event.AWTEventListener; import java.awt.event.InputEvent; import java.awt.event.KeyEvent; @@ -93,6 +95,14 @@ protected SearchMenusMenu(Gui gui) { // Always focus field, but don't always select its text, because it loses focus when packing new search results. this.field.addHierarchyListener(e -> { if (this.field.isShowing()) { + final Window window = SwingUtilities.getWindowAncestor(this.field); + if (window != null && window.getType() == Window.Type.POPUP) { + // HACK: if PopupFactory::fitsOnScreen is false for light- and medium-weight popups, it makes a + // heavy-weight popup instead, whose HeavyWeightWindow component is by default is not focusable. + // It prevented this.field from focusing and receiving input. + window.setFocusableWindowState(true); + } + this.field.requestFocus(); } }); From 94bb67af6e2c95624994381be1c0bd3c7568f240 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sun, 23 Nov 2025 10:44:32 -0800 Subject: [PATCH 097/124] store ItemHolders in MultiTries instead of Results stop redundant String::contains checks --- .../gui/element/menu_bar/SearchMenusMenu.java | 420 +++++++++--------- 1 file changed, 203 insertions(+), 217 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java index 2ad192100..bdce0aea1 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java @@ -42,14 +42,11 @@ import java.awt.event.InputEvent; import java.awt.event.KeyEvent; import java.util.Arrays; -import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.Set; -import java.util.function.Function; import java.util.stream.Stream; import static com.google.common.collect.ImmutableList.toImmutableList; @@ -234,8 +231,8 @@ static int getCommonPrefixLength(String left, String right) { } static Lookup build(Gui gui) { - final CompositeStringMultiTrie prefixBuilder = CompositeStringMultiTrie.createHashed(); - final CompositeStringMultiTrie containingBuilder = + final CompositeStringMultiTrie prefixBuilder = CompositeStringMultiTrie.createHashed(); + final CompositeStringMultiTrie containingBuilder = CompositeStringMultiTrie.createHashed(); gui.getMenuBar() .streamMenus() @@ -246,29 +243,22 @@ static Lookup build(Gui gui) { } }) .forEach(element -> { - final ImmutableMap aliasesByLowercase = element.streamSearchAliases() - .filter(alias -> !alias.isEmpty()) - .collect(toImmutableMap( - String::toLowerCase, - Function.identity(), - // ignore case-insensitive duplicate aliases - (left, right) -> left - )); + final Result result = new Result(element); - final Result result = new Result(element, aliasesByLowercase); + final ImmutableMap holders = result.createHolders(); - aliasesByLowercase.keySet().forEach(alias -> { - prefixBuilder.put(alias, result); + holders.forEach((lowercaseAlias, holder) -> { + prefixBuilder.put(lowercaseAlias, holder); - final int aliasLength = alias.length(); + final int aliasLength = lowercaseAlias.length(); for (int start = NON_PREFIX_START; start < aliasLength; start++) { final int end = Math.min(start + MAX_SUBSTRING_LENGTH, aliasLength); - MutableStringMultiTrie.Node node = containingBuilder.getRoot(); + MutableStringMultiTrie.Node node = containingBuilder.getRoot(); for (int i = start; i < end; i++) { - node = node.next(alias.charAt(i)); + node = node.next(lowercaseAlias.charAt(i)); } - node.put(result); + node.put(holder); } }); }); @@ -276,18 +266,18 @@ static Lookup build(Gui gui) { return new Lookup(prefixBuilder.view(), containingBuilder.view()); } - // maps complete search aliases to their corresponding results - final StringMultiTrie prefixResults; + // maps complete search aliases to their corresponding items + final StringMultiTrie holdersByPrefix; // maps all non-prefix MAX_SUBSTRING_LENGTH-length (or less) substrings of search - // aliases to their corresponding result; used to narrow down the search scope for substring matches - final StringMultiTrie containingResults; + // aliases to their corresponding items; used to narrow down the search scope for substring matches + final StringMultiTrie holdersByContaining; @NonNull ResultCache resultCache = this.emptyCache; - Lookup(StringMultiTrie prefixResults, StringMultiTrie containingResults) { - this.prefixResults = prefixResults; - this.containingResults = containingResults; + Lookup(StringMultiTrie holdersByPrefix, StringMultiTrie holdersByContaining) { + this.holdersByPrefix = holdersByPrefix; + this.holdersByContaining = holdersByContaining; } Results search(String term) { @@ -314,28 +304,15 @@ Results search(String term) { } final class ResultCache { - static ImmutableSet buildContaining( - String term, Set possibilities, - Set excluded - ) { - return possibilities - .stream() - .filter(result -> !excluded.contains(result.searchable)) - .map(result -> result.findContainingItem(term)) - .flatMap(Optional::stream) - .sorted() - .collect(toImmutableSet()); - } - final String term; - final Node prefixNode; - final ImmutableMap prefixedItemsBySearchable; - final ImmutableSet containingItems; + final Node prefixNode; + final ImmutableMap prefixedItemsBySearchable; + final ImmutableSet containingItems; ResultCache( - String term, Node prefixNode, - ImmutableMap prefixedItemsBySearchable, - ImmutableSet containingItems + String term, Node prefixNode, + ImmutableMap prefixedItemsBySearchable, + ImmutableSet containingItems ) { this.term = term; this.prefixNode = prefixNode; @@ -367,7 +344,7 @@ ResultCache updated(String term) { return this; } else { final int backSteps = cachedTermLength - commonPrefixLength; - Node prefixNode = this.prefixNode.previous(backSteps); + Node prefixNode = this.prefixNode.previous(backSteps); // true iff this.term is a prefix of term or vice versa final boolean oneTermIsPrefix; if (termLength > commonPrefixLength) { @@ -384,28 +361,16 @@ ResultCache updated(String term) { oneTermIsPrefix = true; } - final ImmutableMap prefixedItemsBySearchable; + final ImmutableMap prefixedItemsBySearchable; if (oneTermIsPrefix && this.prefixNode.getSize() == prefixNode.getSize()) { prefixedItemsBySearchable = this.prefixedItemsBySearchable; } else { - prefixedItemsBySearchable = buildPrefixedItemsBySearchable(term, prefixNode); + prefixedItemsBySearchable = buildPrefixedItemsBySearchable(prefixNode); } - final ImmutableSet containingItems; - if (cachedTermLength == commonPrefixLength) { - if (termLength > MAX_SUBSTRING_LENGTH) { - containingItems = this.narrowedContainingItemsOf(term); - } else { - final Set containingPossibilities = this.getContainingPossibilities(term); - if (containingPossibilities.size() <= this.containingItems.size()) { - containingItems = buildContaining( - term, containingPossibilities, - prefixedItemsBySearchable.keySet() - ); - } else { - containingItems = this.narrowedContainingItemsOf(term); - } - } + final ImmutableSet containingItems; + if (cachedTermLength == commonPrefixLength && termLength > MAX_SUBSTRING_LENGTH) { + containingItems = this.narrowedContainingItemsOf(term); } else { containingItems = this.buildContaining(term, prefixedItemsBySearchable.keySet()); } @@ -416,9 +381,9 @@ ResultCache updated(String term) { } ResultCache createFresh(String term) { - final Node prefixNode = Lookup.this.prefixResults.get(term); - final ImmutableMap prefixedItemsByElement = - buildPrefixedItemsBySearchable(term, prefixNode); + final Node prefixNode = Lookup.this.holdersByPrefix.get(term); + final ImmutableMap prefixedItemsByElement = + buildPrefixedItemsBySearchable(prefixNode); return new ResultCache( term, prefixNode, prefixedItemsByElement, @@ -426,36 +391,36 @@ ResultCache createFresh(String term) { ); } - static ImmutableMap buildPrefixedItemsBySearchable( - String term, Node prefixNode + static ImmutableMap buildPrefixedItemsBySearchable( + Node prefixNode ) { return prefixNode .streamValues() .sorted() - .distinct() .collect(toImmutableMap( - Result::getSearchable, - result -> result.getPrefixedItemOrThrow(term) + Result.ItemHolder::getSearchable, + Result.ItemHolder::getItem, + // if aliases share a prefix, try keeping non-aliased item + (left, right) -> right.isSearchNamed() && !left.isSearchNamed() ? right : left )); } - ImmutableSet narrowedContainingItemsOf(String term) { + ImmutableSet narrowedContainingItemsOf(String term) { return this.containingItems.stream() - .map(item -> item.getOwner().findContainingItem(term)) - .flatMap(Optional::stream) + .filter(item -> item.getHolder().lowercaseAlias.contains(term)) .collect(toImmutableSet()); } - ImmutableSet buildContaining(String term, Set excluded) { - return buildContaining(term, this.getContainingPossibilities(term), excluded); - } - - Set getContainingPossibilities(String term) { + ImmutableSet buildContaining(String term, Set excluded) { final int termLength = term.length(); - final Set possibilities = new HashSet<>(); - for (int start = NON_PREFIX_START; start <= termLength; start++) { - final int end = Math.min(start + MAX_SUBSTRING_LENGTH, termLength); - Node node = Lookup.this.containingResults.getRoot(); + final boolean longTerm = termLength > MAX_SUBSTRING_LENGTH; + + final Set possibilities = new HashSet<>(); + final int substringLength = longTerm ? MAX_SUBSTRING_LENGTH : termLength; + final int lastSubstringStart = termLength - substringLength; + for (int start = 0; start <= lastSubstringStart; start++) { + final int end = start + substringLength; + Node node = Lookup.this.holdersByContaining.getRoot(); for (int i = start; i < end; i++) { node = node.next(term.charAt(i)); @@ -467,184 +432,205 @@ Set getContainingPossibilities(String term) { node.streamValues().forEach(possibilities::add); } - return possibilities; - } - } - } - - private static class Result implements Comparable { - final SearchableElement searchable; - - final ImmutableMap aliasesByLowercase; - final Map itemsByAlias; - - Result(SearchableElement searchable, ImmutableMap aliasesByLowercase) { - this.searchable = searchable; + Stream stream = possibilities + .stream() + .filter(holder -> !excluded.contains(holder.getSearchable())); - this.aliasesByLowercase = aliasesByLowercase; - this.itemsByAlias = new HashMap<>(aliasesByLowercase.size()); - } - - @Override - public int compareTo(@NonNull Result other) { - return this.searchable.getSearchName().compareTo(other.searchable.getSearchName()); - } + if (longTerm) { + stream = stream.filter(holder -> holder.lowercaseAlias.contains(term)); + } - SearchableElement getSearchable() { - return this.searchable; + return stream + .sorted() + .map(Result.ItemHolder::getItem) + .collect(toImmutableSet()); + } } + } - Optional findContainingItem(String term) { - return this.aliasesByLowercase.entrySet().stream() - .filter(entry -> entry.getKey().contains(term)) - .findFirst() - .map(Map.Entry::getValue) - .map(this::getItemForAlias); + /** + * A wrapper for a {@link SearchableElement}. + * + *

    Its only purpose is to link its (non-static) inner classes to the same {@link SearchableElement}. + */ + private record Result(SearchableElement searchable) { + ImmutableMap createHolders() { + final String searchName = this.searchable.getSearchName(); + return this.searchable.streamSearchAliases() + .filter(alias -> !alias.isEmpty()) + .map(alias -> Map.entry(alias.toLowerCase(), alias)) + .collect(toImmutableMap( + Map.Entry::getKey, + entry -> new ItemHolder(searchName, entry.getKey(), entry.getValue()), + // ignore case-insensitive duplicate aliases + (left, right) -> left + )); } /** - * @return the {@link Item} representing the alias prefixed with the passed {@code term} + * A holder for a {@linkplain #getItem() lazily-created} {@link Item}. * - * @throws IllegalArgumentException if no lowercase alias starts with the passed {@code term} + *

    Contains the information needed to determine whether its item should be included in results. */ - Item getPrefixedItemOrThrow(String term) { - return this.aliasesByLowercase.entrySet() - .stream() - .filter(entry -> entry.getKey().startsWith(term)) - .findFirst() - .map(Map.Entry::getValue) - .map(this::getItemForAlias) - .orElseThrow(() -> new IllegalArgumentException( - """ - No lowercase alias starts with "%s"! - \tlowercase aliases: %s\ - """.formatted(term, this.aliasesByLowercase.keySet()) - )); - } + class ItemHolder implements Comparable { + private final String searchName; + private final String alias; + final String lowercaseAlias; - Item getItemForAlias(String alias) { - return this.itemsByAlias.computeIfAbsent(alias, itemAlias -> { - final String searchName = Result.this.searchable.getSearchName(); + @Nullable + Item item; - return itemAlias.equals(searchName) - ? new Item(searchName) - : new AliasedItem(searchName, itemAlias); - }); - } + ItemHolder(String searchName, String lowercaseAlias, String alias) { + this.searchName = searchName; + this.lowercaseAlias = lowercaseAlias; + this.alias = alias; + } - class Item extends JMenuItem implements Comparable { - Item(String searchName) { - super(searchName); + Item getItem() { + if (this.item == null) { + this.item = this.alias.equals(this.searchName) + ? new Item(this.searchName) + : new AliasedItem(this.searchName, this.alias); + } - this.addActionListener(e -> { - Result.this.searchable.onSearchClicked(); - }); + return this.item; } - Result getOwner() { + SearchableElement getSearchable() { + return Result.this.searchable(); + } + + Result getResult() { return Result.this; } @Override - public int compareTo(@NonNull Item other) { - return this.getOwner().compareTo(other.getOwner()); + public int compareTo(@NonNull ItemHolder other) { + return this.getSearchable().getSearchName().compareTo(other.getSearchable().getSearchName()); } - void selectSearchable() { - final MenuSelectionManager manager = MenuSelectionManager.defaultManager(); - final List pathBuilder = new LinkedList<>(); - pathBuilder.add(this.getOwner().searchable); - Component element = this.getOwner().searchable.getComponent().getParent(); - while (true) { - if (element instanceof JMenu menu) { - pathBuilder.add(0, menu); - element = menu.getParent(); - } else if (element instanceof JPopupMenu popup) { - pathBuilder.add(0, popup); - element = popup.getInvoker(); - } else { - break; - } + class Item extends JMenuItem { + Item(String searchName) { + super(searchName); + + this.addActionListener(e -> { + Result.this.searchable.onSearchClicked(); + }); } - if (element instanceof JMenuBar bar) { - pathBuilder.add(0, bar); + boolean isSearchNamed() { + return true; + } - manager.setSelectedPath(pathBuilder.toArray(pathBuilder.toArray(new MenuElement[0]))); + ItemHolder getHolder() { + return ItemHolder.this; + } + + void selectSearchable() { + final MenuSelectionManager manager = MenuSelectionManager.defaultManager(); + final List pathBuilder = new LinkedList<>(); + pathBuilder.add(this.getSearchable()); + Component element = this.getSearchable().getComponent().getParent(); + while (true) { + if (element instanceof JMenu menu) { + pathBuilder.add(0, menu); + element = menu.getParent(); + } else if (element instanceof JPopupMenu popup) { + pathBuilder.add(0, popup); + element = popup.getInvoker(); + } else { + break; + } + } + + if (element instanceof JMenuBar bar) { + pathBuilder.add(0, bar); + + manager.setSelectedPath(pathBuilder.toArray(pathBuilder.toArray(new MenuElement[0]))); + } + } + + private SearchableElement getSearchable() { + return this.getHolder().getResult().searchable; } } - } - class AliasedItem extends Item { - static final int UNSET_WIDTH = -1; + class AliasedItem extends Item { + static final int UNSET_WIDTH = -1; - final String alias; + final String alias; - int aliasWidth = UNSET_WIDTH; - @Nullable - Font aliasFont; + int aliasWidth = UNSET_WIDTH; + @Nullable + Font aliasFont; - AliasedItem(String searchName, String alias) { - super(searchName); + AliasedItem(String searchName, String alias) { + super(searchName); - this.alias = alias; - } + this.alias = alias; + } - @Override - public void setFont(Font font) { - super.setFont(font); + @Override + boolean isSearchNamed() { + return false; + } - this.aliasWidth = UNSET_WIDTH; - this.aliasFont = null; - } + @Override + public void setFont(Font font) { + super.setFont(font); - @Nullable - Font getAliasFont() { - if (this.aliasFont == null) { - final Font font = this.getFont(); - if (font != null) { - this.aliasFont = font.deriveFont(Font.ITALIC); + this.aliasWidth = UNSET_WIDTH; + this.aliasFont = null; + } + + @Nullable + Font getAliasFont() { + if (this.aliasFont == null) { + final Font font = this.getFont(); + if (font != null) { + this.aliasFont = font.deriveFont(Font.ITALIC); + } } + + return this.aliasFont; } - return this.aliasFont; - } + @Override + public Dimension getPreferredSize() { + final Dimension size = super.getPreferredSize(); - @Override - public Dimension getPreferredSize() { - final Dimension size = super.getPreferredSize(); + size.width += this.getAliasWidth(); - size.width += this.getAliasWidth(); + return size; + } - return size; - } + @Override + public void paint(Graphics graphics) { + super.paint(graphics); - @Override - public void paint(Graphics graphics) { - super.paint(graphics); + GuiUtil.trySetRenderingHints(graphics); + final Color color = this.getForeground(); + if (color != null) { + graphics.setColor(color); + } - GuiUtil.trySetRenderingHints(graphics); - final Color color = this.getForeground(); - if (color != null) { - graphics.setColor(color); - } + final Font aliasFont = this.getAliasFont(); + if (aliasFont != null) { + graphics.setFont(aliasFont); + } - final Font aliasFont = this.getAliasFont(); - if (aliasFont != null) { - graphics.setFont(aliasFont); + final Insets insets = this.getInsets(); + final int baseY = graphics.getFontMetrics().getMaxAscent() + insets.top; + graphics.drawString(this.alias, this.getWidth() - insets.right - this.getAliasWidth(), baseY); } - final Insets insets = this.getInsets(); - final int baseY = graphics.getFontMetrics().getMaxAscent() + insets.top; - graphics.drawString(this.alias, this.getWidth() - insets.right - this.getAliasWidth(), baseY); - } + int getAliasWidth() { + if (this.aliasWidth < 0) { + this.aliasWidth = this.getFontMetrics(this.getAliasFont()).stringWidth(this.alias); + } - int getAliasWidth() { - if (this.aliasWidth < 0) { - this.aliasWidth = this.getFontMetrics(this.getAliasFont()).stringWidth(this.alias); + return this.aliasWidth; } - - return this.aliasWidth; } } } @@ -693,9 +679,9 @@ public void eventDispatched(AWTEvent e) { if (selectedPath.length > 0) { final MenuElement selectedElement = selectedPath[selectedPath.length - 1]; - if (selectedElement instanceof Result.Item item) { + if (selectedElement instanceof Result.ItemHolder.Item item) { this.restorablePath = - new RestorablePath(item.getOwner().searchable, selectedPath); + new RestorablePath(item.getSearchable(), selectedPath); item.selectSearchable(); return; From cc4aa8ea676b4851bc748e4ca64ca7d94ba1b1b0 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sun, 23 Nov 2025 12:31:30 -0800 Subject: [PATCH 098/124] replace useages of AbstractButton#doClick() with AbstractButton#doClick(0) to prevent sleeping rename SearchableElement's onSearchClicked -> onSearchChosen allow pressing enter when search item or preview is selected to do onSearchChosen --- .../gui/dialog/EnigmaQuickFindToolBar.java | 4 +- .../AbstractSearchableEnigmaMenu.java | 4 +- .../gui/element/menu_bar/CollabMenu.java | 4 +- .../gui/element/menu_bar/DecompilerMenu.java | 4 +- .../gui/element/menu_bar/SearchMenusMenu.java | 71 +++++++++++++------ .../element/menu_bar/SearchableElement.java | 2 +- .../element/menu_bar/SimpleCheckBoxItem.java | 4 +- .../gui/element/menu_bar/SimpleItem.java | 4 +- .../gui/element/menu_bar/SimpleRadioItem.java | 4 +- .../element/menu_bar/view/LanguagesMenu.java | 4 +- .../gui/element/menu_bar/view/ThemesMenu.java | 4 +- .../quiltmc/enigma/gui/panel/EditorPanel.java | 3 +- 12 files changed, 69 insertions(+), 43 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/dialog/EnigmaQuickFindToolBar.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/dialog/EnigmaQuickFindToolBar.java index f94872929..ea9139e9b 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/dialog/EnigmaQuickFindToolBar.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/dialog/EnigmaQuickFindToolBar.java @@ -93,8 +93,8 @@ public void translate() { } public void reloadKeyBinds() { - putKeyBindAction(KeyBinds.QUICK_FIND_DIALOG_PREVIOUS, this.searchField, e -> this.prevButton.doClick()); - putKeyBindAction(KeyBinds.QUICK_FIND_DIALOG_NEXT, this.searchField, e -> this.nextButton.doClick()); + putKeyBindAction(KeyBinds.QUICK_FIND_DIALOG_PREVIOUS, this.searchField, e -> this.prevButton.doClick(0)); + putKeyBindAction(KeyBinds.QUICK_FIND_DIALOG_NEXT, this.searchField, e -> this.nextButton.doClick(0)); putKeyBindAction( KeyBinds.QUICK_FIND_DIALOG_CLOSE, this, FocusCondition.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT, e -> this.setVisible(false) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/AbstractSearchableEnigmaMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/AbstractSearchableEnigmaMenu.java index bae8cd3d0..9b7090385 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/AbstractSearchableEnigmaMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/AbstractSearchableEnigmaMenu.java @@ -13,7 +13,7 @@ public String getSearchName() { } @Override - public void onSearchClicked() { - this.doClick(); + public void onSearchChosen() { + this.doClick(0); } } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/CollabMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/CollabMenu.java index 327661285..081f03d31 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/CollabMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/CollabMenu.java @@ -150,8 +150,8 @@ public String getSearchName() { } @Override - public void onSearchClicked() { - this.doClick(); + public void onSearchChosen() { + this.doClick(0); } } } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/DecompilerMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/DecompilerMenu.java index 2bfb7292e..19d1ecb3b 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/DecompilerMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/DecompilerMenu.java @@ -68,8 +68,8 @@ public String getSearchName() { } @Override - public void onSearchClicked() { - this.doClick(); + public void onSearchChosen() { + this.doClick(0); } } } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java index bdce0aea1..2ff5c47d1 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java @@ -69,6 +69,15 @@ private static Stream streamElementTree(MenuElement root) { ); } + private static void clearSelectionAndChoose(SearchableElement searchable, MenuSelectionManager manager) { + // clearing the path ensures: + // - the help menu doesn't stay selected if onSearchChosen doesn't set the path + // - the current path doesn't interfere with onSearchChosen implementations that set the + // path based on the current path + manager.clearSelectedPath(); + searchable.onSearchChosen(); + } + private final PlaceheldTextField field = new PlaceheldTextField(); private final JMenuItem noResults = new JMenuItem(); @@ -80,7 +89,7 @@ protected SearchMenusMenu(Gui gui) { // global listener because menu/item key listeners didn't fire // also more reliably clears restorablePath - Toolkit.getDefaultToolkit().addAWTEventListener(SearchPathPreviewer.INSTANCE, AWTEvent.KEY_EVENT_MASK); + Toolkit.getDefaultToolkit().addAWTEventListener(KeyHandler.INSTANCE, AWTEvent.KEY_EVENT_MASK); this.defaultPopupBorder = this.getPopupMenu().getBorder(); @@ -136,8 +145,6 @@ public void changedUpdate(DocumentEvent e) { } }); - // TODO KeyBinds: enter -> doClick on selected result - this.retranslate(); } @@ -514,7 +521,7 @@ class Item extends JMenuItem { super(searchName); this.addActionListener(e -> { - Result.this.searchable.onSearchClicked(); + clearSelectionAndChoose(Result.this.searchable, MenuSelectionManager.defaultManager()); }); } @@ -657,11 +664,20 @@ static Different of(Lookup.ResultCache cache) { } } - private static class SearchPathPreviewer implements AWTEventListener { - static final int PREVIEW_MODIFIER_DOWN_MASK = InputEvent.SHIFT_DOWN_MASK; + private static class KeyHandler implements AWTEventListener { + static final int PREVIEW_MODIFIER_MASK = InputEvent.SHIFT_DOWN_MASK; static final int PREVIEW_MODIFIER_KEY = KeyEvent.VK_SHIFT; - static final SearchPathPreviewer INSTANCE = new SearchPathPreviewer(); + @Nullable + static T getLastOrNull(T[] array) { + if (array.length > 0) { + return array[array.length - 1]; + } else { + return null; + } + } + + static final KeyHandler INSTANCE = new KeyHandler(); @Nullable RestorablePath restorablePath; @@ -670,28 +686,37 @@ private static class SearchPathPreviewer implements AWTEventListener { public void eventDispatched(AWTEvent e) { if (e instanceof KeyEvent keyEvent) { if (keyEvent.getID() == KeyEvent.KEY_PRESSED) { - if ( - keyEvent.getKeyCode() == PREVIEW_MODIFIER_KEY - && (keyEvent.getModifiersEx() & PREVIEW_MODIFIER_DOWN_MASK) != 0 - ) { - final MenuElement[] selectedPath = MenuSelectionManager.defaultManager() - .getSelectedPath(); - - if (selectedPath.length > 0) { - final MenuElement selectedElement = selectedPath[selectedPath.length - 1]; - if (selectedElement instanceof Result.ItemHolder.Item item) { - this.restorablePath = - new RestorablePath(item.getSearchable(), selectedPath); + final int keyCode = keyEvent.getKeyCode(); + if (keyCode == PREVIEW_MODIFIER_KEY && keyEvent.getModifiersEx() == PREVIEW_MODIFIER_MASK) { + final MenuElement[] selectedPath = MenuSelectionManager.defaultManager().getSelectedPath(); + + final MenuElement selected = getLastOrNull(selectedPath); + if (selected != null) { + if (selected instanceof Result.ItemHolder.Item item) { + this.restorablePath = new RestorablePath(item.getSearchable(), selectedPath); + item.selectSearchable(); return; - } else if ( - this.restorablePath != null - && this.restorablePath.searched == selectedElement - ) { + } else if (this.restorablePath != null && this.restorablePath.searched == selected) { return; } } + } else if (keyCode == KeyEvent.VK_ENTER) { + final int modifiers = keyEvent.getModifiersEx(); + if (modifiers == 0) { + final MenuSelectionManager manager = MenuSelectionManager.defaultManager(); + if (getLastOrNull(manager.getSelectedPath()) instanceof Result.ItemHolder.Item item) { + clearSelectionAndChoose(item.getSearchable(), manager); + } + } else if (modifiers == PREVIEW_MODIFIER_MASK) { + if (this.restorablePath != null) { + final MenuSelectionManager manager = MenuSelectionManager.defaultManager(); + if (this.restorablePath.searched == getLastOrNull(manager.getSelectedPath())) { + clearSelectionAndChoose(this.restorablePath.searched, manager); + } + } + } } this.restorablePath = null; diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchableElement.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchableElement.java index 93ce73968..396d2f6f8 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchableElement.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchableElement.java @@ -19,5 +19,5 @@ static Stream translateExtraAliases(String translationKey) { String getSearchName(); - void onSearchClicked(); + void onSearchChosen(); } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SimpleCheckBoxItem.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SimpleCheckBoxItem.java index ea2b81293..fe92c3f5f 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SimpleCheckBoxItem.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SimpleCheckBoxItem.java @@ -27,7 +27,7 @@ public String getAliasesTranslationKeyPrefix() { } @Override - public void onSearchClicked() { - this.doClick(); + public void onSearchChosen() { + this.doClick(0); } } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SimpleItem.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SimpleItem.java index 018838841..1409adfbf 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SimpleItem.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SimpleItem.java @@ -27,7 +27,7 @@ public String getAliasesTranslationKeyPrefix() { } @Override - public void onSearchClicked() { - this.doClick(); + public void onSearchChosen() { + this.doClick(0); } } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SimpleRadioItem.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SimpleRadioItem.java index b527843f9..66a0d4d4d 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SimpleRadioItem.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SimpleRadioItem.java @@ -22,8 +22,8 @@ public String getAliasesTranslationKeyPrefix() { } @Override - public void onSearchClicked() { - this.doClick(); + public void onSearchChosen() { + this.doClick(0); } @Override diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/LanguagesMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/LanguagesMenu.java index 192b01814..fc56e13d2 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/LanguagesMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/LanguagesMenu.java @@ -83,8 +83,8 @@ public String getAliasesTranslationKeyPrefix() { } @Override - public void onSearchClicked() { - this.doClick(); + public void onSearchChosen() { + this.doClick(0); } } } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/ThemesMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/ThemesMenu.java index 9a2d23f64..b632921c8 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/ThemesMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/ThemesMenu.java @@ -87,8 +87,8 @@ public String getAliasesTranslationKeyPrefix() { } @Override - public void onSearchClicked() { - this.doClick(); + public void onSearchChosen() { + this.doClick(0); } } } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java index 10d3e993f..aa8f435ec 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java @@ -290,7 +290,8 @@ public void actionPerformed(JTextComponent target, SyntaxDocument sDoc, int dot, }); this.quickFindToolBar.reloadKeyBinds(); - this.popupMenu.getButtonKeyBinds().forEach((key, button) -> putKeyBindAction(key, this.editor, e -> button.doClick())); + this.popupMenu.getButtonKeyBinds() + .forEach((key, button) -> putKeyBindAction(key, this.editor, e -> button.doClick(0))); } private class TooltipManager { From ebea542a2c723a44154c8d49b551ba69525ea5cf Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sun, 23 Nov 2025 12:51:37 -0800 Subject: [PATCH 099/124] add separator between prefix and containing result items --- .../enigma/gui/element/menu_bar/SearchMenusMenu.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java index 2ff5c47d1..4a67ed068 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java @@ -3,6 +3,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Streams; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; import org.quiltmc.enigma.gui.ConnectionState; @@ -653,9 +654,15 @@ final class Same implements Results { record Different(ImmutableList results) implements Results { static Different of(Lookup.ResultCache cache) { - return new Different(Stream + final Stream separator = + !cache.prefixedItemsBySearchable.isEmpty() && !cache.containingItems.isEmpty() + ? Stream.of(new JPopupMenu.Separator()) + : Stream.empty(); + + return new Different(Streams .concat( cache.prefixedItemsBySearchable.values().stream(), + separator, cache.containingItems.stream() ) .collect(toImmutableList()) From f79dbc3943a18f4924903aa44725b989f6aa80a0 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sun, 23 Nov 2025 13:57:31 -0800 Subject: [PATCH 100/124] add non-functional hints to SearchMenusMenu --- .../gui/element/menu_bar/SearchMenusMenu.java | 57 +++++++++++++++++++ enigma/src/main/resources/lang/en_us.json | 2 + 2 files changed, 59 insertions(+) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java index 4a67ed068..b509af015 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java @@ -9,6 +9,7 @@ import org.quiltmc.enigma.gui.ConnectionState; import org.quiltmc.enigma.gui.Gui; import org.quiltmc.enigma.gui.element.PlaceheldTextField; +import org.quiltmc.enigma.gui.util.GridBagConstraintsBuilder; import org.quiltmc.enigma.gui.util.GuiUtil; import org.quiltmc.enigma.util.I18n; import org.quiltmc.enigma.util.multi_trie.CompositeStringMultiTrie; @@ -17,9 +18,13 @@ import org.quiltmc.enigma.util.multi_trie.StringMultiTrie; import org.quiltmc.enigma.util.multi_trie.StringMultiTrie.Node; +import javax.swing.Box; +import javax.swing.JButton; +import javax.swing.JLabel; import javax.swing.JMenu; import javax.swing.JMenuBar; import javax.swing.JMenuItem; +import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.MenuElement; import javax.swing.MenuSelectionManager; @@ -35,6 +40,8 @@ import java.awt.Dimension; import java.awt.Font; import java.awt.Graphics; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.Point; import java.awt.Toolkit; @@ -53,6 +60,7 @@ import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.collect.ImmutableMap.toImmutableMap; import static com.google.common.collect.ImmutableSet.toImmutableSet; +import static javax.swing.BorderFactory.createEmptyBorder; public class SearchMenusMenu extends AbstractEnigmaMenu { @Nullable @@ -81,6 +89,8 @@ private static void clearSelectionAndChoose(SearchableElement searchable, MenuSe private final PlaceheldTextField field = new PlaceheldTextField(); private final JMenuItem noResults = new JMenuItem(); + private final HintItem previewHint = new HintItem("menu.help.search.hint.preview"); + private final HintItem executeHint = new HintItem("menu.help.search.hint.execute"); @Nullable private Lookup lookup; @@ -184,6 +194,8 @@ private void refreshPopup() { private void addPermanentChildren() { this.add(this.field); this.add(this.noResults); + this.add(this.previewHint); + this.add(this.executeHint); } private void keepOnlyPermanentChildren() { @@ -743,4 +755,49 @@ public void eventDispatched(AWTEvent e) { record RestorablePath(SearchableElement searched, MenuElement[] helpPath) { } } + + // not a MenuElement so it can't be selected + private static class HintItem extends JPanel implements Retranslatable { + final String translationKey; + + final JLabel infoIndicator = new JLabel("ⓘ"); + final JLabel hint = new JLabel(); + final JButton dismiss = new JButton("⊗"); + + HintItem(String translationKey) { + this.translationKey = translationKey; + + this.setBorder(createEmptyBorder(0, 2, 0, 0)); + + this.setLayout(new GridBagLayout()); + + this.add(this.infoIndicator); + + final var spacer = Box.createHorizontalBox(); + spacer.setPreferredSize(new Dimension(3, 1)); + this.add(spacer); + + final Font oldHintFont = this.hint.getFont(); + this.hint.setFont(oldHintFont.deriveFont(Font.ITALIC, oldHintFont.getSize2D() * 0.85f)); + this.add(this.hint, GridBagConstraintsBuilder.create() + .weightX(1) + .fill(GridBagConstraints.HORIZONTAL) + .build() + ); + + this.dismiss.setBorderPainted(false); + this.dismiss.setBackground(new Color(0, true)); + this.dismiss.setMargin(new Insets(0, 0, 0, 0)); + final Font oldDismissFont = this.dismiss.getFont(); + this.dismiss.setFont(oldDismissFont.deriveFont(oldDismissFont.getSize2D() * 1.5f)); + this.add(this.dismiss); + + this.retranslate(); + } + + @Override + public void retranslate() { + this.hint.setText(I18n.translate(this.translationKey)); + } + } } diff --git a/enigma/src/main/resources/lang/en_us.json b/enigma/src/main/resources/lang/en_us.json index 33876b666..58d4239a1 100644 --- a/enigma/src/main/resources/lang/en_us.json +++ b/enigma/src/main/resources/lang/en_us.json @@ -122,6 +122,8 @@ "menu.help.search": "Search menus", "menu.help.search.placeholder": "Search menus...", "menu.help.search.no_results": "No results", + "menu.help.search.hint.preview": "Hold shift to preview the selected result", + "menu.help.search.hint.execute": "Press enter to execute the selected result", "popup_menu.rename": "Rename", "popup_menu.paste": "Paste text", From c22b3f452426b5754048f9c94c65956a8085212f Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sun, 23 Nov 2025 14:27:14 -0800 Subject: [PATCH 101/124] allow permanently dismissing hints via button click --- .../org/quiltmc/enigma/gui/config/Config.java | 3 ++ .../enigma/gui/config/SearchMenusSection.java | 16 ++++++++++ .../gui/element/menu_bar/SearchMenusMenu.java | 30 ++++++++++++++++--- 3 files changed, 45 insertions(+), 4 deletions(-) create mode 100644 enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/SearchMenusSection.java diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/Config.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/Config.java index dbb64fbc3..e38aebd9b 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/Config.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/Config.java @@ -81,6 +81,9 @@ public final class Config extends ReflectiveConfig { @Comment("The settings for the statistics window.") public final StatsSection stats = new StatsSection(); + @Comment("Settings for the search menus menu.") + public final SearchMenusSection searchMenus = new SearchMenusSection(); + @Comment("You shouldn't enable options in this section unless you know what you're doing!") public final DevSection development = new DevSection(); diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/SearchMenusSection.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/SearchMenusSection.java new file mode 100644 index 000000000..e01b10fb5 --- /dev/null +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/SearchMenusSection.java @@ -0,0 +1,16 @@ +package org.quiltmc.enigma.gui.config; + +import org.quiltmc.config.api.ReflectiveConfig; +import org.quiltmc.config.api.annotations.Comment; +import org.quiltmc.config.api.annotations.SerializedNameConvention; +import org.quiltmc.config.api.metadata.NamingSchemes; +import org.quiltmc.config.api.values.TrackedValue; + +@SerializedNameConvention(NamingSchemes.SNAKE_CASE) +public class SearchMenusSection extends ReflectiveConfig.Section { + @Comment("Whether to show the search menus preview hint until it's dismissed.") + public final TrackedValue showPreviewHint = this.value(true); + + @Comment("Whether to show the search menus execute hint until it's dismissed.") + public final TrackedValue showExecuteHint = this.value(true); +} diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java index b509af015..a2e375f39 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java @@ -6,8 +6,10 @@ import com.google.common.collect.Streams; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; +import org.quiltmc.config.api.values.TrackedValue; import org.quiltmc.enigma.gui.ConnectionState; import org.quiltmc.enigma.gui.Gui; +import org.quiltmc.enigma.gui.config.Config; import org.quiltmc.enigma.gui.element.PlaceheldTextField; import org.quiltmc.enigma.gui.util.GridBagConstraintsBuilder; import org.quiltmc.enigma.gui.util.GuiUtil; @@ -89,8 +91,15 @@ private static void clearSelectionAndChoose(SearchableElement searchable, MenuSe private final PlaceheldTextField field = new PlaceheldTextField(); private final JMenuItem noResults = new JMenuItem(); - private final HintItem previewHint = new HintItem("menu.help.search.hint.preview"); - private final HintItem executeHint = new HintItem("menu.help.search.hint.execute"); + + private final HintItem previewHint = new HintItem( + "menu.help.search.hint.preview", + Config.main().searchMenus.showPreviewHint + ); + private final HintItem executeHint = new HintItem( + "menu.help.search.hint.execute", + Config.main().searchMenus.showExecuteHint + ); @Nullable private Lookup lookup; @@ -107,6 +116,9 @@ protected SearchMenusMenu(Gui gui) { this.noResults.setEnabled(false); this.noResults.setVisible(false); + this.previewHint.setVisible(false); + this.executeHint.setVisible(false); + this.addPermanentChildren(); // Always focus field, but don't always select its text, because it loses focus when packing new search results. @@ -167,6 +179,9 @@ private void updateResultItems() { if (results instanceof Results.None) { this.keepOnlyPermanentChildren(); + this.previewHint.setVisible(false); + this.executeHint.setVisible(false); + this.noResults.setVisible(!searchTerm.isEmpty()); this.refreshPopup(); @@ -174,6 +189,8 @@ private void updateResultItems() { this.keepOnlyPermanentChildren(); this.noResults.setVisible(different.results.isEmpty()); + this.previewHint.setVisible(Config.main().searchMenus.showPreviewHint.value()); + this.executeHint.setVisible(Config.main().searchMenus.showExecuteHint.value()); different.results.forEach(this::add); @@ -757,14 +774,14 @@ record RestorablePath(SearchableElement searched, MenuElement[] helpPath) { } } // not a MenuElement so it can't be selected - private static class HintItem extends JPanel implements Retranslatable { + private class HintItem extends JPanel implements Retranslatable { final String translationKey; final JLabel infoIndicator = new JLabel("ⓘ"); final JLabel hint = new JLabel(); final JButton dismiss = new JButton("⊗"); - HintItem(String translationKey) { + HintItem(String translationKey, TrackedValue config) { this.translationKey = translationKey; this.setBorder(createEmptyBorder(0, 2, 0, 0)); @@ -790,6 +807,11 @@ private static class HintItem extends JPanel implements Retranslatable { this.dismiss.setMargin(new Insets(0, 0, 0, 0)); final Font oldDismissFont = this.dismiss.getFont(); this.dismiss.setFont(oldDismissFont.deriveFont(oldDismissFont.getSize2D() * 1.5f)); + this.dismiss.addActionListener(e -> { + config.setValue(false); + this.setVisible(false); + SearchMenusMenu.this.refreshPopup(); + }); this.add(this.dismiss); this.retranslate(); From 42ab8516fdb7361524998dd2c9d17556727fe255 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sun, 23 Nov 2025 14:38:40 -0800 Subject: [PATCH 102/124] dismiss hints when used --- .../gui/element/menu_bar/SearchMenusMenu.java | 61 +++++++++++-------- 1 file changed, 36 insertions(+), 25 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java index a2e375f39..5e53c3b5d 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java @@ -109,7 +109,7 @@ protected SearchMenusMenu(Gui gui) { // global listener because menu/item key listeners didn't fire // also more reliably clears restorablePath - Toolkit.getDefaultToolkit().addAWTEventListener(KeyHandler.INSTANCE, AWTEvent.KEY_EVENT_MASK); + Toolkit.getDefaultToolkit().addAWTEventListener(new KeyHandler(), AWTEvent.KEY_EVENT_MASK); this.defaultPopupBorder = this.getPopupMenu().getBorder(); @@ -199,13 +199,15 @@ private void updateResultItems() { } private void refreshPopup() { - // HACK: When popups are resizing in limited space, they may remove their borders. - // The border won't be restored when re-packing or showing, so manually restore the original border here. - this.getPopupMenu().setBorder(this.defaultPopupBorder); - this.getPopupMenu().pack(); - - final Point popupMenuOrigin = this.getPopupMenuOrigin(); - this.getPopupMenu().show(this, popupMenuOrigin.x, popupMenuOrigin.y); + if (this.isShowing()) { + // HACK: When popups are resizing in limited space, they may remove their borders. + // The border won't be restored when re-packing or showing, so manually restore the original border here. + this.getPopupMenu().setBorder(this.defaultPopupBorder); + this.getPopupMenu().pack(); + + final Point popupMenuOrigin = this.getPopupMenuOrigin(); + this.getPopupMenu().show(this, popupMenuOrigin.x, popupMenuOrigin.y); + } } private void addPermanentChildren() { @@ -700,7 +702,7 @@ static Different of(Lookup.ResultCache cache) { } } - private static class KeyHandler implements AWTEventListener { + private class KeyHandler implements AWTEventListener { static final int PREVIEW_MODIFIER_MASK = InputEvent.SHIFT_DOWN_MASK; static final int PREVIEW_MODIFIER_KEY = KeyEvent.VK_SHIFT; @@ -713,8 +715,6 @@ static T getLastOrNull(T[] array) { } } - static final KeyHandler INSTANCE = new KeyHandler(); - @Nullable RestorablePath restorablePath; @@ -729,6 +729,8 @@ public void eventDispatched(AWTEvent e) { final MenuElement selected = getLastOrNull(selectedPath); if (selected != null) { if (selected instanceof Result.ItemHolder.Item item) { + SearchMenusMenu.this.previewHint.dismiss(); + this.restorablePath = new RestorablePath(item.getSearchable(), selectedPath); item.selectSearchable(); @@ -743,13 +745,13 @@ public void eventDispatched(AWTEvent e) { if (modifiers == 0) { final MenuSelectionManager manager = MenuSelectionManager.defaultManager(); if (getLastOrNull(manager.getSelectedPath()) instanceof Result.ItemHolder.Item item) { - clearSelectionAndChoose(item.getSearchable(), manager); + this.execute(item.getSearchable(), manager); } } else if (modifiers == PREVIEW_MODIFIER_MASK) { if (this.restorablePath != null) { final MenuSelectionManager manager = MenuSelectionManager.defaultManager(); if (this.restorablePath.searched == getLastOrNull(manager.getSelectedPath())) { - clearSelectionAndChoose(this.restorablePath.searched, manager); + this.execute(this.restorablePath.searched, manager); } } } @@ -770,19 +772,26 @@ public void eventDispatched(AWTEvent e) { } } + void execute(SearchableElement searchable, MenuSelectionManager manager) { + SearchMenusMenu.this.executeHint.dismiss(); + clearSelectionAndChoose(searchable, manager); + } + record RestorablePath(SearchableElement searched, MenuElement[] helpPath) { } } // not a MenuElement so it can't be selected private class HintItem extends JPanel implements Retranslatable { final String translationKey; + final TrackedValue config; final JLabel infoIndicator = new JLabel("ⓘ"); final JLabel hint = new JLabel(); - final JButton dismiss = new JButton("⊗"); + final JButton dismissButton = new JButton("⊗"); HintItem(String translationKey, TrackedValue config) { this.translationKey = translationKey; + this.config = config; this.setBorder(createEmptyBorder(0, 2, 0, 0)); @@ -802,21 +811,23 @@ private class HintItem extends JPanel implements Retranslatable { .build() ); - this.dismiss.setBorderPainted(false); - this.dismiss.setBackground(new Color(0, true)); - this.dismiss.setMargin(new Insets(0, 0, 0, 0)); - final Font oldDismissFont = this.dismiss.getFont(); - this.dismiss.setFont(oldDismissFont.deriveFont(oldDismissFont.getSize2D() * 1.5f)); - this.dismiss.addActionListener(e -> { - config.setValue(false); - this.setVisible(false); - SearchMenusMenu.this.refreshPopup(); - }); - this.add(this.dismiss); + this.dismissButton.setBorderPainted(false); + this.dismissButton.setBackground(new Color(0, true)); + this.dismissButton.setMargin(new Insets(0, 0, 0, 0)); + final Font oldDismissFont = this.dismissButton.getFont(); + this.dismissButton.setFont(oldDismissFont.deriveFont(oldDismissFont.getSize2D() * 1.5f)); + this.dismissButton.addActionListener(e -> this.dismiss()); + this.add(this.dismissButton); this.retranslate(); } + private void dismiss() { + this.config.setValue(false); + this.setVisible(false); + SearchMenusMenu.this.refreshPopup(); + } + @Override public void retranslate() { this.hint.setText(I18n.translate(this.translationKey)); From cba2630131bd06f8bc23c6e9f27b54102e8b4e1d Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sun, 23 Nov 2025 19:35:07 -0800 Subject: [PATCH 103/124] remove CompositeBiMap :[ --- .../quiltmc/enigma/util/CompositeBiMap.java | 289 ------------------ 1 file changed, 289 deletions(-) delete mode 100644 enigma/src/main/java/org/quiltmc/enigma/util/CompositeBiMap.java diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/CompositeBiMap.java b/enigma/src/main/java/org/quiltmc/enigma/util/CompositeBiMap.java deleted file mode 100644 index 53ee23ad5..000000000 --- a/enigma/src/main/java/org/quiltmc/enigma/util/CompositeBiMap.java +++ /dev/null @@ -1,289 +0,0 @@ -package org.quiltmc.enigma.util; - -import com.google.common.collect.BiMap; -import com.google.common.collect.MapMaker; -import org.jspecify.annotations.NonNull; - -import java.util.Collection; -import java.util.Iterator; -import java.util.Map; -import java.util.Set; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.function.Supplier; - -public class CompositeBiMap implements BiMap { - public static BiMap ofWeakValues() { - return of(new MapMaker().weakValues().makeMap(), new MapMaker().weakKeys().makeMap()); - } - - public static BiMap of(Map forward, Map reverse) { - return new CompositeBiMap<>(forward, reverse); - } - - private final Map forward; - private final Map reverse; - - CompositeBiMap inverse; - - private CompositeBiMap(Map forward, Map reverse) { - this.forward = forward; - this.reverse = reverse; - } - - @Override - public int size() { - return this.forward.size(); - } - - @Override - public boolean isEmpty() { - return this.forward.isEmpty(); - } - - @Override - public boolean containsKey(Object key) { - return this.forward.containsKey(key); - } - - @SuppressWarnings("SuspiciousMethodCalls") - @Override - public boolean containsValue(Object value) { - return this.reverse.containsKey(value); - } - - @Override - public V get(Object key) { - return this.forward.get(key); - } - - /** - * @throws IllegalArgumentException see {@link BiMap#put(Object, Object)} - * - * @see #put(Object, Object) - */ - @Override - public V put(K key, V value) { - if (this.containsValue(value)) { - throw new IllegalArgumentException( - "Tried to put duplicate value %s, already associated with key %s!" - .formatted(value, this.reverse.get(value)) - ); - } else { - this.reverse.put(value, key); - return this.forward.put(key, value); - } - } - - @Override - public V remove(Object key) { - final V removed = this.forward.remove(key); - if (removed == null) { - return null; - } else { - this.reverse.remove(removed); - return removed; - } - } - - @Override - public V forcePut(K key, V value) { - this.reverse.remove(value); - this.reverse.put(value, key); - return this.forward.put(key, value); - } - - @Override - public void putAll(Map map) { - map.forEach(this::put); - } - - @Override - public void clear() { - this.forward.clear(); - this.reverse.clear(); - } - - @SuppressWarnings("SuspiciousMethodCalls") - @Override - @NonNull - public Set keySet() { - return new LiveSet<>(Map.Entry::getKey, "key", this.forward::keySet, this.forward::containsKey); - } - - @SuppressWarnings("SuspiciousMethodCalls") - @Override - @NonNull - public Set values() { - return new LiveSet<>(Map.Entry::getValue, "value", this.reverse::keySet, this.reverse::containsKey); - } - - @SuppressWarnings("SuspiciousMethodCalls") - @Override - @NonNull - public Set> entrySet() { - return new LiveSet<>( - Function.identity(), "entry", this.forward::entrySet, - o -> o instanceof Map.Entry e - && this.forward.containsKey(e.getKey()) - && this.reverse.containsKey(e.getValue()) - ); - } - - @Override - @NonNull - public BiMap inverse() { - if (this.inverse == null) { - this.inverse = new Inverse<>(this); - } - - return this.inverse; - } - - private static class Inverse extends CompositeBiMap { - Inverse(CompositeBiMap original) { - super(original.reverse, original.forward); - this.inverse = original; - } - - @Override - @NonNull - public BiMap inverse() { - return this.inverse; - } - } - - private class LiveSet implements Set { - private static UnsupportedOperationException addExceptionOf(String elementName) { - return new UnsupportedOperationException("Cannot add to map via " + elementName + " set!"); - } - - final Function, E> elementFromEntry; - final Supplier> getDelegateSet; - final Predicate containsElement; - - final String elementName; - - LiveSet( - Function, E> elementFromEntry, String elementName, - Supplier> getDelegateSet, Predicate containsElement - ) { - this.elementFromEntry = elementFromEntry; - this.elementName = elementName; - this.getDelegateSet = getDelegateSet; - this.containsElement = containsElement; - } - - @Override - public int size() { - return CompositeBiMap.this.size(); - } - - @Override - public boolean isEmpty() { - return CompositeBiMap.this.isEmpty(); - } - - @Override - public boolean contains(Object o) { - return this.containsElement.test(o); - } - - @Override - @NonNull - public Iterator iterator() { - return new LiveIterator(); - } - - @Override - public Object @NonNull[] toArray() { - return this.getDelegateSet.get().toArray(); - } - - @Override - @NonNull - public T @NonNull[] toArray(T @NonNull[] array) { - return this.getDelegateSet.get().toArray(array); - } - - @Override - public boolean add(E element) { - throw addExceptionOf(this.elementName); - } - - @Override - public boolean remove(Object o) { - return CompositeBiMap.this.remove(o) != null; - } - - @Override - public boolean containsAll(Collection collection) { - for (final Object o : collection) { - if (!this.containsElement.test(o)) { - return false; - } - } - - return true; - } - - @Override - public boolean addAll(@NonNull Collection collection) { - throw addExceptionOf(this.elementName); - } - - @Override - public boolean retainAll(@NonNull Collection collection) { - return this.removeAllMatching(key -> !collection.contains(key)); - } - - @Override - public boolean removeAll(Collection collection) { - return this.removeAllMatching(collection::contains); - } - - private boolean removeAllMatching(Predicate predicate) { - boolean removed = false; - final Iterator> itr = CompositeBiMap.this.forward.entrySet().iterator(); - while (itr.hasNext()) { - final Entry entry = itr.next(); - if (predicate.test(this.elementFromEntry.apply(entry))) { - removed = true; - itr.remove(); - CompositeBiMap.this.reverse.remove(entry.getValue()); - } - } - - return removed; - } - - @Override - public void clear() { - CompositeBiMap.this.clear(); - } - - private final class LiveIterator implements Iterator { - final Iterator> delegate = CompositeBiMap.this.forward.entrySet().iterator(); - - Entry current; - - @Override - public boolean hasNext() { - return this.delegate.hasNext(); - } - - @Override - public E next() { - this.current = this.delegate.next(); - return LiveSet.this.elementFromEntry.apply(this.current); - } - - @Override - public void remove() { - this.delegate.remove(); - - CompositeBiMap.this.reverse.remove(this.current.getValue()); - } - } - } -} From 723df145659c7daf20629ea597922b3a7a5506da Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 26 Nov 2025 11:05:09 -0800 Subject: [PATCH 104/124] reword hints: preview -> view, execute -> choose --- .../enigma/gui/config/SearchMenusSection.java | 8 ++-- .../gui/element/menu_bar/SearchMenusMenu.java | 38 ++++++++++--------- enigma/src/main/resources/lang/en_us.json | 4 +- 3 files changed, 27 insertions(+), 23 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/SearchMenusSection.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/SearchMenusSection.java index e01b10fb5..091e5ca8c 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/SearchMenusSection.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/SearchMenusSection.java @@ -8,9 +8,9 @@ @SerializedNameConvention(NamingSchemes.SNAKE_CASE) public class SearchMenusSection extends ReflectiveConfig.Section { - @Comment("Whether to show the search menus preview hint until it's dismissed.") - public final TrackedValue showPreviewHint = this.value(true); + @Comment("Whether to show the search menus 'view' hint until it's dismissed.") + public final TrackedValue showViewHint = this.value(true); - @Comment("Whether to show the search menus execute hint until it's dismissed.") - public final TrackedValue showExecuteHint = this.value(true); + @Comment("Whether to show the search menus 'choose' hint until it's dismissed.") + public final TrackedValue showChooseHint = this.value(true); } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java index 5e53c3b5d..fbeff42ef 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java @@ -92,13 +92,13 @@ private static void clearSelectionAndChoose(SearchableElement searchable, MenuSe private final PlaceheldTextField field = new PlaceheldTextField(); private final JMenuItem noResults = new JMenuItem(); - private final HintItem previewHint = new HintItem( - "menu.help.search.hint.preview", - Config.main().searchMenus.showPreviewHint + private final HintItem viewHint = new HintItem( + "menu.help.search.hint.view", + Config.main().searchMenus.showViewHint ); - private final HintItem executeHint = new HintItem( - "menu.help.search.hint.execute", - Config.main().searchMenus.showExecuteHint + private final HintItem chooseHint = new HintItem( + "menu.help.search.hint.choose", + Config.main().searchMenus.showChooseHint ); @Nullable @@ -116,8 +116,8 @@ protected SearchMenusMenu(Gui gui) { this.noResults.setEnabled(false); this.noResults.setVisible(false); - this.previewHint.setVisible(false); - this.executeHint.setVisible(false); + this.viewHint.setVisible(false); + this.chooseHint.setVisible(false); this.addPermanentChildren(); @@ -179,8 +179,8 @@ private void updateResultItems() { if (results instanceof Results.None) { this.keepOnlyPermanentChildren(); - this.previewHint.setVisible(false); - this.executeHint.setVisible(false); + this.viewHint.setVisible(false); + this.chooseHint.setVisible(false); this.noResults.setVisible(!searchTerm.isEmpty()); @@ -189,8 +189,8 @@ private void updateResultItems() { this.keepOnlyPermanentChildren(); this.noResults.setVisible(different.results.isEmpty()); - this.previewHint.setVisible(Config.main().searchMenus.showPreviewHint.value()); - this.executeHint.setVisible(Config.main().searchMenus.showExecuteHint.value()); + this.viewHint.configureVisibility(); + this.chooseHint.configureVisibility(); different.results.forEach(this::add); @@ -213,8 +213,8 @@ private void refreshPopup() { private void addPermanentChildren() { this.add(this.field); this.add(this.noResults); - this.add(this.previewHint); - this.add(this.executeHint); + this.add(this.viewHint); + this.add(this.chooseHint); } private void keepOnlyPermanentChildren() { @@ -729,7 +729,7 @@ public void eventDispatched(AWTEvent e) { final MenuElement selected = getLastOrNull(selectedPath); if (selected != null) { if (selected instanceof Result.ItemHolder.Item item) { - SearchMenusMenu.this.previewHint.dismiss(); + SearchMenusMenu.this.viewHint.dismiss(); this.restorablePath = new RestorablePath(item.getSearchable(), selectedPath); @@ -773,7 +773,7 @@ public void eventDispatched(AWTEvent e) { } void execute(SearchableElement searchable, MenuSelectionManager manager) { - SearchMenusMenu.this.executeHint.dismiss(); + SearchMenusMenu.this.chooseHint.dismiss(); clearSelectionAndChoose(searchable, manager); } @@ -822,12 +822,16 @@ private class HintItem extends JPanel implements Retranslatable { this.retranslate(); } - private void dismiss() { + void dismiss() { this.config.setValue(false); this.setVisible(false); SearchMenusMenu.this.refreshPopup(); } + void configureVisibility() { + this.setVisible(this.config.value()); + } + @Override public void retranslate() { this.hint.setText(I18n.translate(this.translationKey)); diff --git a/enigma/src/main/resources/lang/en_us.json b/enigma/src/main/resources/lang/en_us.json index 58d4239a1..ccad2c8aa 100644 --- a/enigma/src/main/resources/lang/en_us.json +++ b/enigma/src/main/resources/lang/en_us.json @@ -122,8 +122,8 @@ "menu.help.search": "Search menus", "menu.help.search.placeholder": "Search menus...", "menu.help.search.no_results": "No results", - "menu.help.search.hint.preview": "Hold shift to preview the selected result", - "menu.help.search.hint.execute": "Press enter to execute the selected result", + "menu.help.search.hint.view": "Hold shift to view the selected result", + "menu.help.search.hint.choose": "Press enter to choose the selected result", "popup_menu.rename": "Rename", "popup_menu.paste": "Paste text", From 589ddf882408d778c85f59f6dd2a8b0e56cca64c Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 26 Nov 2025 11:16:07 -0800 Subject: [PATCH 105/124] update CONTRIBUTING.md for substring search, remove a substring alias --- CONTRIBUTING.md | 6 +++--- enigma/src/main/resources/lang/en_us.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d2cf89b62..0dd0f59f5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -41,12 +41,12 @@ This means that aliases may not contain the `;` character. Some things to keep in mind when adding search aliases: - elements' names are always searchable; there's no need to add their names to their aliases - searching is case-insensitive, so there's no need to add variations that only differ in capitalization -- searching matches prefixes, so there's no need to add variations that are prefixes of one another, -just add the longest variation (note that the element name may be a prefix of an alias, as is the case with `Dev`'s +- searching matches substrings, so there's no need to add a variation that's a substring of another variation, +just add the longer variation (note that the element name may be a substring of an alias, as is the case with `Dev`'s `"Development"` alias) If you'd like to add search aliases to an element that doesn't already have aliases, add its alias translation key to -its translation file. +the translation file. #### Complete list of search alias translation keys | Element | Translation Key | diff --git a/enigma/src/main/resources/lang/en_us.json b/enigma/src/main/resources/lang/en_us.json index ccad2c8aa..1585ff23f 100644 --- a/enigma/src/main/resources/lang/en_us.json +++ b/enigma/src/main/resources/lang/en_us.json @@ -3,7 +3,7 @@ "language.fr_fr.aliases": "French", "language.ja_jp.aliases": "Japanese", - "language.zh_cn.aliases": "Chinese;Simplified Chinese", + "language.zh_cn.aliases": "Simplified Chinese", "enigma:enigma_file": "Enigma File", "enigma:enigma_directory": "Enigma Directory", From b76dc005ca21a8730cdb0e410b1fb44bcbcf105c Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 26 Nov 2025 11:58:31 -0800 Subject: [PATCH 106/124] add path tooltips to search result items --- .../gui/element/menu_bar/SearchMenusMenu.java | 93 ++++++++++++++----- 1 file changed, 70 insertions(+), 23 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java index fbeff42ef..d4b43ea0e 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java @@ -19,6 +19,7 @@ import org.quiltmc.enigma.util.multi_trie.MutableStringMultiTrie; import org.quiltmc.enigma.util.multi_trie.StringMultiTrie; import org.quiltmc.enigma.util.multi_trie.StringMultiTrie.Node; +import org.tinylog.Logger; import javax.swing.Box; import javax.swing.JButton; @@ -57,6 +58,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; import java.util.stream.Stream; import static com.google.common.collect.ImmutableList.toImmutableList; @@ -65,9 +67,6 @@ import static javax.swing.BorderFactory.createEmptyBorder; public class SearchMenusMenu extends AbstractEnigmaMenu { - @Nullable - private final Border defaultPopupBorder; - /** * @return a breadth-first stream of the passed {@code root} element and all of its sub-elements, * excluding the {@link HelpMenu} and its sub-elements; the help menu is not searchable because it must be open @@ -89,6 +88,9 @@ private static void clearSelectionAndChoose(SearchableElement searchable, MenuSe searchable.onSearchChosen(); } + @Nullable + private final Border defaultPopupBorder; + private final PlaceheldTextField field = new PlaceheldTextField(); private final JMenuItem noResults = new JMenuItem(); @@ -549,12 +551,75 @@ public int compareTo(@NonNull ItemHolder other) { } class Item extends JMenuItem { + private static ImmutableList buildPath(SearchableElement searchable) { + final List pathBuilder = new LinkedList<>(); + pathBuilder.add(searchable); + Component element = searchable.getComponent().getParent(); + while (true) { + if (element instanceof JMenu menu) { + pathBuilder.add(0, menu); + element = menu.getParent(); + } else if (element instanceof JPopupMenu popup) { + pathBuilder.add(0, popup); + element = popup.getInvoker(); + } else { + break; + } + } + + if (element instanceof JMenuBar bar) { + pathBuilder.add(0, bar); + + return ImmutableList.copyOf(pathBuilder); + } else { + Logger.error( + """ + Failed to build path to %s! + \tPath does not begin with menu bar: %s + """.formatted(searchable.getSearchName(), pathBuilder) + ); + + return ImmutableList.of(); + } + } + + private final ImmutableList path; + Item(String searchName) { super(searchName); this.addActionListener(e -> { clearSelectionAndChoose(Result.this.searchable, MenuSelectionManager.defaultManager()); }); + + this.path = buildPath(this.getSearchable()); + + if (!this.path.isEmpty()) { + final String pathText = this.path.stream() + .flatMap(element -> { + if (element instanceof SearchableElement searchableElement) { + return Stream.of(searchableElement.getSearchName()); + } else if (element.getComponent() instanceof JMenuItem menuItem) { + return Stream.of(menuItem.getText()); + } else { + // JPopupMenus' names come from their parent JMenus; skip them + // JMenuBar has no name + if (element instanceof JPopupMenu || element instanceof JMenuBar) { + return Stream.empty(); + } else { + Logger.error( + "Cannot determine name of menu element in path to %s: %s" + .formatted(searchName, element) + ); + + return Stream.of("???"); + } + } + }) + .collect(Collectors.joining(" > ")); + + this.setToolTipText(pathText); + } } boolean isSearchNamed() { @@ -566,26 +631,8 @@ ItemHolder getHolder() { } void selectSearchable() { - final MenuSelectionManager manager = MenuSelectionManager.defaultManager(); - final List pathBuilder = new LinkedList<>(); - pathBuilder.add(this.getSearchable()); - Component element = this.getSearchable().getComponent().getParent(); - while (true) { - if (element instanceof JMenu menu) { - pathBuilder.add(0, menu); - element = menu.getParent(); - } else if (element instanceof JPopupMenu popup) { - pathBuilder.add(0, popup); - element = popup.getInvoker(); - } else { - break; - } - } - - if (element instanceof JMenuBar bar) { - pathBuilder.add(0, bar); - - manager.setSelectedPath(pathBuilder.toArray(pathBuilder.toArray(new MenuElement[0]))); + if (!this.path.isEmpty()) { + MenuSelectionManager.defaultManager().setSelectedPath(this.path.toArray(new MenuElement[0])); } } From 66010539b23e47dfc063330d788652a89a4219ff Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 26 Nov 2025 12:14:31 -0800 Subject: [PATCH 107/124] base ItemHolds' identity on their SearchableElement to prevent duplicate search results capitalize OS --- .../enigma/gui/element/menu_bar/SearchMenusMenu.java | 10 ++++++++++ enigma/src/main/resources/lang/en_us.json | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java index d4b43ea0e..5b4bdadc5 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java @@ -550,6 +550,16 @@ public int compareTo(@NonNull ItemHolder other) { return this.getSearchable().getSearchName().compareTo(other.getSearchable().getSearchName()); } + @Override + public int hashCode() { + return this.getSearchable().hashCode(); + } + + @Override + public boolean equals(Object o) { + return o instanceof ItemHolder other && this.getSearchable() == other.getSearchable(); + } + class Item extends JMenuItem { private static ImmutableList buildPath(SearchableElement searchable) { final List pathBuilder = new LinkedList<>(); diff --git a/enigma/src/main/resources/lang/en_us.json b/enigma/src/main/resources/lang/en_us.json index 1585ff23f..25ef8936a 100644 --- a/enigma/src/main/resources/lang/en_us.json +++ b/enigma/src/main/resources/lang/en_us.json @@ -80,7 +80,7 @@ "menu.view.themes.darcerula.aliases": "Darker;Dracula", "menu.view.themes.metal": "Metal", "menu.view.themes.system": "System", - "menu.view.themes.system.aliases": "Os;Operating System", + "menu.view.themes.system.aliases": "OS;Operating System", "menu.view.themes.none": "None (JVM Default)", "menu.view.themes.none.aliases": "Java", "menu.view.languages": "Languages", From b6767d38411002f06da02024a391fc2021a54dc2 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 26 Nov 2025 12:43:49 -0800 Subject: [PATCH 108/124] fix showing 'No results' even after deleting search term --- .../gui/element/menu_bar/SearchMenusMenu.java | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java index 5b4bdadc5..80c1dd329 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java @@ -202,13 +202,19 @@ private void updateResultItems() { private void refreshPopup() { if (this.isShowing()) { + final JPopupMenu popupMenu = this.getPopupMenu(); + // HACK: When popups are resizing in limited space, they may remove their borders. // The border won't be restored when re-packing or showing, so manually restore the original border here. - this.getPopupMenu().setBorder(this.defaultPopupBorder); - this.getPopupMenu().pack(); - - final Point popupMenuOrigin = this.getPopupMenuOrigin(); - this.getPopupMenu().show(this, popupMenuOrigin.x, popupMenuOrigin.y); + popupMenu.setBorder(this.defaultPopupBorder); + popupMenu.pack(); + + // only re-show + // the initial showing from JMenu does the same thing and would cause an SOE if we also showed here + if (popupMenu.isShowing()) { + final Point newOrigin = this.getPopupMenuOrigin(); + popupMenu.show(this, newOrigin.x, newOrigin.y); + } } } @@ -323,11 +329,9 @@ static Lookup build(Gui gui) { Results search(String term) { if (term.isEmpty()) { - final boolean wasEmpty = !this.resultCache.hasResults(); - this.resultCache = this.emptyCache; - return wasEmpty ? Results.Same.INSTANCE : Results.None.INSTANCE; + return Results.None.INSTANCE; } final ResultCache oldCache = this.resultCache; From c3fe298694c4f102a33418bf112ebfa626167abd Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 26 Nov 2025 18:17:39 -0800 Subject: [PATCH 109/124] truncate results to avoid dropping keystrokes due to lag caused by packing numerous results --- .../gui/element/menu_bar/SearchMenusMenu.java | 77 ++++++++++++------- 1 file changed, 51 insertions(+), 26 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java index 80c1dd329..1fcfaaf5b 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java @@ -2,8 +2,6 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableSet; -import com.google.common.collect.Streams; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; import org.quiltmc.config.api.values.TrackedValue; @@ -63,10 +61,11 @@ import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.collect.ImmutableMap.toImmutableMap; -import static com.google.common.collect.ImmutableSet.toImmutableSet; import static javax.swing.BorderFactory.createEmptyBorder; public class SearchMenusMenu extends AbstractEnigmaMenu { + private static final int MAX_INITIAL_RESULTS = 20; + /** * @return a breadth-first stream of the passed {@code root} element and all of its sub-elements, * excluding the {@link HelpMenu} and its sub-elements; the help menu is not searchable because it must be open @@ -190,11 +189,35 @@ private void updateResultItems() { } else if (results instanceof Results.Different different) { this.keepOnlyPermanentChildren(); - this.noResults.setVisible(different.results.isEmpty()); + this.noResults.setVisible(different.isEmpty()); this.viewHint.configureVisibility(); this.chooseHint.configureVisibility(); - different.results.forEach(this::add); + // truncate results because the popup lags when packing numerous items, which can cause keystroke drops + int remainingItems = MAX_INITIAL_RESULTS; + for (final Result.ItemHolder.Item result : different.prefixItems) { + if (remainingItems > 0) { + remainingItems--; + this.add(result); + } else { + break; + } + } + + if (remainingItems > 0 && !different.containingItems.isEmpty()) { + if (!different.prefixItems.isEmpty()) { + this.add(new JPopupMenu.Separator()); + } + + for (final Result.ItemHolder.Item result : different.containingItems) { + if (remainingItems > 0) { + remainingItems--; + this.add(result); + } else { + break; + } + } + } this.refreshPopup(); } // else Results.Same @@ -262,7 +285,7 @@ private static final class Lookup { final ResultCache emptyCache = new ResultCache( "", EmptyStringMultiTrie.Node.get(), - ImmutableMap.of(), ImmutableSet.of() + ImmutableMap.of(), ImmutableList.of() ); static int getCommonPrefixLength(String left, String right) { @@ -352,12 +375,12 @@ final class ResultCache { final String term; final Node prefixNode; final ImmutableMap prefixedItemsBySearchable; - final ImmutableSet containingItems; + final ImmutableList containingItems; ResultCache( String term, Node prefixNode, ImmutableMap prefixedItemsBySearchable, - ImmutableSet containingItems + ImmutableList containingItems ) { this.term = term; this.prefixNode = prefixNode; @@ -413,7 +436,7 @@ ResultCache updated(String term) { prefixedItemsBySearchable = buildPrefixedItemsBySearchable(prefixNode); } - final ImmutableSet containingItems; + final ImmutableList containingItems; if (cachedTermLength == commonPrefixLength && termLength > MAX_SUBSTRING_LENGTH) { containingItems = this.narrowedContainingItemsOf(term); } else { @@ -450,13 +473,13 @@ static ImmutableMap buildPrefixedItem )); } - ImmutableSet narrowedContainingItemsOf(String term) { + ImmutableList narrowedContainingItemsOf(String term) { return this.containingItems.stream() .filter(item -> item.getHolder().lowercaseAlias.contains(term)) - .collect(toImmutableSet()); + .collect(toImmutableList()); } - ImmutableSet buildContaining(String term, Set excluded) { + ImmutableList buildContaining(String term, Set excluded) { final int termLength = term.length(); final boolean longTerm = termLength > MAX_SUBSTRING_LENGTH; @@ -488,7 +511,7 @@ ImmutableSet buildContaining(String term, Set results) implements Results { + record Different( + ImmutableList prefixItems, + ImmutableList containingItems + ) implements Results { static Different of(Lookup.ResultCache cache) { - final Stream separator = - !cache.prefixedItemsBySearchable.isEmpty() && !cache.containingItems.isEmpty() - ? Stream.of(new JPopupMenu.Separator()) - : Stream.empty(); - - return new Different(Streams - .concat( - cache.prefixedItemsBySearchable.values().stream(), - separator, - cache.containingItems.stream() - ) - .collect(toImmutableList()) + return new Different( + cache.prefixedItemsBySearchable.values().stream().distinct().collect(toImmutableList()), + cache.containingItems.stream().distinct().collect(toImmutableList()) ); } + + boolean isEmpty() { + return this.prefixItems.isEmpty() && this.containingItems.isEmpty(); + } + + int getSize() { + return this.prefixItems.size() + this.containingItems.size(); + } } } From 0d20950127f22364148a220f6bfa421f7ec3f9ad Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 26 Nov 2025 19:15:42 -0800 Subject: [PATCH 110/124] add button for showing truncated results --- .../gui/element/menu_bar/SearchMenusMenu.java | 64 ++++++++++++++----- 1 file changed, 49 insertions(+), 15 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java index 1fcfaaf5b..af08ced60 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java @@ -2,6 +2,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.UnmodifiableIterator; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; import org.quiltmc.config.api.values.TrackedValue; @@ -195,30 +196,34 @@ private void updateResultItems() { // truncate results because the popup lags when packing numerous items, which can cause keystroke drops int remainingItems = MAX_INITIAL_RESULTS; - for (final Result.ItemHolder.Item result : different.prefixItems) { - if (remainingItems > 0) { - remainingItems--; - this.add(result); - } else { - break; - } + final UnmodifiableIterator prefixItr = different.prefixItems.iterator(); + while (prefixItr.hasNext() && remainingItems > 0) { + this.add(prefixItr.next()); + remainingItems--; } - if (remainingItems > 0 && !different.containingItems.isEmpty()) { + final UnmodifiableIterator containingItr = different.containingItems.iterator(); + if (remainingItems > 0 && containingItr.hasNext()) { if (!different.prefixItems.isEmpty()) { this.add(new JPopupMenu.Separator()); } - for (final Result.ItemHolder.Item result : different.containingItems) { - if (remainingItems > 0) { - remainingItems--; - this.add(result); - } else { - break; - } + while (containingItr.hasNext() && remainingItems > 0) { + this.add(containingItr.next()); + remainingItems--; } } + if (different.getSize() > MAX_INITIAL_RESULTS) { + final ImmutableList.Builder truncatedPrefixResults = ImmutableList.builder(); + prefixItr.forEachRemaining(truncatedPrefixResults::add); + + final ImmutableList.Builder truncatedContainingResults = ImmutableList.builder(); + containingItr.forEachRemaining(truncatedContainingResults::add); + + this.add(this.moreButtonOf(truncatedPrefixResults.build(), truncatedContainingResults.build())); + } + this.refreshPopup(); } // else Results.Same } @@ -279,6 +284,35 @@ public void retranslate() { this.noResults.setText(I18n.translate("menu.help.search.no_results")); } + private JMenuItem moreButtonOf( + ImmutableList prefixItems, + ImmutableList containingItems + ) { + final var button = new JMenuItem(); + + button.setText("⋯"); + + button.addActionListener(e -> { + this.remove(button); + + prefixItems.forEach(this::add); + + if (!prefixItems.isEmpty() && !containingItems.isEmpty()) { + this.add(new JPopupMenu.Separator()); + } + + containingItems.forEach(this::add); + + // clicking the button closes the menu + // this re-opens it (which includes re-packing and selecting the search field) + this.doClick(0); + // de-select and move caret to end + this.field.setSelectionStart(Integer.MAX_VALUE); + }); + + return button; + } + private static final class Lookup { static final int NON_PREFIX_START = 1; static final int MAX_SUBSTRING_LENGTH = 2; From c0aeed032562bcaa693f59d4bd6d601e1ad171f5 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Thu, 27 Nov 2025 10:05:09 -0800 Subject: [PATCH 111/124] add selection border to PlaceheldTextField scale SearchMenusMenu.field's font select SearchMenusMenu.field when focusing it --- .../gui/element/PlaceheldTextField.java | 108 +++++++++++++++++- .../gui/element/menu_bar/SearchMenusMenu.java | 103 ++++++++++------- 2 files changed, 168 insertions(+), 43 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/PlaceheldTextField.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/PlaceheldTextField.java index 0cda5f483..332d8c9f8 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/PlaceheldTextField.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/PlaceheldTextField.java @@ -2,14 +2,20 @@ import org.jspecify.annotations.Nullable; import org.quiltmc.enigma.gui.util.GuiUtil; +import org.quiltmc.enigma.gui.util.ScaleUtil; import org.quiltmc.enigma.util.Utils; import javax.swing.JTextField; import javax.swing.MenuElement; import javax.swing.MenuSelectionManager; +import javax.swing.UIManager; +import javax.swing.border.Border; +import javax.swing.border.CompoundBorder; +import javax.swing.border.MatteBorder; import javax.swing.text.Document; import java.awt.Color; import java.awt.Component; +import java.awt.Container; import java.awt.Dimension; import java.awt.Font; import java.awt.Graphics; @@ -17,16 +23,30 @@ import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; +import static javax.swing.BorderFactory.createCompoundBorder; +import static javax.swing.BorderFactory.createEmptyBorder; + /** * A text field that displays placeholder text when it's empty. */ public class PlaceheldTextField extends JTextField implements MenuElement { + private static final int DEFAULT_SELECTION_BORDER_LEFT = ScaleUtil.scale(3); + private static final int DEFAULT_SELECTION_BORDER_RIGHT = DEFAULT_SELECTION_BORDER_LEFT; + private static final int DEFAULT_SELECTION_BORDER_TOP = ScaleUtil.scale(1); + private static final int DEFAULT_SELECTION_BORDER_BOTTOM = DEFAULT_SELECTION_BORDER_TOP; + @Nullable private Placeholder placeholder; @Nullable private Color placeholderColor; + private CompoundBorder defaultBorder; + + private CompoundBorder selectionBorder; + + private boolean selectionIncluded; + /** * Constructs a new field with the default {@link Document}, {@code 0} columns, and no initial text or placeholder. */ @@ -61,6 +81,26 @@ public PlaceheldTextField( super(doc, text, columns); this.placeholder = new Placeholder(placeholder); + + final Border originalBorder = this.getBorder(); + this.selectionBorder = createCompoundBorder( + new MatteBorder( + DEFAULT_SELECTION_BORDER_TOP, DEFAULT_SELECTION_BORDER_LEFT, + DEFAULT_SELECTION_BORDER_BOTTOM, DEFAULT_SELECTION_BORDER_RIGHT, + UIManager.getColor("MenuItem.selectionBackground") + ), + originalBorder + ); + + this.defaultBorder = createCompoundBorder( + createEmptyBorder( + DEFAULT_SELECTION_BORDER_TOP, DEFAULT_SELECTION_BORDER_LEFT, + DEFAULT_SELECTION_BORDER_BOTTOM, DEFAULT_SELECTION_BORDER_RIGHT + ), + originalBorder + ); + + super.setBorder(this.defaultBorder); } @Override @@ -76,6 +116,66 @@ public Dimension getPreferredSize() { return size; } + @Override + public void setBorder(Border border) { + if (this.selectionBorder == null) { + // JTextField sets border in constructor before selectionBorder is initialized + super.setBorder(border); + } else { + this.defaultBorder = this.createDefaultBorder(border); + this.selectionBorder = createCompoundBorder(this.selectionBorder.getOutsideBorder(), border); + super.setBorder(this.defaultBorder); + } + } + + private CompoundBorder createDefaultBorder(Border border) { + final Border selectionOuter = this.selectionBorder.getOutsideBorder(); + if (selectionOuter == null) { + return createCompoundBorder(null, border); + } else { + final Insets selectionInsets = selectionOuter.getBorderInsets(this); + return createCompoundBorder( + createEmptyBorder(selectionInsets.top, selectionInsets.left, selectionInsets.bottom, selectionInsets.right), + border + ); + } + } + + public void setSelectionBorder(Border border) { + final Border insideBorder = this.defaultBorder.getInsideBorder(); + this.selectionBorder = createCompoundBorder(border, insideBorder); + this.defaultBorder = this.createDefaultBorder(insideBorder); + super.setBorder(this.defaultBorder); + } + + @Override + protected void paintBorder(Graphics graphics) { + if (this.selectionIncluded) { + final int leftInset; + final int topInset; + { + final Container parent = this.getParent(); + + if (parent == null) { + leftInset = 0; + topInset = 0; + } else { + final Insets insets = parent.getInsets(); + leftInset = insets.left; + topInset = insets.top; + } + } + + this.selectionBorder.paintBorder( + this, graphics, + this.getX() - leftInset, this.getY() - topInset, + this.getWidth(), this.getHeight() + ); + } else { + super.paintBorder(graphics); + } + } + @Override protected void paintComponent(Graphics graphics) { super.paintComponent(graphics); @@ -115,7 +215,13 @@ public void processMouseEvent(MouseEvent event, MenuElement[] path, MenuSelectio public void processKeyEvent(KeyEvent event, MenuElement[] path, MenuSelectionManager manager) { } @Override - public void menuSelectionChanged(boolean isIncluded) { } + public void menuSelectionChanged(boolean isIncluded) { + if (this.selectionIncluded != isIncluded) { + this.selectionIncluded = isIncluded; + // update border + this.repaint(); + } + } @Override public MenuElement[] getSubElements() { diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java index af08ced60..866750bd7 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java @@ -12,6 +12,7 @@ import org.quiltmc.enigma.gui.element.PlaceheldTextField; import org.quiltmc.enigma.gui.util.GridBagConstraintsBuilder; import org.quiltmc.enigma.gui.util.GuiUtil; +import org.quiltmc.enigma.gui.util.ScaleUtil; import org.quiltmc.enigma.util.I18n; import org.quiltmc.enigma.util.multi_trie.CompositeStringMultiTrie; import org.quiltmc.enigma.util.multi_trie.EmptyStringMultiTrie; @@ -49,6 +50,8 @@ import java.awt.Toolkit; import java.awt.Window; import java.awt.event.AWTEventListener; +import java.awt.event.HierarchyEvent; +import java.awt.event.HierarchyListener; import java.awt.event.InputEvent; import java.awt.event.KeyEvent; import java.util.Arrays; @@ -88,6 +91,38 @@ private static void clearSelectionAndChoose(SearchableElement searchable, MenuSe searchable.onSearchChosen(); } + private static ImmutableList buildPathTo(MenuElement target) { + final List pathBuilder = new LinkedList<>(); + pathBuilder.add(target); + Component element = target.getComponent().getParent(); + while (true) { + if (element instanceof JMenu menu) { + pathBuilder.add(0, menu); + element = menu.getParent(); + } else if (element instanceof JPopupMenu popup) { + pathBuilder.add(0, popup); + element = popup.getInvoker(); + } else { + break; + } + } + + if (element instanceof JMenuBar bar) { + pathBuilder.add(0, bar); + + return ImmutableList.copyOf(pathBuilder); + } else { + Logger.error( + """ + Failed to build path to %s! + \tPath does not begin with menu bar: %s + """.formatted(target, pathBuilder) + ); + + return ImmutableList.of(); + } + } + @Nullable private final Border defaultPopupBorder; @@ -123,18 +158,34 @@ protected SearchMenusMenu(Gui gui) { this.addPermanentChildren(); + this.field.setFont(ScaleUtil.scaleFont(this.field.getFont())); // Always focus field, but don't always select its text, because it loses focus when packing new search results. - this.field.addHierarchyListener(e -> { - if (this.field.isShowing()) { - final Window window = SwingUtilities.getWindowAncestor(this.field); - if (window != null && window.getType() == Window.Type.POPUP) { - // HACK: if PopupFactory::fitsOnScreen is false for light- and medium-weight popups, it makes a - // heavy-weight popup instead, whose HeavyWeightWindow component is by default is not focusable. - // It prevented this.field from focusing and receiving input. - window.setFocusableWindowState(true); + this.field.addHierarchyListener(new HierarchyListener() { + @Nullable + ImmutableList fieldPath; + + ImmutableList getFieldPath() { + if (this.fieldPath == null) { + this.fieldPath = buildPathTo(SearchMenusMenu.this.field); } - this.field.requestFocus(); + return this.fieldPath; + } + + @Override + public void hierarchyChanged(HierarchyEvent e) { + if (SearchMenusMenu.this.field.isShowing()) { + final Window window = SwingUtilities.getWindowAncestor(SearchMenusMenu.this.field); + if (window != null && window.getType() == Window.Type.POPUP) { + // HACK: if PopupFactory::fitsOnScreen is false for light- and medium-weight popups, it makes a + // heavy-weight popup instead, whose HeavyWeightWindow component is by default is not focusable. + // It prevented this.field from focusing and receiving input. + window.setFocusableWindowState(true); + } + + SearchMenusMenu.this.field.requestFocus(); + MenuSelectionManager.defaultManager().setSelectedPath(this.getFieldPath().toArray(new MenuElement[0])); + } } }); @@ -622,38 +673,6 @@ public boolean equals(Object o) { } class Item extends JMenuItem { - private static ImmutableList buildPath(SearchableElement searchable) { - final List pathBuilder = new LinkedList<>(); - pathBuilder.add(searchable); - Component element = searchable.getComponent().getParent(); - while (true) { - if (element instanceof JMenu menu) { - pathBuilder.add(0, menu); - element = menu.getParent(); - } else if (element instanceof JPopupMenu popup) { - pathBuilder.add(0, popup); - element = popup.getInvoker(); - } else { - break; - } - } - - if (element instanceof JMenuBar bar) { - pathBuilder.add(0, bar); - - return ImmutableList.copyOf(pathBuilder); - } else { - Logger.error( - """ - Failed to build path to %s! - \tPath does not begin with menu bar: %s - """.formatted(searchable.getSearchName(), pathBuilder) - ); - - return ImmutableList.of(); - } - } - private final ImmutableList path; Item(String searchName) { @@ -663,7 +682,7 @@ private static ImmutableList buildPath(SearchableElement searchable clearSelectionAndChoose(Result.this.searchable, MenuSelectionManager.defaultManager()); }); - this.path = buildPath(this.getSearchable()); + this.path = buildPathTo(this.getSearchable()); if (!this.path.isEmpty()) { final String pathText = this.path.stream() From 5c9e5f9a24670b34db47c51ffcaa66282a7d55c0 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Thu, 27 Nov 2025 11:21:16 -0800 Subject: [PATCH 112/124] split PlaceheldMenuTextField subclass from PlaceheldTextField --- .../gui/element/PlaceheldTextField.java | 162 +++-------------- .../menu_bar/PlaceheldMenuTextField.java | 168 ++++++++++++++++++ .../gui/element/menu_bar/SearchMenusMenu.java | 3 +- 3 files changed, 189 insertions(+), 144 deletions(-) create mode 100644 enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/PlaceheldMenuTextField.java diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/PlaceheldTextField.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/PlaceheldTextField.java index 332d8c9f8..db53b8e13 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/PlaceheldTextField.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/PlaceheldTextField.java @@ -2,67 +2,44 @@ import org.jspecify.annotations.Nullable; import org.quiltmc.enigma.gui.util.GuiUtil; -import org.quiltmc.enigma.gui.util.ScaleUtil; import org.quiltmc.enigma.util.Utils; import javax.swing.JTextField; -import javax.swing.MenuElement; -import javax.swing.MenuSelectionManager; -import javax.swing.UIManager; -import javax.swing.border.Border; -import javax.swing.border.CompoundBorder; -import javax.swing.border.MatteBorder; import javax.swing.text.Document; import java.awt.Color; -import java.awt.Component; -import java.awt.Container; import java.awt.Dimension; import java.awt.Font; import java.awt.Graphics; import java.awt.Insets; -import java.awt.event.KeyEvent; -import java.awt.event.MouseEvent; - -import static javax.swing.BorderFactory.createCompoundBorder; -import static javax.swing.BorderFactory.createEmptyBorder; /** * A text field that displays placeholder text when it's empty. */ -public class PlaceheldTextField extends JTextField implements MenuElement { - private static final int DEFAULT_SELECTION_BORDER_LEFT = ScaleUtil.scale(3); - private static final int DEFAULT_SELECTION_BORDER_RIGHT = DEFAULT_SELECTION_BORDER_LEFT; - private static final int DEFAULT_SELECTION_BORDER_TOP = ScaleUtil.scale(1); - private static final int DEFAULT_SELECTION_BORDER_BOTTOM = DEFAULT_SELECTION_BORDER_TOP; +public class PlaceheldTextField extends JTextField { + protected static final int DEFAULT_COLUMNS = 0; @Nullable - private Placeholder placeholder; - + protected Placeholder placeholder; @Nullable private Color placeholderColor; - private CompoundBorder defaultBorder; - - private CompoundBorder selectionBorder; - - private boolean selectionIncluded; - /** - * Constructs a new field with the default {@link Document}, {@code 0} columns, and no initial text or placeholder. + * Constructs a new field with the default {@link Document}, {@value PlaceheldTextField#DEFAULT_COLUMNS} columns, + * and no initial text or placeholder. */ public PlaceheldTextField() { this(null, null); } /** - * Constructs a new field with the default {@link Document}, {@code 0} columns, and the passed initial - * {@code text} and {@code placeholder}. + * Constructs a new field with the default {@link Document}, {@value PlaceheldTextField#DEFAULT_COLUMNS} columns, + * and the passed initial {@code text} and {@code placeholder}. * * @param text the initial text; may be {@code null} * @param placeholder the initial placeholder; may be {@code null} */ public PlaceheldTextField(String text, String placeholder) { - this(null, text, placeholder, 0); + this(null, text, placeholder, DEFAULT_COLUMNS); } /** @@ -75,32 +52,9 @@ public PlaceheldTextField(String text, String placeholder) { * * @exception IllegalArgumentException if {@code columns} is negative */ - public PlaceheldTextField( - @Nullable Document doc, @Nullable String text, @Nullable String placeholder, int columns - ) { + public PlaceheldTextField(Document doc, String text, @Nullable String placeholder, int columns) { super(doc, text, columns); - this.placeholder = new Placeholder(placeholder); - - final Border originalBorder = this.getBorder(); - this.selectionBorder = createCompoundBorder( - new MatteBorder( - DEFAULT_SELECTION_BORDER_TOP, DEFAULT_SELECTION_BORDER_LEFT, - DEFAULT_SELECTION_BORDER_BOTTOM, DEFAULT_SELECTION_BORDER_RIGHT, - UIManager.getColor("MenuItem.selectionBackground") - ), - originalBorder - ); - - this.defaultBorder = createCompoundBorder( - createEmptyBorder( - DEFAULT_SELECTION_BORDER_TOP, DEFAULT_SELECTION_BORDER_LEFT, - DEFAULT_SELECTION_BORDER_BOTTOM, DEFAULT_SELECTION_BORDER_RIGHT - ), - originalBorder - ); - - super.setBorder(this.defaultBorder); } @Override @@ -116,66 +70,6 @@ public Dimension getPreferredSize() { return size; } - @Override - public void setBorder(Border border) { - if (this.selectionBorder == null) { - // JTextField sets border in constructor before selectionBorder is initialized - super.setBorder(border); - } else { - this.defaultBorder = this.createDefaultBorder(border); - this.selectionBorder = createCompoundBorder(this.selectionBorder.getOutsideBorder(), border); - super.setBorder(this.defaultBorder); - } - } - - private CompoundBorder createDefaultBorder(Border border) { - final Border selectionOuter = this.selectionBorder.getOutsideBorder(); - if (selectionOuter == null) { - return createCompoundBorder(null, border); - } else { - final Insets selectionInsets = selectionOuter.getBorderInsets(this); - return createCompoundBorder( - createEmptyBorder(selectionInsets.top, selectionInsets.left, selectionInsets.bottom, selectionInsets.right), - border - ); - } - } - - public void setSelectionBorder(Border border) { - final Border insideBorder = this.defaultBorder.getInsideBorder(); - this.selectionBorder = createCompoundBorder(border, insideBorder); - this.defaultBorder = this.createDefaultBorder(insideBorder); - super.setBorder(this.defaultBorder); - } - - @Override - protected void paintBorder(Graphics graphics) { - if (this.selectionIncluded) { - final int leftInset; - final int topInset; - { - final Container parent = this.getParent(); - - if (parent == null) { - leftInset = 0; - topInset = 0; - } else { - final Insets insets = parent.getInsets(); - leftInset = insets.left; - topInset = insets.top; - } - } - - this.selectionBorder.paintBorder( - this, graphics, - this.getX() - leftInset, this.getY() - topInset, - this.getWidth(), this.getHeight() - ); - } else { - super.paintBorder(graphics); - } - } - @Override protected void paintComponent(Graphics graphics) { super.paintComponent(graphics); @@ -197,7 +91,16 @@ protected void paintComponent(Graphics graphics) { * @param placeholder the placeholder text for this field; if {@code null}, no placeholder will be shown */ public void setPlaceholder(@Nullable String placeholder) { - this.placeholder = new Placeholder(placeholder); + this.placeholder = placeholder == null ? null : new Placeholder(placeholder); + } + + public String getPlaceholder() { + return this.placeholder == null ? "" : this.placeholder.text; + } + + @Nullable + protected Placeholder getPlaceholderObject() { + return this.placeholder; } /** @@ -208,31 +111,6 @@ public void setPlaceholderColor(@Nullable Color color) { this.placeholderColor = color; } - @Override - public void processMouseEvent(MouseEvent event, MenuElement[] path, MenuSelectionManager manager) { } - - @Override - public void processKeyEvent(KeyEvent event, MenuElement[] path, MenuSelectionManager manager) { } - - @Override - public void menuSelectionChanged(boolean isIncluded) { - if (this.selectionIncluded != isIncluded) { - this.selectionIncluded = isIncluded; - // update border - this.repaint(); - } - } - - @Override - public MenuElement[] getSubElements() { - return new MenuElement[0]; - } - - @Override - public Component getComponent() { - return this; - } - @Override public void setFont(Font f) { super.setFont(f); @@ -242,7 +120,7 @@ public void setFont(Font f) { } } - private class Placeholder { + protected class Placeholder { static final int UNSET_WIDTH = -1; final String text; diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/PlaceheldMenuTextField.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/PlaceheldMenuTextField.java new file mode 100644 index 000000000..4f326c45d --- /dev/null +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/PlaceheldMenuTextField.java @@ -0,0 +1,168 @@ +package org.quiltmc.enigma.gui.element.menu_bar; + +import org.jspecify.annotations.Nullable; +import org.quiltmc.enigma.gui.element.PlaceheldTextField; +import org.quiltmc.enigma.gui.util.ScaleUtil; + +import javax.swing.MenuElement; +import javax.swing.MenuSelectionManager; +import javax.swing.UIManager; +import javax.swing.border.Border; +import javax.swing.border.CompoundBorder; +import javax.swing.border.MatteBorder; +import javax.swing.text.Document; +import java.awt.Component; +import java.awt.Container; +import java.awt.Graphics; +import java.awt.Insets; +import java.awt.event.KeyEvent; +import java.awt.event.MouseEvent; + +import static javax.swing.BorderFactory.createCompoundBorder; +import static javax.swing.BorderFactory.createEmptyBorder; + +/** + * A {@link PlaceheldTextField} that is also a {@link MenuElement}. + * + *

    Displays an {@linkplain #setSelectionBorder(Border) additional border} when part of the + * {@linkplain MenuSelectionManager#getSelectedPath() menu selection}. + */ +public class PlaceheldMenuTextField extends PlaceheldTextField implements MenuElement { + private static final int DEFAULT_SELECTION_BORDER_LEFT = ScaleUtil.scale(3); + private static final int DEFAULT_SELECTION_BORDER_RIGHT = DEFAULT_SELECTION_BORDER_LEFT; + private static final int DEFAULT_SELECTION_BORDER_TOP = ScaleUtil.scale(1); + private static final int DEFAULT_SELECTION_BORDER_BOTTOM = DEFAULT_SELECTION_BORDER_TOP; + + private CompoundBorder defaultBorder; + private CompoundBorder selectionBorder; + + private boolean selectionIncluded; + + /** + * @see PlaceheldTextField#PlaceheldTextField() PlaceheldTextField + */ + public PlaceheldMenuTextField() { + this(null, null); + } + + /** + * @see PlaceheldTextField#PlaceheldTextField(String, String) PlaceheldTextField + */ + public PlaceheldMenuTextField(String text, String placeholder) { + this(null, text, placeholder, DEFAULT_COLUMNS); + } + + /** + * @see PlaceheldTextField#PlaceheldTextField(Document, String, String, int) PlaceheldTextField + */ + public PlaceheldMenuTextField( + @Nullable Document doc, @Nullable String text, @Nullable String placeholder, int columns + ) { + super(doc, text, placeholder, columns); + + final Border originalBorder = this.getBorder(); + this.selectionBorder = createCompoundBorder( + new MatteBorder( + DEFAULT_SELECTION_BORDER_TOP, DEFAULT_SELECTION_BORDER_LEFT, + DEFAULT_SELECTION_BORDER_BOTTOM, DEFAULT_SELECTION_BORDER_RIGHT, + UIManager.getColor("MenuItem.selectionBackground") + ), + originalBorder + ); + + this.defaultBorder = createCompoundBorder( + createEmptyBorder( + DEFAULT_SELECTION_BORDER_TOP, DEFAULT_SELECTION_BORDER_LEFT, + DEFAULT_SELECTION_BORDER_BOTTOM, DEFAULT_SELECTION_BORDER_RIGHT + ), + originalBorder + ); + + super.setBorder(this.defaultBorder); + } + + @Override + public void setBorder(Border border) { + if (this.selectionBorder == null) { + // JTextField sets border in constructor before selectionBorder is initialized + super.setBorder(border); + } else { + this.defaultBorder = this.createDefaultBorder(border); + this.selectionBorder = createCompoundBorder(this.selectionBorder.getOutsideBorder(), border); + super.setBorder(this.defaultBorder); + } + } + + private CompoundBorder createDefaultBorder(Border border) { + final Border selectionOuter = this.selectionBorder.getOutsideBorder(); + if (selectionOuter == null) { + return createCompoundBorder(null, border); + } else { + final Insets selectionInsets = selectionOuter.getBorderInsets(this); + return createCompoundBorder( + createEmptyBorder(selectionInsets.top, selectionInsets.left, selectionInsets.bottom, selectionInsets.right), + border + ); + } + } + + public void setSelectionBorder(Border border) { + final Border insideBorder = this.defaultBorder.getInsideBorder(); + this.selectionBorder = createCompoundBorder(border, insideBorder); + this.defaultBorder = this.createDefaultBorder(insideBorder); + super.setBorder(this.defaultBorder); + } + + @Override + protected void paintBorder(Graphics graphics) { + if (this.selectionIncluded) { + final int leftInset; + final int topInset; + { + final Container parent = this.getParent(); + + if (parent == null) { + leftInset = 0; + topInset = 0; + } else { + final Insets insets = parent.getInsets(); + leftInset = insets.left; + topInset = insets.top; + } + } + + this.selectionBorder.paintBorder( + this, graphics, + this.getX() - leftInset, this.getY() - topInset, + this.getWidth(), this.getHeight() + ); + } else { + super.paintBorder(graphics); + } + } + + @Override + public void processMouseEvent(MouseEvent event, MenuElement[] path, MenuSelectionManager manager) { } + + @Override + public void processKeyEvent(KeyEvent event, MenuElement[] path, MenuSelectionManager manager) { } + + @Override + public void menuSelectionChanged(boolean isIncluded) { + if (this.selectionIncluded != isIncluded) { + this.selectionIncluded = isIncluded; + // update border + this.repaint(); + } + } + + @Override + public MenuElement[] getSubElements() { + return new MenuElement[0]; + } + + @Override + public Component getComponent() { + return this; + } +} diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java index 866750bd7..f037eb6fb 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java @@ -9,7 +9,6 @@ import org.quiltmc.enigma.gui.ConnectionState; import org.quiltmc.enigma.gui.Gui; import org.quiltmc.enigma.gui.config.Config; -import org.quiltmc.enigma.gui.element.PlaceheldTextField; import org.quiltmc.enigma.gui.util.GridBagConstraintsBuilder; import org.quiltmc.enigma.gui.util.GuiUtil; import org.quiltmc.enigma.gui.util.ScaleUtil; @@ -126,7 +125,7 @@ private static ImmutableList buildPathTo(MenuElement target) { @Nullable private final Border defaultPopupBorder; - private final PlaceheldTextField field = new PlaceheldTextField(); + private final PlaceheldMenuTextField field = new PlaceheldMenuTextField(); private final JMenuItem noResults = new JMenuItem(); private final HintItem viewHint = new HintItem( From b7630b8f993cc8b46449d73a70806d7e9285874e Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Thu, 27 Nov 2025 11:38:17 -0800 Subject: [PATCH 113/124] make placeholder non-null --- .../gui/element/PlaceheldTextField.java | 66 +++++++++++++++---- 1 file changed, 53 insertions(+), 13 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/PlaceheldTextField.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/PlaceheldTextField.java index db53b8e13..bb924f3d8 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/PlaceheldTextField.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/PlaceheldTextField.java @@ -18,7 +18,6 @@ public class PlaceheldTextField extends JTextField { protected static final int DEFAULT_COLUMNS = 0; - @Nullable protected Placeholder placeholder; @Nullable private Color placeholderColor; @@ -54,14 +53,14 @@ public PlaceheldTextField(String text, String placeholder) { */ public PlaceheldTextField(Document doc, String text, @Nullable String placeholder, int columns) { super(doc, text, columns); - this.placeholder = new Placeholder(placeholder); + this.setPlaceholder(placeholder); } @Override public Dimension getPreferredSize() { final Dimension size = super.getPreferredSize(); - if (this.placeholder != null) { + if (!this.placeholder.isEmpty()) { final Insets insets = this.getInsets(); size.width = Math.max(insets.left + this.placeholder.getWidth() + insets.right, size.width); @@ -74,7 +73,7 @@ public Dimension getPreferredSize() { protected void paintComponent(Graphics graphics) { super.paintComponent(graphics); - if (this.placeholder != null && this.getText().isEmpty()) { + if (!this.placeholder.isEmpty() && this.getText().isEmpty()) { GuiUtil.trySetRenderingHints(graphics); Utils.findFirstNonNull(this.placeholderColor, this.getDisabledTextColor(), this.getForeground()) @@ -83,7 +82,7 @@ protected void paintComponent(Graphics graphics) { final Insets insets = this.getInsets(); final int baseY = graphics.getFontMetrics().getMaxAscent() + insets.top; - graphics.drawString(this.placeholder.text, insets.left, baseY); + graphics.drawString(this.placeholder.getText(), insets.left, baseY); } } @@ -91,11 +90,13 @@ protected void paintComponent(Graphics graphics) { * @param placeholder the placeholder text for this field; if {@code null}, no placeholder will be shown */ public void setPlaceholder(@Nullable String placeholder) { - this.placeholder = placeholder == null ? null : new Placeholder(placeholder); + this.placeholder = placeholder == null || placeholder.isEmpty() + ? EmptyPlaceholder.INSTANCE + : new FullPlaceholder(placeholder); } public String getPlaceholder() { - return this.placeholder == null ? "" : this.placeholder.text; + return this.placeholder.getText(); } @Nullable @@ -115,23 +116,62 @@ public void setPlaceholderColor(@Nullable Color color) { public void setFont(Font f) { super.setFont(f); - if (this.placeholder != null) { - this.placeholder.clearWidth(); + // placeholder is null when the super constructor calls setFont + if (this.placeholder instanceof FullPlaceholder full) { + full.clearWidth(); } } - protected class Placeholder { + protected sealed interface Placeholder { + String getText(); + + boolean isEmpty(); + + int getWidth(); + } + + private static final class EmptyPlaceholder implements Placeholder { + static final EmptyPlaceholder INSTANCE = new EmptyPlaceholder(); + + @Override + public String getText() { + return ""; + } + + @Override + public boolean isEmpty() { + return true; + } + + @Override + public int getWidth() { + return 0; + } + } + + private final class FullPlaceholder implements Placeholder { static final int UNSET_WIDTH = -1; final String text; int width = UNSET_WIDTH; - Placeholder(String text) { + FullPlaceholder(String text) { this.text = text; } - int getWidth() { + @Override + public String getText() { + return this.text; + } + + @Override + public boolean isEmpty() { + return false; + } + + @Override + public int getWidth() { if (this.width < 0) { this.width = PlaceheldTextField.this .getFontMetrics(PlaceheldTextField.this.getFont()).stringWidth(this.text); @@ -140,7 +180,7 @@ int getWidth() { return this.width; } - void clearWidth() { + public void clearWidth() { this.width = UNSET_WIDTH; } } From ff22d96ce91951315b150b9c0cfb285356d5313e Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Thu, 27 Nov 2025 12:00:56 -0800 Subject: [PATCH 114/124] minor tweaks --- .../gui/element/PlaceheldTextField.java | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/PlaceheldTextField.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/PlaceheldTextField.java index bb924f3d8..27813734f 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/PlaceheldTextField.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/PlaceheldTextField.java @@ -18,7 +18,7 @@ public class PlaceheldTextField extends JTextField { protected static final int DEFAULT_COLUMNS = 0; - protected Placeholder placeholder; + private Placeholder placeholder; @Nullable private Color placeholderColor; @@ -60,7 +60,7 @@ public PlaceheldTextField(Document doc, String text, @Nullable String placeholde public Dimension getPreferredSize() { final Dimension size = super.getPreferredSize(); - if (!this.placeholder.isEmpty()) { + if (this.placeholder.isFull()) { final Insets insets = this.getInsets(); size.width = Math.max(insets.left + this.placeholder.getWidth() + insets.right, size.width); @@ -73,7 +73,7 @@ public Dimension getPreferredSize() { protected void paintComponent(Graphics graphics) { super.paintComponent(graphics); - if (!this.placeholder.isEmpty() && this.getText().isEmpty()) { + if (this.placeholder.isFull() && this.getText().isEmpty()) { GuiUtil.trySetRenderingHints(graphics); Utils.findFirstNonNull(this.placeholderColor, this.getDisabledTextColor(), this.getForeground()) @@ -99,9 +99,8 @@ public String getPlaceholder() { return this.placeholder.getText(); } - @Nullable - protected Placeholder getPlaceholderObject() { - return this.placeholder; + protected int getPlaceholderWidth() { + return this.placeholder.getWidth(); } /** @@ -112,6 +111,11 @@ public void setPlaceholderColor(@Nullable Color color) { this.placeholderColor = color; } + @Nullable + public Color getPlaceholderColor() { + return this.placeholderColor; + } + @Override public void setFont(Font f) { super.setFont(f); @@ -122,10 +126,10 @@ public void setFont(Font f) { } } - protected sealed interface Placeholder { + private sealed interface Placeholder { String getText(); - boolean isEmpty(); + boolean isFull(); int getWidth(); } @@ -139,8 +143,8 @@ public String getText() { } @Override - public boolean isEmpty() { - return true; + public boolean isFull() { + return false; } @Override @@ -166,8 +170,8 @@ public String getText() { } @Override - public boolean isEmpty() { - return false; + public boolean isFull() { + return true; } @Override From 156dc96cbd53c54cadb8a032b8dffc5a9ffd7ce1 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Thu, 27 Nov 2025 12:21:17 -0800 Subject: [PATCH 115/124] inversely scale MAX_INITIAL_RESULTS when scale is greater than 1 --- .../gui/element/menu_bar/SearchMenusMenu.java | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java index f037eb6fb..1206abe2c 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java @@ -11,7 +11,6 @@ import org.quiltmc.enigma.gui.config.Config; import org.quiltmc.enigma.gui.util.GridBagConstraintsBuilder; import org.quiltmc.enigma.gui.util.GuiUtil; -import org.quiltmc.enigma.gui.util.ScaleUtil; import org.quiltmc.enigma.util.I18n; import org.quiltmc.enigma.util.multi_trie.CompositeStringMultiTrie; import org.quiltmc.enigma.util.multi_trie.EmptyStringMultiTrie; @@ -67,7 +66,16 @@ import static javax.swing.BorderFactory.createEmptyBorder; public class SearchMenusMenu extends AbstractEnigmaMenu { - private static final int MAX_INITIAL_RESULTS = 20; + private static final int MAX_INITIAL_RESULTS; + + static { + // this is a heuristic that tries to make the initial results fit on screen + // this is to avoid the laggy creation of a scrolling heavy-weight popup while typing + // I (sss) tried measuring items as they were added and stopping when no more would fit, but that also lagged. + final int maxMaxInitialResults = 20; + final Float scale = Config.main().scaleFactor.value(); + MAX_INITIAL_RESULTS = scale > 1 ? (int) (maxMaxInitialResults / scale) : maxMaxInitialResults; + } /** * @return a breadth-first stream of the passed {@code root} element and all of its sub-elements, @@ -157,7 +165,6 @@ protected SearchMenusMenu(Gui gui) { this.addPermanentChildren(); - this.field.setFont(ScaleUtil.scaleFont(this.field.getFont())); // Always focus field, but don't always select its text, because it loses focus when packing new search results. this.field.addHierarchyListener(new HierarchyListener() { @Nullable @@ -244,7 +251,9 @@ private void updateResultItems() { this.viewHint.configureVisibility(); this.chooseHint.configureVisibility(); - // truncate results because the popup lags when packing numerous items, which can cause keystroke drops + // truncate results because the popup lags when packing numerous items + // - especially when a scrolling heavy-weight popup is required - + // which can cause keystroke drops int remainingItems = MAX_INITIAL_RESULTS; final UnmodifiableIterator prefixItr = different.prefixItems.iterator(); while (prefixItr.hasNext() && remainingItems > 0) { From 6c842d1cd3e632c162e350b095f4161957dd46ff Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Thu, 27 Nov 2025 13:44:17 -0800 Subject: [PATCH 116/124] don't truncate only re-show popup when shrinking stop scaling field font (menu item fonts are automatically scaled) --- .../gui/element/menu_bar/SearchMenusMenu.java | 77 ++----------------- 1 file changed, 8 insertions(+), 69 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java index 1206abe2c..9f3b723b6 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java @@ -2,7 +2,6 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; -import com.google.common.collect.UnmodifiableIterator; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; import org.quiltmc.config.api.values.TrackedValue; @@ -66,17 +65,6 @@ import static javax.swing.BorderFactory.createEmptyBorder; public class SearchMenusMenu extends AbstractEnigmaMenu { - private static final int MAX_INITIAL_RESULTS; - - static { - // this is a heuristic that tries to make the initial results fit on screen - // this is to avoid the laggy creation of a scrolling heavy-weight popup while typing - // I (sss) tried measuring items as they were added and stopping when no more would fit, but that also lagged. - final int maxMaxInitialResults = 20; - final Float scale = Config.main().scaleFactor.value(); - MAX_INITIAL_RESULTS = scale > 1 ? (int) (maxMaxInitialResults / scale) : maxMaxInitialResults; - } - /** * @return a breadth-first stream of the passed {@code root} element and all of its sub-elements, * excluding the {@link HelpMenu} and its sub-elements; the help menu is not searchable because it must be open @@ -251,36 +239,14 @@ private void updateResultItems() { this.viewHint.configureVisibility(); this.chooseHint.configureVisibility(); - // truncate results because the popup lags when packing numerous items - // - especially when a scrolling heavy-weight popup is required - - // which can cause keystroke drops - int remainingItems = MAX_INITIAL_RESULTS; - final UnmodifiableIterator prefixItr = different.prefixItems.iterator(); - while (prefixItr.hasNext() && remainingItems > 0) { - this.add(prefixItr.next()); - remainingItems--; - } + different.prefixItems.forEach(this::add); - final UnmodifiableIterator containingItr = different.containingItems.iterator(); - if (remainingItems > 0 && containingItr.hasNext()) { + if (!different.containingItems.isEmpty()) { if (!different.prefixItems.isEmpty()) { this.add(new JPopupMenu.Separator()); } - while (containingItr.hasNext() && remainingItems > 0) { - this.add(containingItr.next()); - remainingItems--; - } - } - - if (different.getSize() > MAX_INITIAL_RESULTS) { - final ImmutableList.Builder truncatedPrefixResults = ImmutableList.builder(); - prefixItr.forEachRemaining(truncatedPrefixResults::add); - - final ImmutableList.Builder truncatedContainingResults = ImmutableList.builder(); - containingItr.forEachRemaining(truncatedContainingResults::add); - - this.add(this.moreButtonOf(truncatedPrefixResults.build(), truncatedContainingResults.build())); + different.containingItems.forEach(this::add); } this.refreshPopup(); @@ -291,14 +257,16 @@ private void refreshPopup() { if (this.isShowing()) { final JPopupMenu popupMenu = this.getPopupMenu(); + final int oldHeight = popupMenu.getHeight(); // HACK: When popups are resizing in limited space, they may remove their borders. // The border won't be restored when re-packing or showing, so manually restore the original border here. popupMenu.setBorder(this.defaultPopupBorder); popupMenu.pack(); - // only re-show - // the initial showing from JMenu does the same thing and would cause an SOE if we also showed here - if (popupMenu.isShowing()) { + // re-show if shrinking to move the popup back down in case it had to be shifted up to fit items + // re-showing can also result in dropped keystrokes; do so as infrequently as possible + // note: the initial showing from JMenu would cause an SOE if we also showed here for the initial showing + if (popupMenu.getHeight() < oldHeight && popupMenu.isShowing()) { final Point newOrigin = this.getPopupMenuOrigin(); popupMenu.show(this, newOrigin.x, newOrigin.y); } @@ -343,35 +311,6 @@ public void retranslate() { this.noResults.setText(I18n.translate("menu.help.search.no_results")); } - private JMenuItem moreButtonOf( - ImmutableList prefixItems, - ImmutableList containingItems - ) { - final var button = new JMenuItem(); - - button.setText("⋯"); - - button.addActionListener(e -> { - this.remove(button); - - prefixItems.forEach(this::add); - - if (!prefixItems.isEmpty() && !containingItems.isEmpty()) { - this.add(new JPopupMenu.Separator()); - } - - containingItems.forEach(this::add); - - // clicking the button closes the menu - // this re-opens it (which includes re-packing and selecting the search field) - this.doClick(0); - // de-select and move caret to end - this.field.setSelectionStart(Integer.MAX_VALUE); - }); - - return button; - } - private static final class Lookup { static final int NON_PREFIX_START = 1; static final int MAX_SUBSTRING_LENGTH = 2; From e1f942343232cf00b880377179a6813b41923627 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Thu, 27 Nov 2025 14:58:35 -0800 Subject: [PATCH 117/124] give PlaceheldMenuTextField a preferred min height no less than a JMenuItem to fix positioning at low scales --- .../gui/element/PlaceheldTextField.java | 5 +++- .../menu_bar/PlaceheldMenuTextField.java | 23 +++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/PlaceheldTextField.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/PlaceheldTextField.java index 27813734f..19684d46f 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/PlaceheldTextField.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/PlaceheldTextField.java @@ -81,7 +81,10 @@ protected void paintComponent(Graphics graphics) { graphics.setFont(this.getFont()); final Insets insets = this.getInsets(); - final int baseY = graphics.getFontMetrics().getMaxAscent() + insets.top; + // HACK to keep the text vertically centered when subclasses adjust preferred height + final int extraTop = (this.getPreferredSize().height - super.getPreferredSize().height) / 2; + final int baseY = graphics.getFontMetrics().getMaxAscent() + insets.top + extraTop; + graphics.drawString(this.placeholder.getText(), insets.left, baseY); } } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/PlaceheldMenuTextField.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/PlaceheldMenuTextField.java index 4f326c45d..2c0e4fc89 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/PlaceheldMenuTextField.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/PlaceheldMenuTextField.java @@ -4,6 +4,7 @@ import org.quiltmc.enigma.gui.element.PlaceheldTextField; import org.quiltmc.enigma.gui.util.ScaleUtil; +import javax.swing.JMenuItem; import javax.swing.MenuElement; import javax.swing.MenuSelectionManager; import javax.swing.UIManager; @@ -13,6 +14,7 @@ import javax.swing.text.Document; import java.awt.Component; import java.awt.Container; +import java.awt.Dimension; import java.awt.Graphics; import java.awt.Insets; import java.awt.event.KeyEvent; @@ -38,6 +40,8 @@ public class PlaceheldMenuTextField extends PlaceheldTextField implements MenuEl private boolean selectionIncluded; + private int minHeight = -1; + /** * @see PlaceheldTextField#PlaceheldTextField() PlaceheldTextField */ @@ -165,4 +169,23 @@ public MenuElement[] getSubElements() { public Component getComponent() { return this; } + + @Override + public Dimension getPreferredSize() { + final Dimension size = super.getPreferredSize(); + + size.height = Math.max(size.height, this.getMinHeight()); + + return size; + } + + private int getMinHeight() { + if (this.minHeight < 0) { + // HACK: have at least the height of a menu item + // this fixes containing popup menus' positions being off at small scales when this is the only item + this.minHeight = new JMenuItem().getPreferredSize().height; + } + + return this.minHeight; + } } From d7dfaa19b2a38114cb13f09291131a686005312e Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Thu, 27 Nov 2025 15:37:29 -0800 Subject: [PATCH 118/124] add and improve javadocs give SearchableElement#streamSearchAliases a default implementation --- .../AbstractSearchableEnigmaMenu.java | 5 +++ .../ConventionalSearchableElement.java | 15 +++---- .../gui/element/menu_bar/DecompilerMenu.java | 6 --- .../element/menu_bar/SearchableElement.java | 42 ++++++++++++++++++- 4 files changed, 54 insertions(+), 14 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/AbstractSearchableEnigmaMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/AbstractSearchableEnigmaMenu.java index 9b7090385..5e4c68b4b 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/AbstractSearchableEnigmaMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/AbstractSearchableEnigmaMenu.java @@ -2,6 +2,11 @@ import org.quiltmc.enigma.gui.Gui; +/** + * A base {@link EnigmaMenu} implementation for menus that can appear in {@link SearchMenusMenu} search results. + * + *

    In most cases, children should be {@link SearchableElement}s. + */ public abstract class AbstractSearchableEnigmaMenu extends AbstractEnigmaMenu implements ConventionalSearchableElement { protected AbstractSearchableEnigmaMenu(Gui gui) { super(gui); diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/ConventionalSearchableElement.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/ConventionalSearchableElement.java index 13a54e177..95badd688 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/ConventionalSearchableElement.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/ConventionalSearchableElement.java @@ -2,13 +2,20 @@ import java.util.stream.Stream; +/** + * A {@link SearchableElement} that loads {@linkplain #streamSearchAliases() search aliases} from translations using + * {@link #translateExtraAliases(String)}. + * + *

    Aliases are loaded from the translation key formed by appending {@value ALIASES_SUFFIX} to the + * {@linkplain #getAliasesTranslationKeyPrefix() key prefix}.
    + */ public interface ConventionalSearchableElement extends SearchableElement { String ALIASES_SUFFIX = ".aliases"; @Override default Stream streamSearchAliases() { return Stream.concat( - Stream.of(this.getSearchName()), + SearchableElement.super.streamSearchAliases(), SearchableElement.translateExtraAliases(this.getAliasesTranslationKeyPrefix() + ALIASES_SUFFIX) ); } @@ -16,12 +23,6 @@ default Stream streamSearchAliases() { /** * Returns a translation key prefix used to retrieve translatable search aliases.
    * Usually the prefix is the translation key of the translatable element. - * - *

    {@value ALIASES_SUFFIX} is appended to create the complete translation key.
    - * Alias translations hold multiple aliases separated by {@value ALIAS_DELIMITER}. - * - *

    All alias translation key prefixes should be documented in {@code CONTRIBUTING.md} under
    - * {@code Translating > Search Aliases > Complete list of search alias translation keys} */ String getAliasesTranslationKeyPrefix(); } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/DecompilerMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/DecompilerMenu.java index 19d1ecb3b..b6d897842 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/DecompilerMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/DecompilerMenu.java @@ -8,7 +8,6 @@ import javax.swing.ButtonGroup; import javax.swing.JRadioButtonMenuItem; -import java.util.stream.Stream; public class DecompilerMenu extends AbstractSearchableEnigmaMenu { private static final String TRANSLATION_KEY = "menu.decompiler"; @@ -57,11 +56,6 @@ private static final class DecompilerItem extends JRadioButtonMenuItem implement super(name); } - @Override - public Stream streamSearchAliases() { - return Stream.of(this.getText()); - } - @Override public String getSearchName() { return this.getText(); diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchableElement.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchableElement.java index 396d2f6f8..3acb56b0c 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchableElement.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchableElement.java @@ -2,22 +2,62 @@ import org.quiltmc.enigma.util.I18n; +import javax.swing.AbstractButton; import javax.swing.MenuElement; import java.util.Arrays; import java.util.stream.Stream; +/** + * A menu element that can appear as a search result in {@link SearchMenusMenu}. + * + *

    All alias translation keys passed to {@link #translateExtraAliases(String)} should be documented in + * {@code CONTRIBUTING.md} under
    + * {@code Translating > Search Aliases > Complete list of search alias translation keys}
    + * Alias translations are often absent from language files, so keeping the table populated is essential for + * discoverability for translators. + * + *

    {@link ConventionalSearchableElement} adds a convention for obtaining + * {@linkplain #streamSearchAliases() search aliases} using {@link #translateExtraAliases(String)}. + * + * @see SimpleItem + * @see SimpleCheckBoxItem + * @see SimpleRadioItem + */ public interface SearchableElement extends MenuElement { String ALIAS_DELIMITER = ";"; + /** + * Loads a {@value #ALIAS_DELIMITER}-separated list of aliases from the passed {@code translationKey}. + * + *

    Having no entry for {@code translationKey} in a language file is common; + * it means the element has no aliases in that language. + * + * @see ConventionalSearchableElement + */ static Stream translateExtraAliases(String translationKey) { final String aliases = I18n.translateOrNull(translationKey); return aliases == null ? Stream.empty() : Arrays.stream(aliases.split(ALIAS_DELIMITER)); } - Stream streamSearchAliases(); + /** + * @return a finite, ordered {@link Stream} of aliases that this element can be searched by + * + * @implSpec the {@linkplain #getSearchName() search name} should always be included first + * (unless this element is not currently searchable, in which case an empty stream may be returned) + */ + default Stream streamSearchAliases() { + return Stream.of(this.getSearchName()); + } + /** + * @return the name of this element uses in search results; usually {@link AbstractButton#getText()} + */ String getSearchName(); + /** + * Called when this element's search result is chosen.
    + * Most implementations call {@link AbstractButton#doClick(int) doClick(0)} + */ void onSearchChosen(); } From 337d77342cefc0aa3a2451989b5e5802c1fd0452 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Thu, 27 Nov 2025 16:00:15 -0800 Subject: [PATCH 119/124] add "Dismiss" tooltip to hint dismiss buttons make hint dismiss buttons slightly smaller adjust hint padding --- .../enigma/gui/element/menu_bar/SearchMenusMenu.java | 9 ++++++--- enigma/src/main/resources/lang/en_us.json | 1 + 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java index 9f3b723b6..ee2f46fd0 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java @@ -868,6 +868,8 @@ record RestorablePath(SearchableElement searched, MenuElement[] helpPath) { } // not a MenuElement so it can't be selected private class HintItem extends JPanel implements Retranslatable { + static final int PAD = 3; + final String translationKey; final TrackedValue config; @@ -879,14 +881,14 @@ private class HintItem extends JPanel implements Retranslatable { this.translationKey = translationKey; this.config = config; - this.setBorder(createEmptyBorder(0, 2, 0, 0)); + this.setBorder(createEmptyBorder(0, PAD, 0, PAD)); this.setLayout(new GridBagLayout()); this.add(this.infoIndicator); final var spacer = Box.createHorizontalBox(); - spacer.setPreferredSize(new Dimension(3, 1)); + spacer.setPreferredSize(new Dimension(PAD, 1)); this.add(spacer); final Font oldHintFont = this.hint.getFont(); @@ -901,7 +903,7 @@ private class HintItem extends JPanel implements Retranslatable { this.dismissButton.setBackground(new Color(0, true)); this.dismissButton.setMargin(new Insets(0, 0, 0, 0)); final Font oldDismissFont = this.dismissButton.getFont(); - this.dismissButton.setFont(oldDismissFont.deriveFont(oldDismissFont.getSize2D() * 1.5f)); + this.dismissButton.setFont(oldDismissFont.deriveFont(oldDismissFont.getSize2D() * 1.3f)); this.dismissButton.addActionListener(e -> this.dismiss()); this.add(this.dismissButton); @@ -921,6 +923,7 @@ void configureVisibility() { @Override public void retranslate() { this.hint.setText(I18n.translate(this.translationKey)); + this.dismissButton.setToolTipText(I18n.translate("prompt.dismiss")); } } } diff --git a/enigma/src/main/resources/lang/en_us.json b/enigma/src/main/resources/lang/en_us.json index 25ef8936a..b7a162ae9 100644 --- a/enigma/src/main/resources/lang/en_us.json +++ b/enigma/src/main/resources/lang/en_us.json @@ -278,6 +278,7 @@ "prompt.open": "Open", "prompt.error": "Error", "prompt.invalid_input": "Invalid input", + "prompt.dismiss": "Dismiss", "prompt.search.classes": "Classes", "prompt.search.methods": "Methods", From 454300890239923e9fd8d10e4ab81e074f4b4baa Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Thu, 27 Nov 2025 18:41:50 -0800 Subject: [PATCH 120/124] select SearchMenusMenu.field from a MenuSelectionManager ChangeListener because setting it from a HeirarchyListener was buggy (when restoring after viewing, couldn't select with arrow keys; field got duplicated in path) --- .../gui/element/menu_bar/SearchMenusMenu.java | 68 ++++++++++--------- .../java/org/quiltmc/enigma/util/Utils.java | 10 +++ 2 files changed, 45 insertions(+), 33 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java index ee2f46fd0..76d7b9dc1 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java @@ -30,6 +30,8 @@ import javax.swing.MenuSelectionManager; import javax.swing.SwingUtilities; import javax.swing.border.Border; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import javax.swing.event.PopupMenuEvent; @@ -47,8 +49,6 @@ import java.awt.Toolkit; import java.awt.Window; import java.awt.event.AWTEventListener; -import java.awt.event.HierarchyEvent; -import java.awt.event.HierarchyListener; import java.awt.event.InputEvent; import java.awt.event.KeyEvent; import java.util.Arrays; @@ -63,6 +63,7 @@ import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.collect.ImmutableMap.toImmutableMap; import static javax.swing.BorderFactory.createEmptyBorder; +import static org.quiltmc.enigma.util.Utils.getLastOrNull; public class SearchMenusMenu extends AbstractEnigmaMenu { /** @@ -153,8 +154,7 @@ protected SearchMenusMenu(Gui gui) { this.addPermanentChildren(); - // Always focus field, but don't always select its text, because it loses focus when packing new search results. - this.field.addHierarchyListener(new HierarchyListener() { + MenuSelectionManager.defaultManager().addChangeListener(new ChangeListener() { @Nullable ImmutableList fieldPath; @@ -167,19 +167,28 @@ ImmutableList getFieldPath() { } @Override - public void hierarchyChanged(HierarchyEvent e) { + public void stateChanged(ChangeEvent e) { if (SearchMenusMenu.this.field.isShowing()) { - final Window window = SwingUtilities.getWindowAncestor(SearchMenusMenu.this.field); - if (window != null && window.getType() == Window.Type.POPUP) { - // HACK: if PopupFactory::fitsOnScreen is false for light- and medium-weight popups, it makes a - // heavy-weight popup instead, whose HeavyWeightWindow component is by default is not focusable. - // It prevented this.field from focusing and receiving input. - window.setFocusableWindowState(true); + final var manager = (MenuSelectionManager) e.getSource(); + if (getLastOrNull(manager.getSelectedPath()) == SearchMenusMenu.this.getPopupMenu()) { + manager.setSelectedPath(this.getFieldPath().toArray(new MenuElement[0])); } + } + } + }); - SearchMenusMenu.this.field.requestFocus(); - MenuSelectionManager.defaultManager().setSelectedPath(this.getFieldPath().toArray(new MenuElement[0])); + // Always focus field, but don't always select its text, because it loses focus when packing new search results. + this.field.addHierarchyListener(e -> { + if (this.field.isShowing()) { + final Window window = SwingUtilities.getWindowAncestor(this.field); + if (window != null && window.getType() == Window.Type.POPUP) { + // HACK: if PopupFactory::fitsOnScreen is false for light- and medium-weight popups, it makes a + // heavy-weight popup instead, whose HeavyWeightWindow component is by default is not focusable. + // It prevented this.field from focusing and receiving input. + window.setFocusableWindowState(true); } + + this.field.requestFocus(); } }); @@ -263,7 +272,7 @@ private void refreshPopup() { popupMenu.setBorder(this.defaultPopupBorder); popupMenu.pack(); - // re-show if shrinking to move the popup back down in case it had to be shifted up to fit items + // HACK: re-show if shrinking to move the popup back down in case it had to be shifted up to fit items // re-showing can also result in dropped keystrokes; do so as infrequently as possible // note: the initial showing from JMenu would cause an SOE if we also showed here for the initial showing if (popupMenu.getHeight() < oldHeight && popupMenu.isShowing()) { @@ -620,7 +629,7 @@ public boolean equals(Object o) { } class Item extends JMenuItem { - private final ImmutableList path; + final ImmutableList searchablePath; Item(String searchName) { super(searchName); @@ -629,10 +638,10 @@ class Item extends JMenuItem { clearSelectionAndChoose(Result.this.searchable, MenuSelectionManager.defaultManager()); }); - this.path = buildPathTo(this.getSearchable()); + this.searchablePath = buildPathTo(this.getSearchable()); - if (!this.path.isEmpty()) { - final String pathText = this.path.stream() + if (!this.searchablePath.isEmpty()) { + final String pathText = this.searchablePath.stream() .flatMap(element -> { if (element instanceof SearchableElement searchableElement) { return Stream.of(searchableElement.getSearchName()); @@ -667,13 +676,13 @@ ItemHolder getHolder() { return ItemHolder.this; } - void selectSearchable() { - if (!this.path.isEmpty()) { - MenuSelectionManager.defaultManager().setSelectedPath(this.path.toArray(new MenuElement[0])); + void selectSearchable(MenuSelectionManager manager) { + if (!this.searchablePath.isEmpty()) { + manager.setSelectedPath(this.searchablePath.toArray(new MenuElement[0])); } } - private SearchableElement getSearchable() { + SearchableElement getSearchable() { return this.getHolder().getResult().searchable; } } @@ -792,15 +801,6 @@ private class KeyHandler implements AWTEventListener { static final int PREVIEW_MODIFIER_MASK = InputEvent.SHIFT_DOWN_MASK; static final int PREVIEW_MODIFIER_KEY = KeyEvent.VK_SHIFT; - @Nullable - static T getLastOrNull(T[] array) { - if (array.length > 0) { - return array[array.length - 1]; - } else { - return null; - } - } - @Nullable RestorablePath restorablePath; @@ -810,7 +810,8 @@ public void eventDispatched(AWTEvent e) { if (keyEvent.getID() == KeyEvent.KEY_PRESSED) { final int keyCode = keyEvent.getKeyCode(); if (keyCode == PREVIEW_MODIFIER_KEY && keyEvent.getModifiersEx() == PREVIEW_MODIFIER_MASK) { - final MenuElement[] selectedPath = MenuSelectionManager.defaultManager().getSelectedPath(); + final MenuSelectionManager manager = MenuSelectionManager.defaultManager(); + final MenuElement[] selectedPath = manager.getSelectedPath(); final MenuElement selected = getLastOrNull(selectedPath); if (selected != null) { @@ -819,7 +820,7 @@ public void eventDispatched(AWTEvent e) { this.restorablePath = new RestorablePath(item.getSearchable(), selectedPath); - item.selectSearchable(); + item.selectSearchable(manager); return; } else if (this.restorablePath != null && this.restorablePath.searched == selected) { @@ -849,6 +850,7 @@ public void eventDispatched(AWTEvent e) { if (keyEvent.getKeyCode() == PREVIEW_MODIFIER_KEY) { if (keyEvent.getModifiersEx() == 0) { MenuSelectionManager.defaultManager().setSelectedPath(this.restorablePath.helpPath); + this.restorablePath = null; } } else { this.restorablePath = null; diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/Utils.java b/enigma/src/main/java/org/quiltmc/enigma/util/Utils.java index fd9404543..b7dca8203 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/Utils.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/Utils.java @@ -2,6 +2,7 @@ import com.google.common.io.CharStreams; import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; import java.io.IOException; import java.io.InputStream; @@ -216,4 +217,13 @@ public static Optional findFirstNonNull(T... values) { return Optional.empty(); } + + /** + * @return {@code null} if the passed {@code array} is {@code null} or empty, + * or the last element of the {@code array} otherwise + */ + @Nullable + public static T getLastOrNull(@Nullable T[] array) { + return array == null || array.length == 0 ? null : array[array.length - 1]; + } } From 72296062a0383cba86d69b3999f8935e51056f7e Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Thu, 27 Nov 2025 19:03:30 -0800 Subject: [PATCH 121/124] extract EMPTY_MENU_ELEMENTS comment bug fix --- .../element/menu_bar/PlaceheldMenuTextField.java | 3 ++- .../gui/element/menu_bar/SearchMenusMenu.java | 13 ++++++++++--- .../java/org/quiltmc/enigma/gui/util/GuiUtil.java | 3 +++ 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/PlaceheldMenuTextField.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/PlaceheldMenuTextField.java index 2c0e4fc89..1f354dc46 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/PlaceheldMenuTextField.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/PlaceheldMenuTextField.java @@ -20,6 +20,7 @@ import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; +import static org.quiltmc.enigma.gui.util.GuiUtil.EMPTY_MENU_ELEMENTS; import static javax.swing.BorderFactory.createCompoundBorder; import static javax.swing.BorderFactory.createEmptyBorder; @@ -162,7 +163,7 @@ public void menuSelectionChanged(boolean isIncluded) { @Override public MenuElement[] getSubElements() { - return new MenuElement[0]; + return EMPTY_MENU_ELEMENTS; } @Override diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java index 76d7b9dc1..66d4994a5 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java @@ -62,8 +62,9 @@ import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.collect.ImmutableMap.toImmutableMap; -import static javax.swing.BorderFactory.createEmptyBorder; +import static org.quiltmc.enigma.gui.util.GuiUtil.EMPTY_MENU_ELEMENTS; import static org.quiltmc.enigma.util.Utils.getLastOrNull; +import static javax.swing.BorderFactory.createEmptyBorder; public class SearchMenusMenu extends AbstractEnigmaMenu { /** @@ -171,7 +172,13 @@ public void stateChanged(ChangeEvent e) { if (SearchMenusMenu.this.field.isShowing()) { final var manager = (MenuSelectionManager) e.getSource(); if (getLastOrNull(manager.getSelectedPath()) == SearchMenusMenu.this.getPopupMenu()) { - manager.setSelectedPath(this.getFieldPath().toArray(new MenuElement[0])); + // select here instead of in the hierarchy listener below because: + // 1. the manager doesn't report the final path in a hierarchy listener + // 2. selecting from the hierarchy listener caused bugs when restoring after showing a result: + // - this.field and the restored item both appeared selected, but arrow keys couldn't select + // - the final selected path got an increasing number of duplicates of this.field; + // none should have been in the path + manager.setSelectedPath(this.getFieldPath().toArray(EMPTY_MENU_ELEMENTS)); } } } @@ -678,7 +685,7 @@ ItemHolder getHolder() { void selectSearchable(MenuSelectionManager manager) { if (!this.searchablePath.isEmpty()) { - manager.setSelectedPath(this.searchablePath.toArray(new MenuElement[0])); + manager.setSelectedPath(this.searchablePath.toArray(EMPTY_MENU_ELEMENTS)); } } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/GuiUtil.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/GuiUtil.java index 4a754d972..0e7827d93 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/GuiUtil.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/GuiUtil.java @@ -29,6 +29,7 @@ import javax.swing.JToolTip; import javax.swing.JTree; import javax.swing.KeyStroke; +import javax.swing.MenuElement; import javax.swing.Popup; import javax.swing.PopupFactory; import javax.swing.Timer; @@ -101,6 +102,8 @@ private GuiUtil() { public static final Icon CHEVRON_UP_WHITE = loadIcon("chevron-up-white"); public static final Icon CHEVRON_DOWN_WHITE = loadIcon("chevron-down-white"); + public static final MenuElement[] EMPTY_MENU_ELEMENTS = new MenuElement[0]; + private static final String DESKTOP_FONT_HINTS_KEY = "awt.font.desktophints"; @Nullable From 0e449a00b964c8bc831e56aed06c996f25a1e2e2 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Thu, 27 Nov 2025 19:09:38 -0800 Subject: [PATCH 122/124] remove unused method --- .../quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java index 66d4994a5..368b41d1b 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java @@ -797,10 +797,6 @@ static Different of(Lookup.ResultCache cache) { boolean isEmpty() { return this.prefixItems.isEmpty() && this.containingItems.isEmpty(); } - - int getSize() { - return this.prefixItems.size() + this.containingItems.size(); - } } } From 435e0a488d47219cd84602e18cd3019db39aa34f Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Thu, 4 Dec 2025 12:54:02 -0800 Subject: [PATCH 123/124] copy and dispose graphics in custom paint implementations --- .../enigma/gui/element/PlaceheldTextField.java | 13 ++++++++----- .../gui/element/menu_bar/SearchMenusMenu.java | 13 ++++++++----- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/PlaceheldTextField.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/PlaceheldTextField.java index 19684d46f..5b8f23492 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/PlaceheldTextField.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/PlaceheldTextField.java @@ -74,18 +74,21 @@ protected void paintComponent(Graphics graphics) { super.paintComponent(graphics); if (this.placeholder.isFull() && this.getText().isEmpty()) { - GuiUtil.trySetRenderingHints(graphics); + final Graphics disposableGraphics = graphics.create(); + GuiUtil.trySetRenderingHints(disposableGraphics); Utils.findFirstNonNull(this.placeholderColor, this.getDisabledTextColor(), this.getForeground()) - .ifPresent(graphics::setColor); - graphics.setFont(this.getFont()); + .ifPresent(disposableGraphics::setColor); + disposableGraphics.setFont(this.getFont()); final Insets insets = this.getInsets(); // HACK to keep the text vertically centered when subclasses adjust preferred height final int extraTop = (this.getPreferredSize().height - super.getPreferredSize().height) / 2; - final int baseY = graphics.getFontMetrics().getMaxAscent() + insets.top + extraTop; + final int baseY = disposableGraphics.getFontMetrics().getMaxAscent() + insets.top + extraTop; - graphics.drawString(this.placeholder.getText(), insets.left, baseY); + disposableGraphics.drawString(this.placeholder.getText(), insets.left, baseY); + + disposableGraphics.dispose(); } } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java index 368b41d1b..8c94b1102 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java @@ -747,20 +747,23 @@ public Dimension getPreferredSize() { public void paint(Graphics graphics) { super.paint(graphics); - GuiUtil.trySetRenderingHints(graphics); + final Graphics disposableGraphics = graphics.create(); + GuiUtil.trySetRenderingHints(disposableGraphics); final Color color = this.getForeground(); if (color != null) { - graphics.setColor(color); + disposableGraphics.setColor(color); } final Font aliasFont = this.getAliasFont(); if (aliasFont != null) { - graphics.setFont(aliasFont); + disposableGraphics.setFont(aliasFont); } final Insets insets = this.getInsets(); - final int baseY = graphics.getFontMetrics().getMaxAscent() + insets.top; - graphics.drawString(this.alias, this.getWidth() - insets.right - this.getAliasWidth(), baseY); + final int baseY = disposableGraphics.getFontMetrics().getMaxAscent() + insets.top; + disposableGraphics.drawString(this.alias, this.getWidth() - insets.right - this.getAliasWidth(), baseY); + + disposableGraphics.dispose(); } int getAliasWidth() { From 9972923114df345399ca5e7c5ab612da6b4c3c49 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Tue, 9 Dec 2025 19:13:16 -0800 Subject: [PATCH 124/124] menu-select field when content or non-initial text-selection change so shift capitalizes instead of viewing --- .../gui/element/menu_bar/SearchMenusMenu.java | 88 +++++++++++++------ 1 file changed, 61 insertions(+), 27 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java index 8c94b1102..87022e6a4 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenusMenu.java @@ -30,7 +30,6 @@ import javax.swing.MenuSelectionManager; import javax.swing.SwingUtilities; import javax.swing.border.Border; -import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; @@ -135,9 +134,20 @@ private static ImmutableList buildPathTo(MenuElement target) { Config.main().searchMenus.showChooseHint ); + /** + * Lazily populated cache. + * + * @see #getLookup() + */ @Nullable private Lookup lookup; + /** + * Lazily populated by {@link #getFieldPath()} + */ + @Nullable + ImmutableList fieldPath; + protected SearchMenusMenu(Gui gui) { super(gui); @@ -155,31 +165,17 @@ protected SearchMenusMenu(Gui gui) { this.addPermanentChildren(); - MenuSelectionManager.defaultManager().addChangeListener(new ChangeListener() { - @Nullable - ImmutableList fieldPath; - - ImmutableList getFieldPath() { - if (this.fieldPath == null) { - this.fieldPath = buildPathTo(SearchMenusMenu.this.field); - } - - return this.fieldPath; - } - - @Override - public void stateChanged(ChangeEvent e) { - if (SearchMenusMenu.this.field.isShowing()) { - final var manager = (MenuSelectionManager) e.getSource(); - if (getLastOrNull(manager.getSelectedPath()) == SearchMenusMenu.this.getPopupMenu()) { - // select here instead of in the hierarchy listener below because: - // 1. the manager doesn't report the final path in a hierarchy listener - // 2. selecting from the hierarchy listener caused bugs when restoring after showing a result: - // - this.field and the restored item both appeared selected, but arrow keys couldn't select - // - the final selected path got an increasing number of duplicates of this.field; - // none should have been in the path - manager.setSelectedPath(this.getFieldPath().toArray(EMPTY_MENU_ELEMENTS)); - } + MenuSelectionManager.defaultManager().addChangeListener(e -> { + if (this.field.isShowing()) { + final var manager = (MenuSelectionManager) e.getSource(); + if (getLastOrNull(manager.getSelectedPath()) == this.getPopupMenu()) { + // select here instead of in the hierarchy listener below because: + // 1. the manager doesn't report the final path in a hierarchy listener + // 2. selecting from the hierarchy listener caused bugs when restoring after showing a result: + // - this.field and the restored item both appeared selected, but arrow keys couldn't select + // - the final selected path got an increasing number of duplicates of this.field; + // none should have been in the path + this.selectField(manager); } } }); @@ -199,16 +195,42 @@ public void stateChanged(ChangeEvent e) { } }); + // select field on content change so shift capitalizes instead of viewing selection + this.field.getDocument().addDocumentListener(new DocumentListener() { + @Override + public void insertUpdate(DocumentEvent e) { + SearchMenusMenu.this.selectField(MenuSelectionManager.defaultManager()); + } + + @Override + public void removeUpdate(DocumentEvent e) { + SearchMenusMenu.this.selectField(MenuSelectionManager.defaultManager()); + } + + @Override + public void changedUpdate(DocumentEvent e) { } + }); + this.getPopupMenu().addPopupMenuListener(new PopupMenuListener() { + // menu-select field on caret change so shift capitalizes instead of viewing selection + final ChangeListener selectFieldOnCaretChange = + e -> SearchMenusMenu.this.selectField(MenuSelectionManager.defaultManager()); + @Override public void popupMenuWillBecomeVisible(PopupMenuEvent e) { SearchMenusMenu.this.field.selectAll(); + // Only listen for text selection after initial text selection (see popupMenuWillBecomeInvisible). + SearchMenusMenu.this.field.getCaret().addChangeListener(this.selectFieldOnCaretChange); SearchMenusMenu.this.updateResultItems(); } @Override - public void popupMenuWillBecomeInvisible(PopupMenuEvent e) { } + public void popupMenuWillBecomeInvisible(PopupMenuEvent e) { + // Don't listen for text selection before initial text selection so field doesn't get menu-selected + // when releasing shift to return from viewing. + SearchMenusMenu.this.field.getCaret().removeChangeListener(this.selectFieldOnCaretChange); + } @Override public void popupMenuCanceled(PopupMenuEvent e) { } @@ -234,6 +256,18 @@ public void changedUpdate(DocumentEvent e) { this.retranslate(); } + private void selectField(MenuSelectionManager manager) { + manager.setSelectedPath(SearchMenusMenu.this.getFieldPath().toArray(EMPTY_MENU_ELEMENTS)); + } + + private ImmutableList getFieldPath() { + if (this.fieldPath == null) { + this.fieldPath = buildPathTo(SearchMenusMenu.this.field); + } + + return this.fieldPath; + } + private void updateResultItems() { final String searchTerm = this.field.getText();