Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
124 commits
Select commit Hold shift + click to select a range
a8b4d7d
add non-functional SearchMenusMenu with a search field and an empty r…
supersaiyansubtlety Nov 9, 2025
9ed3b2a
add PlaceheldTextField
supersaiyansubtlety Nov 10, 2025
e247070
first pass at MultiTrie abstraction
supersaiyansubtlety Nov 10, 2025
f14013c
rename all inner MultiTrie Node classes -> Node
supersaiyansubtlety Nov 10, 2025
e149fcf
add Nonnull to base Node::next method
supersaiyansubtlety Nov 10, 2025
02e9e3d
add @Nonnull to MultiTrie::get
supersaiyansubtlety Nov 10, 2025
21f8901
promote View
supersaiyansubtlety Nov 10, 2025
5ff85f3
require non-null field assignments
supersaiyansubtlety Nov 10, 2025
28022d4
rename package collection.trie -> multi_trie
supersaiyansubtlety Nov 10, 2025
8a88bc7
fix accidentally returning null root
supersaiyansubtlety Nov 10, 2025
4cb0dd0
require non-null root field assignment
supersaiyansubtlety Nov 10, 2025
40b2936
rename view -> getView
supersaiyansubtlety Nov 10, 2025
da5727f
improve javadoc
supersaiyansubtlety Nov 10, 2025
2a7b163
annotate createEmpty
supersaiyansubtlety Nov 11, 2025
bccc03a
refactor+rename StringHashMultiTrie -> CompositeStringMultiTrie
supersaiyansubtlety Nov 11, 2025
078db3b
add CompositeStringMultiTrieTest
supersaiyansubtlety Nov 11, 2025
7aa948c
test empty string key
supersaiyansubtlety Nov 11, 2025
5415f3e
propoerly conceal node mutation methods
supersaiyansubtlety Nov 11, 2025
0f99ef9
implement empty node pruning
supersaiyansubtlety Nov 11, 2025
85bfbcc
avoid the need for SplitFirst objects
supersaiyansubtlety Nov 11, 2025
14f3db0
test remove
supersaiyansubtlety Nov 11, 2025
11fca56
implement testPutMulti
supersaiyansubtlety Nov 11, 2025
dd8a421
add testRemoveMulti
supersaiyansubtlety Nov 11, 2025
c92dc0c
implement testRemoveAll
supersaiyansubtlety Nov 11, 2025
281cae7
push the concept of a sequence type down to StringMultiTrie
supersaiyansubtlety Nov 12, 2025
fd37f15
extract View interfaces
supersaiyansubtlety Nov 12, 2025
dabcb79
refactor so that mutable node's types are visible
supersaiyansubtlety Nov 13, 2025
7cd9b94
rename createAddException -> addExceptionOf
supersaiyansubtlety Nov 13, 2025
02cb9cc
fix CompositeBiMap::forcePut
supersaiyansubtlety Nov 13, 2025
be1b6e8
make nodes key-aware instead of using BiMaps :[
supersaiyansubtlety Nov 13, 2025
2a5fd05
rename MultiTrie.Node::streamBranches -> streamStems
supersaiyansubtlety Nov 14, 2025
091ab34
improve javadoc
supersaiyansubtlety Nov 14, 2025
39bbfac
move NodeView -> MutableMutliTrie.Node.View
supersaiyansubtlety Nov 14, 2025
54c4877
add key-by-key tests
supersaiyansubtlety Nov 14, 2025
5504ebd
javadoc CompositeStringMultiTrie
supersaiyansubtlety Nov 14, 2025
f957388
clarify tests
supersaiyansubtlety Nov 14, 2025
945b3e9
use same Branch.Factory instance for all nodes, inline createRoot
supersaiyansubtlety Nov 14, 2025
b7c4811
focus menus search field on parent selection
supersaiyansubtlety Nov 15, 2025
233d8d8
intial (buggy) menu search implemntation
supersaiyansubtlety Nov 15, 2025
89b0853
fix selecting field text on packing
supersaiyansubtlety Nov 15, 2025
dfb5540
implement case-insensitive StringMultiTrie querying
supersaiyansubtlety Nov 15, 2025
e2c2abc
update StringMultiTrie javadocs
supersaiyansubtlety Nov 15, 2025
44161fd
simplify StringMultiTrie generics
supersaiyansubtlety Nov 15, 2025
c309cc9
convolute StringMultiTrie generics (to improve theoretical extensibil…
supersaiyansubtlety Nov 15, 2025
e1b9cd5
add case-insensitive tests
supersaiyansubtlety Nov 15, 2025
bdcddd1
add SearchableElement::getSearchName and use that for SearchMenusMenu…
supersaiyansubtlety Nov 16, 2025
b3d3b07
add CONTRIBUTING.md with information on translations, esp. alias tran…
supersaiyansubtlety Nov 16, 2025
2d633fd
replace javax nullity annotations with jspecify's
supersaiyansubtlety Nov 18, 2025
8fbdead
checkstyle
supersaiyansubtlety Nov 18, 2025
e0fb42d
inline association instances in ALL
supersaiyansubtlety Nov 18, 2025
62b130d
re-rename MutableMultiTrie getView methods -> view
supersaiyansubtlety Nov 18, 2025
1e0c741
add previous methods to MutliTrie.Node, eliminate MapNode
supersaiyansubtlety Nov 19, 2025
942f118
make orphans private
supersaiyansubtlety Nov 19, 2025
0b6654e
convert StringMultiTrie to interface
supersaiyansubtlety Nov 19, 2025
319777a
don't require MutableMapNodes in StringMultiTrie
supersaiyansubtlety Nov 19, 2025
b640b2d
separate StringMultiTrie and MutableStringMultiTrie interfaces
supersaiyansubtlety Nov 19, 2025
b1d88f3
update [Mutable]StringMultiTrie javadocs
supersaiyansubtlety Nov 19, 2025
9f739b6
reduce visibility of CompositeSTringMultiTrie.Root
supersaiyansubtlety Nov 19, 2025
686a169
rename MutableMapNode::tryAdopt -> adoptIfOrphan
supersaiyansubtlety Nov 19, 2025
d0998b1
add testDepth
supersaiyansubtlety Nov 19, 2025
92e7d29
add testPrevious
supersaiyansubtlety Nov 19, 2025
84b63bb
fix ignore case methods so they return all results
supersaiyansubtlety Nov 19, 2025
e82ff68
filter empty nodes in streamNextIgnoreCase
supersaiyansubtlety Nov 19, 2025
aea8b17
add Node::isNonEmpty
supersaiyansubtlety Nov 19, 2025
37e8a77
utilize Node::previous in SearchMenusMenu
supersaiyansubtlety Nov 19, 2025
04df0cd
add testViews
supersaiyansubtlety Nov 19, 2025
086fcd7
move SearchableElement to menu_bar
supersaiyansubtlety Nov 19, 2025
dd835b9
extract Retranslatable interface
supersaiyansubtlety Nov 20, 2025
aaae2f1
make SaveMappingsAsMenu's formats searchable
supersaiyansubtlety Nov 20, 2025
0c4fd4d
use GuiUtil.syncStateWithConfig on FielMenu.autoSaveMappingsItem
supersaiyansubtlety Nov 20, 2025
f095e97
make EntryTooltipsMenu's sub-items searchable
supersaiyansubtlety Nov 20, 2025
c738eb9
eliminate unecessary Simple...Item super classes
supersaiyansubtlety Nov 20, 2025
7ce1371
make NotificationsMenu level items searchable
supersaiyansubtlety Nov 20, 2025
4e59262
make StatsMenu and TypeMenu sub-itmes searchable
supersaiyansubtlety Nov 20, 2025
8df388c
make ThemesMenu themes searchable
supersaiyansubtlety Nov 20, 2025
0af1142
make ViewMenu.fontItem searchable
supersaiyansubtlety Nov 20, 2025
3b18bac
split SearchableElement and ConventionalSearchableElement
supersaiyansubtlety Nov 20, 2025
86f7cde
make DecompilerMenu sub-items searchable
supersaiyansubtlety Nov 20, 2025
2f0413c
make DevMenu sub-items searchable
supersaiyansubtlety Nov 20, 2025
11917e2
make HelpMenu not searchable
supersaiyansubtlety Nov 20, 2025
6d30f65
make SearchMenu items searchable
supersaiyansubtlety Nov 20, 2025
3197b03
remove unused imports
supersaiyansubtlety Nov 20, 2025
57991b0
clearSearchMenusResults in MenuBar::updateUiState
supersaiyansubtlety Nov 20, 2025
497b758
implement contains search
supersaiyansubtlety Nov 21, 2025
8b5466d
CompositeStringMultiTrie: decrease branch map initial capacity with d…
supersaiyansubtlety Nov 21, 2025
8deebdb
add EmptyStringMultiTrie
supersaiyansubtlety Nov 21, 2025
f42b19c
tweak SearchMenusMenu::streamElementTree
supersaiyansubtlety Nov 21, 2025
791766a
fix SearchMenusMenu's popup's positioning+border
supersaiyansubtlety Nov 21, 2025
f8fed77
SearchMenusMenu: replace hacky MenuListener with much cleaner PopupMe…
supersaiyansubtlety Nov 21, 2025
efaecbb
show search alias of search item when it doesn't match name
supersaiyansubtlety Nov 22, 2025
cd4134d
allow CompositeStringMultiTrie branch maps to have an initial capacit…
supersaiyansubtlety Nov 22, 2025
1c6de81
optimize containing results update in some cases
supersaiyansubtlety Nov 22, 2025
c01a801
fix case where some cached containing results could be erroniously kept
supersaiyansubtlety Nov 22, 2025
37cfbaa
implement basic searchable item click functionality
supersaiyansubtlety Nov 22, 2025
e87012d
move ItemHolder functionality into new Item extends JMenuItem
supersaiyansubtlety Nov 23, 2025
dd60c3a
fix SearchMenusMenu.field sometimes being unfocussable
supersaiyansubtlety Nov 23, 2025
94bb67a
store ItemHolders in MultiTries instead of Results
supersaiyansubtlety Nov 23, 2025
cc4aa8e
replace useages of AbstractButton#doClick() with AbstractButton#doCli…
supersaiyansubtlety Nov 23, 2025
ebea542
add separator between prefix and containing result items
supersaiyansubtlety Nov 23, 2025
f79dbc3
add non-functional hints to SearchMenusMenu
supersaiyansubtlety Nov 23, 2025
c22b3f4
allow permanently dismissing hints via button click
supersaiyansubtlety Nov 23, 2025
42ab851
dismiss hints when used
supersaiyansubtlety Nov 23, 2025
cba2630
remove CompositeBiMap :[
supersaiyansubtlety Nov 24, 2025
723df14
reword hints: preview -> view, execute -> choose
supersaiyansubtlety Nov 26, 2025
589ddf8
update CONTRIBUTING.md for substring search, remove a substring alias
supersaiyansubtlety Nov 26, 2025
b76dc00
add path tooltips to search result items
supersaiyansubtlety Nov 26, 2025
6601053
base ItemHolds' identity on their SearchableElement to prevent duplic…
supersaiyansubtlety Nov 26, 2025
b6767d3
fix showing 'No results' even after deleting search term
supersaiyansubtlety Nov 26, 2025
c3fe298
truncate results to avoid dropping keystrokes due to lag caused by pa…
supersaiyansubtlety Nov 27, 2025
0d20950
add button for showing truncated results
supersaiyansubtlety Nov 27, 2025
c0aeed0
add selection border to PlaceheldTextField
supersaiyansubtlety Nov 27, 2025
5c9e5f9
split PlaceheldMenuTextField subclass from PlaceheldTextField
supersaiyansubtlety Nov 27, 2025
b7630b8
make placeholder non-null
supersaiyansubtlety Nov 27, 2025
ff22d96
minor tweaks
supersaiyansubtlety Nov 27, 2025
156dc96
inversely scale MAX_INITIAL_RESULTS when scale is greater than 1
supersaiyansubtlety Nov 27, 2025
6c842d1
don't truncate
supersaiyansubtlety Nov 27, 2025
e1f9423
give PlaceheldMenuTextField a preferred min height no less than a JMe…
supersaiyansubtlety Nov 27, 2025
d7dfaa1
add and improve javadocs
supersaiyansubtlety Nov 27, 2025
337d773
add "Dismiss" tooltip to hint dismiss buttons
supersaiyansubtlety Nov 28, 2025
4543008
select SearchMenusMenu.field from a MenuSelectionManager ChangeListen…
supersaiyansubtlety Nov 28, 2025
7229606
extract EMPTY_MENU_ELEMENTS
supersaiyansubtlety Nov 28, 2025
0e449a0
remove unused method
supersaiyansubtlety Nov 28, 2025
435e0a4
copy and dispose graphics in custom paint implementations
supersaiyansubtlety Dec 4, 2025
9972923
menu-select field when content or non-initial text-selection change s…
supersaiyansubtlety Dec 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 126 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# 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 `;`.<br>
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 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
the translation file.

#### Complete list of search alias translation keys
| Element | Translation Key |
|----------------------------------------------------------|----------------------------------------------------|
| `Dev` menu | `"dev.menu.aliases"` |
| `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.aliases"` |
| `Collab`>`Start Server` | `"menu.collab.server.start.aliases"` |
| `Collab`>`Stop Server` | `"menu.collab.server.stop.aliases"` |
| `Decompiler` menu | `"menu.decompiler.aliases"` |
| `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"` |
| `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.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"` |
| `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"` |
| `View`>`Fonts...` | `"menu.view.font.aliases"` |
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

A tool for deobfuscation of Java bytecode. Forked from <https://bitbucket.org/cuchaz/enigma>, 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).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
Original file line number Diff line number Diff line change
@@ -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 'view' hint until it's dismissed.")
public final TrackedValue<Boolean> showViewHint = this.value(true);

@Comment("Whether to show the search menus 'choose' hint until it's dismissed.")
public final TrackedValue<Boolean> showChooseHint = this.value(true);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
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;
import javax.swing.text.Document;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Insets;

/**
* A text field that displays placeholder text when it's empty.
*/
public class PlaceheldTextField extends JTextField {
protected static final int DEFAULT_COLUMNS = 0;

private Placeholder placeholder;
@Nullable
private Color placeholderColor;

/**
* 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}, {@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, DEFAULT_COLUMNS);
}

/**
* 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(Document doc, String text, @Nullable String placeholder, int columns) {
super(doc, text, columns);
this.setPlaceholder(placeholder);
}

@Override
public Dimension getPreferredSize() {
final Dimension size = super.getPreferredSize();

if (this.placeholder.isFull()) {
final Insets insets = this.getInsets();

size.width = Math.max(insets.left + this.placeholder.getWidth() + insets.right, size.width);
}

return size;
}

@Override
protected void paintComponent(Graphics graphics) {
super.paintComponent(graphics);

if (this.placeholder.isFull() && this.getText().isEmpty()) {
final Graphics disposableGraphics = graphics.create();
GuiUtil.trySetRenderingHints(disposableGraphics);

Utils.findFirstNonNull(this.placeholderColor, this.getDisabledTextColor(), this.getForeground())
.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 = disposableGraphics.getFontMetrics().getMaxAscent() + insets.top + extraTop;

disposableGraphics.drawString(this.placeholder.getText(), insets.left, baseY);

disposableGraphics.dispose();
}
}

/**
* @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 || placeholder.isEmpty()
? EmptyPlaceholder.INSTANCE
: new FullPlaceholder(placeholder);
}

public String getPlaceholder() {
return this.placeholder.getText();
}

protected int getPlaceholderWidth() {
return this.placeholder.getWidth();
}

/**
* @param color the placeholder color for this field; if {@code null}, the
* {@linkplain #getDisabledTextColor() disabled color} will be used
*/
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);

// placeholder is null when the super constructor calls setFont
if (this.placeholder instanceof FullPlaceholder full) {
full.clearWidth();
}
}

private sealed interface Placeholder {
String getText();

boolean isFull();

int getWidth();
}

private static final class EmptyPlaceholder implements Placeholder {
static final EmptyPlaceholder INSTANCE = new EmptyPlaceholder();

@Override
public String getText() {
return "";
}

@Override
public boolean isFull() {
return false;
}

@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;

FullPlaceholder(String text) {
this.text = text;
}

@Override
public String getText() {
return this.text;
}

@Override
public boolean isFull() {
return true;
}

@Override
public int getWidth() {
if (this.width < 0) {
this.width = PlaceheldTextField.this
.getFontMetrics(PlaceheldTextField.this.getFont()).stringWidth(this.text);
}

return this.width;
}

public void clearWidth() {
this.width = UNSET_WIDTH;
}
}
}
Loading