diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/EnigmaQuickFindDialog.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/EnigmaQuickFindDialog.java index 0af8f9c16..b08568463 100644 --- a/enigma-swing/src/main/java/cuchaz/enigma/gui/EnigmaQuickFindDialog.java +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/EnigmaQuickFindDialog.java @@ -17,6 +17,8 @@ import de.sciss.syntaxpane.actions.DocumentSearchData; import de.sciss.syntaxpane.actions.gui.QuickFindDialog; +import cuchaz.enigma.gui.config.keybind.KeyBinds; + public class EnigmaQuickFindDialog extends QuickFindDialog { public EnigmaQuickFindDialog(JTextComponent target) { super(target, DocumentSearchData.getFromEditor(target)); @@ -29,11 +31,12 @@ public EnigmaQuickFindDialog(JTextComponent target) { public void keyPressed(KeyEvent e) { super.keyPressed(e); - if (e.getKeyCode() == KeyEvent.VK_ENTER) { + if (KeyBinds.QUICK_FIND_DIALOG_PREVIOUS.matches(e)) { + JToolBar toolBar = getToolBar(); + getPrevButton(toolBar).doClick(); + } else if (KeyBinds.QUICK_FIND_DIALOG_NEXT.matches(e)) { JToolBar toolBar = getToolBar(); - boolean next = !e.isShiftDown(); - JButton button = next ? getNextButton(toolBar) : getPrevButton(toolBar); - button.doClick(); + getNextButton(toolBar).doClick(); } } }); diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/Gui.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/Gui.java index b3117ce22..46a6df8cb 100644 --- a/enigma-swing/src/main/java/cuchaz/enigma/gui/Gui.java +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/Gui.java @@ -666,4 +666,9 @@ public boolean validateImmediateAction(Consumer op) { public boolean isEditable(EditableType t) { return this.editableTypes.contains(t); } + + public void reloadKeyBinds() { + this.menuBar.setKeyBinds(); + this.editorTabbedPane.reloadKeyBinds(); + } } diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/Main.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/Main.java index 56f438599..3c9ab10f4 100644 --- a/enigma-swing/src/main/java/cuchaz/enigma/gui/Main.java +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/Main.java @@ -29,6 +29,7 @@ import cuchaz.enigma.EnigmaProfile; import cuchaz.enigma.gui.config.Themes; import cuchaz.enigma.gui.config.UiConfig; +import cuchaz.enigma.gui.config.keybind.KeyBinds; import cuchaz.enigma.gui.dialog.CrashDialog; import cuchaz.enigma.translation.mapping.serde.MappingFormat; import cuchaz.enigma.utils.I18n; @@ -105,6 +106,8 @@ public static void main(String[] args) throws IOException { System.setProperty("apple.laf.useScreenMenuBar", "true"); Themes.setupTheme(); + KeyBinds.loadConfig(); + Gui gui = new Gui(parsedProfile, editables); GuiController controller = gui.getController(); diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/config/KeyBindsConfig.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/config/KeyBindsConfig.java new file mode 100644 index 000000000..51017169c --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/config/KeyBindsConfig.java @@ -0,0 +1,28 @@ +package cuchaz.enigma.gui.config; + +import cuchaz.enigma.config.ConfigContainer; +import cuchaz.enigma.config.ConfigSection; +import cuchaz.enigma.gui.config.keybind.KeyBind; + +public final class KeyBindsConfig { + private KeyBindsConfig() { + } + + private static final ConfigContainer cfg = ConfigContainer.getOrCreate("enigma/enigmakeybinds"); + + public static void save() { + cfg.save(); + } + + private static ConfigSection getSection(KeyBind keyBind) { + return keyBind.category().isEmpty() ? cfg.data() : cfg.data().section(keyBind.category()); + } + + public static String[] getKeyBindCodes(KeyBind keyBind) { + return getSection(keyBind).getArray(keyBind.name()).orElse(keyBind.serializeCombinations()); + } + + public static void setKeyBind(KeyBind keyBind) { + getSection(keyBind).setArray(keyBind.name(), keyBind.serializeCombinations()); + } +} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/config/keybind/KeyBind.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/config/keybind/KeyBind.java new file mode 100644 index 000000000..bd4c01e92 --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/config/keybind/KeyBind.java @@ -0,0 +1,146 @@ +package cuchaz.enigma.gui.config.keybind; + +import java.awt.event.KeyEvent; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import javax.swing.KeyStroke; + +import cuchaz.enigma.utils.I18n; + +public record KeyBind(String name, String category, List combinations) { + public record Combination(int keyCode, int keyModifiers) { + public static final Combination EMPTY = new Combination(-1, 0); + public boolean matches(KeyEvent e) { + return e.getKeyCode() == keyCode && e.getModifiersEx() == keyModifiers; + } + + public KeyStroke toKeyStroke(int modifiers) { + modifiers = keyModifiers | modifiers; + return KeyStroke.getKeyStroke(keyCode, modifiers); + } + + public String serialize() { + return keyCode + ";" + Integer.toString(keyModifiers, 16); + } + + public static Combination deserialize(String str) { + String[] parts = str.split(";", 2); + return new Combination(Integer.parseInt(parts[0]), Integer.parseInt(parts[1], 16)); + } + + @Override + public String toString() { + return "Combination[keyCode=" + keyCode + ", keyModifiers=0x" + Integer.toString(keyModifiers, 16).toUpperCase(Locale.ROOT) + "]"; + } + } + + public void setFrom(KeyBind other) { + this.combinations.clear(); + this.combinations.addAll(other.combinations); + } + + public boolean matches(KeyEvent e) { + return combinations.stream().anyMatch(c -> c.matches(e)); + } + + public KeyStroke toKeyStroke(int modifiers) { + return isEmpty() ? null : combinations.get(0).toKeyStroke(modifiers); + } + + public KeyStroke toKeyStroke() { + return toKeyStroke(0); + } + + public boolean isEmpty() { + return combinations.isEmpty(); + } + + public String[] serializeCombinations() { + return combinations.stream().map(Combination::serialize).toArray(String[]::new); + } + + public void deserializeCombinations(String[] serialized) { + combinations.clear(); + + for (String serializedCombination : serialized) { + if (!serializedCombination.isEmpty()) { + combinations.add(Combination.deserialize(serializedCombination)); + } else { + System.out.println("warning: empty combination deserialized for keybind " + (category.isEmpty() ? "" : category + ".") + name); + } + } + } + + private String getTranslationKey() { + return "keybind." + (category.isEmpty() ? "" : category + ".") + this.name; + } + + public String getTranslatedName() { + return I18n.translate(getTranslationKey()); + } + + public KeyBind copy() { + return new KeyBind(name, category, new ArrayList<>(combinations)); + } + + public KeyBind toImmutable() { + return new KeyBind(name, category, List.copyOf(combinations)); + } + + public boolean isSameKeyBind(KeyBind other) { + return name.equals(other.name) && category.equals(other.category); + } + + public static Builder builder(String name) { + return new Builder(name); + } + + public static Builder builder(String name, String category) { + return new Builder(name, category); + } + + public static class Builder { + private final String name; + private final String category; + private final List combinations = new ArrayList<>(); + private int modifiers = 0; + + private Builder(String name) { + this.name = name; + this.category = ""; + } + + private Builder(String name, String category) { + this.name = name; + this.category = category; + } + + public KeyBind build() { + return new KeyBind(name, category, combinations); + } + + public Builder key(int keyCode, int keyModifiers) { + combinations.add(new Combination(keyCode, keyModifiers | modifiers)); + return this; + } + + public Builder key(int keyCode) { + return key(keyCode, 0); + } + + public Builder keys(int... keyCodes) { + for (int keyCode : keyCodes) { + key(keyCode); + } + + return this; + } + + public Builder mod(int modifiers) { + this.modifiers |= modifiers; + return this; + } + } +} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/config/keybind/KeyBinds.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/config/keybind/KeyBinds.java new file mode 100644 index 000000000..e32ce70df --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/config/keybind/KeyBinds.java @@ -0,0 +1,131 @@ +package cuchaz.enigma.gui.config.keybind; + +import java.awt.event.KeyEvent; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import cuchaz.enigma.gui.config.KeyBindsConfig; + +public final class KeyBinds { + private static final String QUICK_FIND_DIALOG_CATEGORY = "quick_find_dialog"; + private static final String SEARCH_DIALOG_CATEGORY = "search_dialog"; + private static final String EDITOR_CATEGORY = "editor"; + private static final String MENU_CATEGORY = "menu"; + + public static final KeyBind EXIT = KeyBind.builder("close").key(KeyEvent.VK_ESCAPE).build(); + public static final KeyBind DIALOG_SAVE = KeyBind.builder("dialog_save").key(KeyEvent.VK_ENTER).build(); + + public static final KeyBind QUICK_FIND_DIALOG_NEXT = KeyBind.builder("next", QUICK_FIND_DIALOG_CATEGORY).key(KeyEvent.VK_ENTER).build(); + public static final KeyBind QUICK_FIND_DIALOG_PREVIOUS = KeyBind.builder("previous", QUICK_FIND_DIALOG_CATEGORY).mod(KeyEvent.SHIFT_DOWN_MASK).key(KeyEvent.VK_ENTER).build(); + public static final KeyBind SEARCH_DIALOG_NEXT = KeyBind.builder("next", SEARCH_DIALOG_CATEGORY).key(KeyEvent.VK_DOWN).build(); + public static final KeyBind SEARCH_DIALOG_PREVIOUS = KeyBind.builder("previous", SEARCH_DIALOG_CATEGORY).key(KeyEvent.VK_UP).build(); + + public static final KeyBind EDITOR_RENAME = KeyBind.builder("rename", EDITOR_CATEGORY).mod(KeyEvent.CTRL_DOWN_MASK).key(KeyEvent.VK_R).build(); + public static final KeyBind EDITOR_PASTE = KeyBind.builder("paste", EDITOR_CATEGORY).mod(KeyEvent.CTRL_DOWN_MASK).key(KeyEvent.VK_V).build(); + public static final KeyBind EDITOR_EDIT_JAVADOC = KeyBind.builder("edit_javadoc", EDITOR_CATEGORY).mod(KeyEvent.CTRL_DOWN_MASK).key(KeyEvent.VK_D).build(); + public static final KeyBind EDITOR_SHOW_INHERITANCE = KeyBind.builder("show_inheritance", EDITOR_CATEGORY).mod(KeyEvent.CTRL_DOWN_MASK).key(KeyEvent.VK_I).build(); + public static final KeyBind EDITOR_SHOW_IMPLEMENTATIONS = KeyBind.builder("show_implementations", EDITOR_CATEGORY).mod(KeyEvent.CTRL_DOWN_MASK).key(KeyEvent.VK_M).build(); + public static final KeyBind EDITOR_SHOW_CALLS = KeyBind.builder("show_calls", EDITOR_CATEGORY).mod(KeyEvent.CTRL_DOWN_MASK).key(KeyEvent.VK_C).build(); + public static final KeyBind EDITOR_SHOW_CALLS_SPECIFIC = KeyBind.builder("show_calls_specific", EDITOR_CATEGORY).mod(KeyEvent.CTRL_DOWN_MASK | KeyEvent.SHIFT_DOWN_MASK).key(KeyEvent.VK_C).build(); + public static final KeyBind EDITOR_OPEN_ENTRY = KeyBind.builder("open_entry", EDITOR_CATEGORY).mod(KeyEvent.CTRL_DOWN_MASK).key(KeyEvent.VK_N).build(); + public static final KeyBind EDITOR_OPEN_PREVIOUS = KeyBind.builder("open_previous", EDITOR_CATEGORY).mod(KeyEvent.CTRL_DOWN_MASK).key(KeyEvent.VK_P).build(); + public static final KeyBind EDITOR_OPEN_NEXT = KeyBind.builder("open_next", EDITOR_CATEGORY).mod(KeyEvent.CTRL_DOWN_MASK).key(KeyEvent.VK_E).build(); + public static final KeyBind EDITOR_TOGGLE_MAPPING = KeyBind.builder("toggle_mapping", EDITOR_CATEGORY).mod(KeyEvent.CTRL_DOWN_MASK).key(KeyEvent.VK_O).build(); + public static final KeyBind EDITOR_ZOOM_IN = KeyBind.builder("zoom_in", EDITOR_CATEGORY).mod(KeyEvent.CTRL_DOWN_MASK).keys(KeyEvent.VK_PLUS, KeyEvent.VK_ADD, KeyEvent.VK_EQUALS).build(); + public static final KeyBind EDITOR_ZOOM_OUT = KeyBind.builder("zoom_out", EDITOR_CATEGORY).mod(KeyEvent.CTRL_DOWN_MASK).keys(KeyEvent.VK_MINUS, KeyEvent.VK_SUBTRACT).build(); + public static final KeyBind EDITOR_CLOSE_TAB = KeyBind.builder("close_tab", EDITOR_CATEGORY).mod(KeyEvent.CTRL_DOWN_MASK).key(KeyEvent.VK_4).build(); + public static final KeyBind EDITOR_RELOAD_CLASS = KeyBind.builder("reload_class", EDITOR_CATEGORY).mod(KeyEvent.CTRL_DOWN_MASK).key(KeyEvent.VK_F5).build(); + public static final KeyBind EDITOR_QUICK_FIND = KeyBind.builder("quick_find", EDITOR_CATEGORY).mod(KeyEvent.CTRL_DOWN_MASK).key(KeyEvent.VK_F).build(); + + public static final KeyBind SAVE_MAPPINGS = KeyBind.builder("save", MENU_CATEGORY).mod(KeyEvent.CTRL_DOWN_MASK).key(KeyEvent.VK_S).build(); + public static final KeyBind DROP_MAPPINGS = KeyBind.builder("drop_mappings", MENU_CATEGORY).build(); + public static final KeyBind RELOAD_MAPPINGS = KeyBind.builder("reload_mappings", MENU_CATEGORY).build(); + public static final KeyBind RELOAD_ALL = KeyBind.builder("reload_all", MENU_CATEGORY).build(); + public static final KeyBind MAPPING_STATS = KeyBind.builder("mapping_stats", MENU_CATEGORY).build(); + public static final KeyBind SEARCH_CLASS = KeyBind.builder("search_class", MENU_CATEGORY).mod(KeyEvent.SHIFT_DOWN_MASK).key(KeyEvent.VK_SPACE).build(); + public static final KeyBind SEARCH_METHOD = KeyBind.builder("search_method", MENU_CATEGORY).build(); + public static final KeyBind SEARCH_FIELD = KeyBind.builder("search_field", MENU_CATEGORY).build(); + + private static final List DEFAULT_KEY_BINDS = Stream.of(EXIT, DIALOG_SAVE, QUICK_FIND_DIALOG_NEXT, + QUICK_FIND_DIALOG_PREVIOUS, SEARCH_DIALOG_NEXT, SEARCH_DIALOG_PREVIOUS, EDITOR_RENAME, EDITOR_PASTE, + EDITOR_EDIT_JAVADOC, EDITOR_SHOW_INHERITANCE, EDITOR_SHOW_IMPLEMENTATIONS, EDITOR_SHOW_CALLS, + EDITOR_SHOW_CALLS_SPECIFIC, EDITOR_OPEN_ENTRY, EDITOR_OPEN_PREVIOUS, EDITOR_OPEN_NEXT, + EDITOR_TOGGLE_MAPPING, EDITOR_ZOOM_IN, EDITOR_ZOOM_OUT, EDITOR_CLOSE_TAB, EDITOR_RELOAD_CLASS, + EDITOR_QUICK_FIND, SAVE_MAPPINGS, DROP_MAPPINGS, RELOAD_MAPPINGS, RELOAD_ALL, MAPPING_STATS, SEARCH_CLASS, + SEARCH_METHOD, SEARCH_FIELD).map(KeyBind::toImmutable).toList(); + + private static final List CONFIGURABLE_KEY_BINDS = List.of(EDITOR_RENAME, EDITOR_PASTE, EDITOR_EDIT_JAVADOC, + EDITOR_SHOW_INHERITANCE, EDITOR_SHOW_IMPLEMENTATIONS, EDITOR_SHOW_CALLS, EDITOR_SHOW_CALLS_SPECIFIC, + EDITOR_OPEN_ENTRY, EDITOR_OPEN_PREVIOUS, EDITOR_OPEN_NEXT, EDITOR_TOGGLE_MAPPING, EDITOR_ZOOM_IN, + EDITOR_ZOOM_OUT, EDITOR_CLOSE_TAB, EDITOR_RELOAD_CLASS, SAVE_MAPPINGS, DROP_MAPPINGS, RELOAD_MAPPINGS, + RELOAD_ALL, MAPPING_STATS, SEARCH_CLASS, SEARCH_METHOD, SEARCH_FIELD); + // Editing entries in CONFIGURABLE_KEY_BINDS directly wouldn't allow to revert the changes instead of saving + private static List EDITABLE_KEY_BINDS; + + private KeyBinds() { + } + + public static boolean isConfigurable(KeyBind keyBind) { + return CONFIGURABLE_KEY_BINDS.stream().anyMatch(bind -> bind.isSameKeyBind(keyBind)); + } + + public static Map> getEditableKeyBindsByCategory() { + return EDITABLE_KEY_BINDS.stream() + .collect(Collectors.groupingBy(KeyBind::category)); + } + + public static void loadConfig() { + for (KeyBind keyBind : CONFIGURABLE_KEY_BINDS) { + keyBind.deserializeCombinations(KeyBindsConfig.getKeyBindCodes(keyBind)); + } + + resetEditableKeyBinds(); + } + + public static void saveConfig() { + boolean modified = false; + + for (int i = 0; i < CONFIGURABLE_KEY_BINDS.size(); i++) { + KeyBind keyBind = CONFIGURABLE_KEY_BINDS.get(i); + KeyBind editedKeyBind = EDITABLE_KEY_BINDS.get(i); + + if (!editedKeyBind.equals(keyBind)) { + modified = true; + keyBind.setFrom(editedKeyBind); + KeyBindsConfig.setKeyBind(editedKeyBind); + } + } + + if (modified) { + KeyBindsConfig.save(); + } + } + + // Reset the key binds to the saved values + public static void resetEditableKeyBinds() { + EDITABLE_KEY_BINDS = CONFIGURABLE_KEY_BINDS.stream().map(KeyBind::copy) + .collect(Collectors.toCollection(ArrayList::new)); + } + + public static void resetToDefault(KeyBind keyBind) { + // Ensure the key bind is editable + if (!EDITABLE_KEY_BINDS.contains(keyBind)) { + return; + } + + KeyBind defaultKeyBind = DEFAULT_KEY_BINDS.stream().filter(bind -> bind.isSameKeyBind(keyBind)).findFirst().orElse(null); + + if (defaultKeyBind == null) { + throw new IllegalStateException("Could not find default key bind for " + keyBind); + } + + keyBind.setFrom(defaultKeyBind); + } + + public static List getEditableKeyBinds() { + return EDITABLE_KEY_BINDS; + } +} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/ChangeDialog.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/ChangeDialog.java index 51948b569..74887885c 100644 --- a/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/ChangeDialog.java +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/ChangeDialog.java @@ -11,6 +11,7 @@ import javax.swing.JLabel; import javax.swing.JPanel; +import cuchaz.enigma.gui.config.keybind.KeyBinds; import cuchaz.enigma.utils.I18n; public class ChangeDialog { @@ -35,7 +36,7 @@ public static void show(Window parent) { okButton.addKeyListener(new KeyAdapter() { @Override public void keyPressed(KeyEvent e) { - if (e.getKeyCode() == KeyEvent.VK_ESCAPE) { + if (KeyBinds.EXIT.matches(e)) { frame.dispose(); } } diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/JavadocDialog.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/JavadocDialog.java index d6e544d09..f09cc4418 100644 --- a/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/JavadocDialog.java +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/JavadocDialog.java @@ -34,6 +34,7 @@ import cuchaz.enigma.analysis.EntryReference; import cuchaz.enigma.gui.GuiController; import cuchaz.enigma.gui.config.UiConfig; +import cuchaz.enigma.gui.config.keybind.KeyBinds; import cuchaz.enigma.gui.elements.ValidatableTextArea; import cuchaz.enigma.gui.util.GuiUtil; import cuchaz.enigma.gui.util.ScaleUtil; @@ -68,8 +69,7 @@ private JavadocDialog(JFrame parent, GuiController controller, Entry entry, S this.text.addKeyListener(new KeyAdapter() { @Override public void keyPressed(KeyEvent event) { - switch (event.getKeyCode()) { - case KeyEvent.VK_ENTER: + if (KeyBinds.DIALOG_SAVE.matches(event)) { if (event.isControlDown()) { doSave(); @@ -77,13 +77,8 @@ public void keyPressed(KeyEvent event) { close(); } } - - break; - case KeyEvent.VK_ESCAPE: + } else if (KeyBinds.EXIT.matches(event)) { close(); - break; - default: - break; } } }); diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/SearchDialog.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/SearchDialog.java index 7814dd811..eabc48dcd 100644 --- a/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/SearchDialog.java +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/SearchDialog.java @@ -43,6 +43,7 @@ import cuchaz.enigma.analysis.index.EntryIndex; import cuchaz.enigma.gui.Gui; import cuchaz.enigma.gui.GuiController; +import cuchaz.enigma.gui.config.keybind.KeyBinds; import cuchaz.enigma.gui.search.SearchEntry; import cuchaz.enigma.gui.search.SearchUtil; import cuchaz.enigma.gui.util.AbstractListCellRenderer; @@ -94,15 +95,15 @@ public void changedUpdate(DocumentEvent e) { searchField.addKeyListener(new KeyAdapter() { @Override public void keyPressed(KeyEvent e) { - if (e.getKeyCode() == KeyEvent.VK_DOWN) { + if (KeyBinds.SEARCH_DIALOG_NEXT.matches(e)) { int next = classList.isSelectionEmpty() ? 0 : classList.getSelectedIndex() + 1; classList.setSelectedIndex(next); classList.ensureIndexIsVisible(next); - } else if (e.getKeyCode() == KeyEvent.VK_UP) { + } else if (KeyBinds.SEARCH_DIALOG_PREVIOUS.matches(e)) { int prev = classList.isSelectionEmpty() ? classList.getModel().getSize() : classList.getSelectedIndex() - 1; classList.setSelectedIndex(prev); classList.ensureIndexIsVisible(prev); - } else if (e.getKeyCode() == KeyEvent.VK_ESCAPE) { + } else if (KeyBinds.EXIT.matches(e)) { close(); } } diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/keybind/CombinationPanel.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/keybind/CombinationPanel.java new file mode 100644 index 000000000..23cc73178 --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/keybind/CombinationPanel.java @@ -0,0 +1,230 @@ +package cuchaz.enigma.gui.dialog.keybind; + +import java.awt.Color; +import java.awt.FlowLayout; +import java.awt.event.InputEvent; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.util.List; + +import javax.swing.JButton; +import javax.swing.JPanel; +import javax.swing.border.EmptyBorder; + +import cuchaz.enigma.gui.config.keybind.KeyBind; +import cuchaz.enigma.gui.util.ScaleUtil; +import cuchaz.enigma.utils.I18n; + +public class CombinationPanel extends JPanel { + private static final List MODIFIER_KEYS = List.of(KeyEvent.VK_SHIFT, KeyEvent.VK_CONTROL, KeyEvent.VK_ALT, KeyEvent.VK_META); + private static final List MODIFIER_FLAGS = List.of(InputEvent.SHIFT_DOWN_MASK, InputEvent.CTRL_DOWN_MASK, InputEvent.ALT_DOWN_MASK, InputEvent.META_DOWN_MASK); + private static final Color EDITING_BUTTON_FOREGROUND = Color.ORANGE; + private final EditKeyBindDialog parent; + private final JButton button; + private final Color defaultButtonFg; + private final KeyBind.Combination originalCombination; + private final MutableCombination editingCombination; + private final MutableCombination lastCombination; + private boolean editing = false; + + public CombinationPanel(EditKeyBindDialog parent, KeyBind.Combination combination) { + this.parent = parent; + this.originalCombination = combination; + this.editingCombination = MutableCombination.fromCombination(combination); + this.lastCombination = editingCombination.copy(); + + setLayout(new FlowLayout(FlowLayout.RIGHT)); + setBorder(new EmptyBorder(0, ScaleUtil.scale(15), 0, ScaleUtil.scale(15))); + + JButton removeButton = new JButton(I18n.translate("menu.file.configure_keybinds.edit.remove")); + removeButton.addActionListener(e -> this.parent.removeCombination(this)); + removeButton.addMouseListener(mouseListener()); + add(removeButton); + + button = new JButton(getButtonText()); + defaultButtonFg = button.getForeground(); + button.addActionListener(e -> onButtonPressed()); + button.addMouseListener(mouseListener()); + button.addKeyListener(new KeyAdapter() { + @Override + public void keyPressed(KeyEvent e) { + onKeyPressed(e); + } + }); + add(button); + } + + private String getButtonText() { + return editingCombination.toString(); + } + + private void onButtonPressed() { + if (editing) { + stopEditing(); + } else { + startEditing(); + } + } + + protected void stopEditing() { + if (editing) { + editing = false; + button.setForeground(defaultButtonFg); + + if (!editingCombination.isEmpty() && !editingCombination.isValid()) { + // Reset combination to last one if invalid + editingCombination.setFrom(lastCombination); + update(); + } else { + lastCombination.setFrom(editingCombination); + } + } + } + + private void startEditing() { + if (!editing) { + editing = true; + button.setForeground(EDITING_BUTTON_FOREGROUND); + } + } + + private void update() { + button.setText(getButtonText()); + parent.pack(); + } + + private void onKeyPressed(KeyEvent e) { + if (editing) { + if (MODIFIER_KEYS.contains(e.getKeyCode())) { + int modifierIndex = MODIFIER_KEYS.indexOf(e.getKeyCode()); + int modifier = MODIFIER_FLAGS.get(modifierIndex); + editingCombination.setKeyModifiers(editingCombination.keyModifiers | modifier); + } else { + editingCombination.setKeyCode(e.getKeyCode()); + } + + update(); + } + } + + // Stop editing other CombinationPanels when clicking on this panel + private MouseAdapter mouseListener() { + return new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + parent.stopEditing(CombinationPanel.this); + } + }; + } + + public boolean isModified() { + return !editingCombination.isSameCombination(originalCombination); + } + + public boolean isCombinationValid() { + return editingCombination.isValid(); + } + + public KeyBind.Combination getOriginalCombination() { + return originalCombination; + } + + public KeyBind.Combination getResultCombination() { + return new KeyBind.Combination(editingCombination.keyCode, editingCombination.keyModifiers); + } + + public static CombinationPanel createEmpty(EditKeyBindDialog parent) { + return new CombinationPanel(parent, KeyBind.Combination.EMPTY); + } + + private static class MutableCombination { + private int keyCode; + private int keyModifiers; + + private MutableCombination(int keyCode, int keyModifiers) { + this.keyCode = keyCode; + this.keyModifiers = keyModifiers; + } + + public static MutableCombination fromCombination(KeyBind.Combination combination) { + return new MutableCombination(combination.keyCode(), combination.keyModifiers()); + } + + public void setFrom(MutableCombination combination) { + set(combination.getKeyCode(), combination.getKeyModifiers()); + } + + public void set(int keyCode, int keyModifiers) { + this.keyCode = keyCode; + this.keyModifiers = keyModifiers; + } + + public int getKeyCode() { + return this.keyCode; + } + + public int getKeyModifiers() { + return this.keyModifiers; + } + + public void setKeyCode(int keyCode) { + this.keyCode = keyCode; + } + + public void setKeyModifiers(int keyModifiers) { + this.keyModifiers = keyModifiers; + } + + public MutableCombination copy() { + return new MutableCombination(keyCode, keyModifiers); + } + + @Override + public String toString() { + String modifiers = modifiersToString(); + String key = keyToString(); + + if (!modifiers.isEmpty()) { + return modifiers + "+" + key; + } + + return key; + } + + private String modifiersToString() { + if (keyModifiers == 0) { + return ""; + } + + return InputEvent.getModifiersExText(keyModifiers); + } + + private String keyToString() { + if (keyCode == -1) { + return I18n.translate("menu.file.configure_keybinds.edit.empty"); + } + + return KeyEvent.getKeyText(keyCode); + } + + public boolean isEmpty() { + return keyCode == -1 && keyModifiers == 0; + } + + public boolean isValid() { + return keyCode != -1; + } + + public boolean isSameCombination(Object obj) { + if (obj instanceof KeyBind.Combination combination) { + return combination.keyCode() == keyCode && combination.keyModifiers() == keyModifiers; + } else if (obj instanceof MutableCombination mutableCombination) { + return mutableCombination.keyCode == keyCode && mutableCombination.keyModifiers == keyModifiers; + } + + return false; + } + } +} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/keybind/ConfigureCategoryKeyBindsDialog.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/keybind/ConfigureCategoryKeyBindsDialog.java new file mode 100644 index 000000000..c1e67eb16 --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/keybind/ConfigureCategoryKeyBindsDialog.java @@ -0,0 +1,64 @@ +package cuchaz.enigma.gui.dialog.keybind; + +import java.awt.BorderLayout; +import java.awt.Container; +import java.awt.FlowLayout; +import java.awt.Frame; +import java.awt.GridLayout; +import java.util.List; + +import javax.swing.JButton; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.border.EmptyBorder; + +import cuchaz.enigma.gui.config.keybind.KeyBind; +import cuchaz.enigma.gui.util.ScaleUtil; +import cuchaz.enigma.utils.I18n; + +public class ConfigureCategoryKeyBindsDialog extends JDialog { + public ConfigureCategoryKeyBindsDialog(Frame owner, String category, List keyBinds) { + super(owner, I18n.translateFormatted("menu.file.configure_keybinds.category_title", I18n.translate("keybind.category." + category)), true); + + Container contentPane = getContentPane(); + contentPane.setLayout(new BorderLayout()); + + // Add keybinds + JPanel keyBindsPanel = new JPanel(new GridLayout(0, 1, 5, 5)); + keyBindsPanel.setBorder(new EmptyBorder(ScaleUtil.scale(10), ScaleUtil.scale(10), ScaleUtil.scale(10), ScaleUtil.scale(10))); + + for (KeyBind keyBind : keyBinds) { + JPanel panel = new JPanel(new BorderLayout()); + + JLabel label = new JLabel(I18n.translate("keybind." + category + "." + keyBind.name())); + panel.add(label, BorderLayout.WEST); + + JButton button = new JButton(I18n.translate("menu.file.configure_keybinds.edit_keybind")); + button.addActionListener(e -> { + EditKeyBindDialog dialog = new EditKeyBindDialog(owner, keyBind); + dialog.setVisible(true); + }); + panel.add(button, BorderLayout.EAST); + + keyBindsPanel.add(panel); + } + + contentPane.add(keyBindsPanel, BorderLayout.CENTER); + + // Add buttons + Container buttonContainer = new JPanel(new FlowLayout(FlowLayout.RIGHT, ScaleUtil.scale(4), ScaleUtil.scale(4))); + JButton okButton = new JButton(I18n.translate("prompt.ok")); + okButton.addActionListener(event -> close()); + buttonContainer.add(okButton); + contentPane.add(buttonContainer, BorderLayout.SOUTH); + + pack(); + setLocationRelativeTo(owner); + } + + private void close() { + setVisible(false); + dispose(); + } +} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/keybind/ConfigureKeyBindsDialog.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/keybind/ConfigureKeyBindsDialog.java new file mode 100644 index 000000000..8862341cc --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/keybind/ConfigureKeyBindsDialog.java @@ -0,0 +1,107 @@ +package cuchaz.enigma.gui.dialog.keybind; + +import java.awt.BorderLayout; +import java.awt.Container; +import java.awt.FlowLayout; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.util.List; +import java.util.Map; + +import javax.swing.JButton; +import javax.swing.JDialog; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.border.EmptyBorder; + +import cuchaz.enigma.gui.Gui; +import cuchaz.enigma.gui.config.keybind.KeyBind; +import cuchaz.enigma.gui.config.keybind.KeyBinds; +import cuchaz.enigma.gui.util.GridBagConstraintsBuilder; +import cuchaz.enigma.gui.util.ScaleUtil; +import cuchaz.enigma.utils.I18n; + +public class ConfigureKeyBindsDialog extends JDialog { + private final Gui gui; + + public ConfigureKeyBindsDialog(Gui gui) { + super(gui.getFrame(), I18n.translate("menu.file.configure_keybinds.title"), true); + this.gui = gui; + JFrame owner = gui.getFrame(); + + Container contentPane = getContentPane(); + contentPane.setLayout(new BorderLayout()); + + // Add categories + JPanel categoriesPanel = new JPanel(new GridBagLayout()); + categoriesPanel.setBorder(new EmptyBorder(ScaleUtil.scale(10), ScaleUtil.scale(10), ScaleUtil.scale(10), ScaleUtil.scale(10))); + Map> keyBinds = KeyBinds.getEditableKeyBindsByCategory(); + int i = 0; + + for (Map.Entry> entry : keyBinds.entrySet()) { + String category = entry.getKey(); + + if (category.isEmpty()) { + // keys in the empty category can't be configured + continue; + } + + JLabel label = new JLabel(I18n.translate("keybind.category." + category)); + JButton button = new JButton(I18n.translate("menu.file.configure_keybinds.edit")); + button.addActionListener(e -> { + ConfigureCategoryKeyBindsDialog dialog = new ConfigureCategoryKeyBindsDialog(owner, category, entry.getValue()); + dialog.setVisible(true); + }); + + GridBagConstraintsBuilder cb = GridBagConstraintsBuilder.create().insets(2); + + categoriesPanel.add(label, cb.pos(0, i).weightX(0.0).anchor(GridBagConstraints.LINE_END).fill(GridBagConstraints.NONE).build()); + categoriesPanel.add(button, cb.pos(1, i).weightX(1.0).anchor(GridBagConstraints.LINE_END).fill(GridBagConstraints.HORIZONTAL).build()); + i++; + } + + contentPane.add(categoriesPanel, BorderLayout.CENTER); + + // Add buttons + Container buttonContainer = new JPanel(new FlowLayout(FlowLayout.RIGHT, ScaleUtil.scale(4), ScaleUtil.scale(4))); + JButton saveButton = new JButton(I18n.translate("menu.file.configure_keybinds.save")); + saveButton.addActionListener(event -> save()); + buttonContainer.add(saveButton); + JButton cancelButton = new JButton(I18n.translate("prompt.cancel")); + cancelButton.addActionListener(event -> cancel()); + buttonContainer.add(cancelButton); + contentPane.add(buttonContainer, BorderLayout.SOUTH); + + addWindowListener(new WindowAdapter() { + @Override + public void windowClosing(WindowEvent e) { + super.windowClosing(e); + KeyBinds.resetEditableKeyBinds(); + } + }); + + pack(); + setLocationRelativeTo(owner); + } + + private void save() { + KeyBinds.saveConfig(); + gui.reloadKeyBinds(); + setVisible(false); + dispose(); + } + + private void cancel() { + KeyBinds.resetEditableKeyBinds(); + setVisible(false); + dispose(); + } + + public static void show(Gui gui) { + ConfigureKeyBindsDialog dialog = new ConfigureKeyBindsDialog(gui); + dialog.setVisible(true); + } +} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/keybind/EditKeyBindDialog.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/keybind/EditKeyBindDialog.java new file mode 100644 index 000000000..27b14af2b --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/keybind/EditKeyBindDialog.java @@ -0,0 +1,183 @@ +package cuchaz.enigma.gui.dialog.keybind; + +import java.awt.BorderLayout; +import java.awt.Container; +import java.awt.FlowLayout; +import java.awt.Frame; +import java.awt.GridLayout; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.util.ArrayList; +import java.util.List; + +import javax.swing.JButton; +import javax.swing.JDialog; +import javax.swing.JPanel; +import javax.swing.border.EmptyBorder; + +import cuchaz.enigma.gui.config.keybind.KeyBind; +import cuchaz.enigma.gui.config.keybind.KeyBinds; +import cuchaz.enigma.gui.util.ScaleUtil; +import cuchaz.enigma.utils.I18n; + +public class EditKeyBindDialog extends JDialog { + private final List combinationPanels = new ArrayList<>(); + private final List combinations; + private final KeyBind keyBind; + private final JPanel combinationsPanel; + + public EditKeyBindDialog(Frame owner, KeyBind bind) { + super(owner, I18n.translate("menu.file.configure_keybinds.edit.title"), true); + this.keyBind = bind; + this.combinations = new ArrayList<>(keyBind.combinations()); + + Container contentPane = getContentPane(); + contentPane.setLayout(new BorderLayout()); + + // Add buttons + JPanel buttonsPanel = new JPanel(new GridLayout(0, 2)); + JButton addButton = new JButton(I18n.translate("menu.file.configure_keybinds.edit.add")); + addButton.addActionListener(e -> addCombination()); + addButton.addMouseListener(mouseListener()); + buttonsPanel.add(addButton); + JButton clearButton = new JButton(I18n.translate("menu.file.configure_keybinds.edit.clear")); + clearButton.addActionListener(e -> clearCombinations()); + clearButton.addMouseListener(mouseListener()); + buttonsPanel.add(clearButton); + JButton resetButton = new JButton(I18n.translate("menu.file.configure_keybinds.edit.reset")); + resetButton.addActionListener(e -> reset()); + resetButton.addMouseListener(mouseListener()); + buttonsPanel.add(resetButton); + contentPane.add(buttonsPanel, BorderLayout.NORTH); + + // Add combinations panel + combinationsPanel = new JPanel(new GridLayout(0, 1, 5, 5)); + combinationsPanel.setBorder(new EmptyBorder(ScaleUtil.scale(10), ScaleUtil.scale(10), ScaleUtil.scale(10), ScaleUtil.scale(10))); + combinationsPanel.addMouseListener(mouseListener()); + + for (KeyBind.Combination combination : keyBind.combinations()) { + CombinationPanel combinationPanel = new CombinationPanel(this, combination); + combinationPanel.addMouseListener(mouseListener()); + combinationPanels.add(combinationPanel); + combinationsPanel.add(combinationPanel); + } + + contentPane.add(combinationsPanel, BorderLayout.CENTER); + + // Add confirmation buttons + Container buttonContainer = new JPanel(new FlowLayout(FlowLayout.RIGHT, ScaleUtil.scale(4), ScaleUtil.scale(4))); + JButton saveButton = new JButton(I18n.translate("menu.file.configure_keybinds.save")); + saveButton.addActionListener(event -> save()); + buttonContainer.add(saveButton); + JButton cancelButton = new JButton(I18n.translate("prompt.cancel")); + cancelButton.addActionListener(event -> cancel()); + buttonContainer.add(cancelButton); + contentPane.add(buttonContainer, BorderLayout.SOUTH); + + addMouseListener(mouseListener()); + + pack(); + setLocationRelativeTo(owner); + } + + private void save() { + boolean modified = !combinations.equals(keyBind.combinations()); + + for (CombinationPanel combinationPanel : combinationPanels) { + if (combinationPanel.isModified() && combinationPanel.isCombinationValid()) { + modified = true; + KeyBind.Combination combination = combinationPanel.getResultCombination(); + + if (isNewCombination(combinationPanel)) { + combinations.add(combination); + } else { + int index = combinations.indexOf(combinationPanel.getOriginalCombination()); + + if (index >= 0) { + combinations.set(index, combination); + } else { + combinations.add(combination); + } + } + } + } + + if (modified) { + keyBind.combinations().clear(); + keyBind.combinations().addAll(combinations); + } + + setVisible(false); + dispose(); + } + + private void cancel() { + setVisible(false); + dispose(); + } + + // Stop editing when the user clicks + private MouseAdapter mouseListener() { + return new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + stopEditing(null); + } + }; + } + + protected void removeCombination(CombinationPanel combinationPanel) { + combinations.remove(combinationPanel.getOriginalCombination()); + combinationsPanel.remove(combinationPanel); + combinationPanels.remove(combinationPanel); + pack(); + } + + private void addCombination() { + CombinationPanel combinationPanel = CombinationPanel.createEmpty(this); + combinationPanel.addMouseListener(mouseListener()); + combinationsPanel.add(combinationPanel); + combinationPanels.add(combinationPanel); + pack(); + } + + private void clearCombinations() { + for (CombinationPanel combinationPanel : combinationPanels) { + combinations.remove(combinationPanel.getOriginalCombination()); + combinationsPanel.remove(combinationPanel); + } + + combinationPanels.clear(); + pack(); + } + + private void reset() { + combinations.clear(); + combinationPanels.clear(); + combinationsPanel.removeAll(); + + KeyBinds.resetToDefault(keyBind); + combinations.addAll(keyBind.combinations()); + + for (KeyBind.Combination combination : combinations) { + CombinationPanel combinationPanel = new CombinationPanel(this, combination); + combinationPanel.addMouseListener(mouseListener()); + combinationPanels.add(combinationPanel); + combinationsPanel.add(combinationPanel); + } + + pack(); + } + + private boolean isNewCombination(CombinationPanel panel) { + return panel.getOriginalCombination() != KeyBind.Combination.EMPTY; + } + + // Stop editing all combination panels but the excluded one + protected void stopEditing(CombinationPanel excluded) { + for (CombinationPanel combinationPanel : combinationPanels) { + if (combinationPanel == excluded) continue; + combinationPanel.stopEditing(); + } + } +} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/ConvertingTextField.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/ConvertingTextField.java index 301ae7f19..34cc9428e 100644 --- a/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/ConvertingTextField.java +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/ConvertingTextField.java @@ -17,6 +17,7 @@ import com.formdev.flatlaf.FlatClientProperties; +import cuchaz.enigma.gui.config.keybind.KeyBinds; import cuchaz.enigma.gui.events.ConvertingTextFieldListener; import cuchaz.enigma.gui.util.GuiUtil; import cuchaz.enigma.utils.validation.ParameterizedMessage; @@ -61,9 +62,9 @@ public void focusLost(FocusEvent e) { this.textField.addKeyListener(new KeyAdapter() { @Override public void keyPressed(KeyEvent e) { - if (e.getKeyCode() == KeyEvent.VK_ESCAPE) { + if (KeyBinds.EXIT.matches(e)) { stopEditing(true); - } else if (e.getKeyCode() == KeyEvent.VK_ENTER) { + } else if (KeyBinds.DIALOG_SAVE.matches(e)) { stopEditing(false); } } diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/EditorPopupMenu.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/EditorPopupMenu.java index d128bf5cb..7aab6a5dc 100644 --- a/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/EditorPopupMenu.java +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/EditorPopupMenu.java @@ -1,16 +1,15 @@ package cuchaz.enigma.gui.elements; -import java.awt.event.InputEvent; import java.awt.event.KeyEvent; import javax.swing.JMenuItem; import javax.swing.JPopupMenu; -import javax.swing.KeyStroke; import cuchaz.enigma.analysis.EntryReference; import cuchaz.enigma.gui.EditableType; import cuchaz.enigma.gui.Gui; import cuchaz.enigma.gui.GuiController; +import cuchaz.enigma.gui.config.keybind.KeyBinds; import cuchaz.enigma.gui.panels.EditorPanel; import cuchaz.enigma.translation.representation.entry.ClassEntry; import cuchaz.enigma.translation.representation.entry.Entry; @@ -70,19 +69,6 @@ public EditorPopupMenu(EditorPanel editor, Gui gui) { this.openNextItem.setEnabled(false); this.toggleMappingItem.setEnabled(false); - this.renameItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_R, InputEvent.CTRL_DOWN_MASK)); - this.editJavadocItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_D, InputEvent.CTRL_DOWN_MASK)); - this.showInheritanceItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_I, InputEvent.CTRL_DOWN_MASK)); - this.showImplementationsItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_M, InputEvent.CTRL_DOWN_MASK)); - this.showCallsItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_C, InputEvent.CTRL_DOWN_MASK)); - this.showCallsSpecificItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_C, InputEvent.CTRL_DOWN_MASK + InputEvent.SHIFT_DOWN_MASK)); - this.openEntryItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_N, InputEvent.CTRL_DOWN_MASK)); - this.openPreviousItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_P, InputEvent.CTRL_DOWN_MASK)); - this.openNextItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_E, InputEvent.CTRL_DOWN_MASK)); - this.toggleMappingItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, InputEvent.CTRL_DOWN_MASK)); - this.zoomInItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_PLUS, InputEvent.CTRL_DOWN_MASK)); - this.zoomOutMenu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_MINUS, InputEvent.CTRL_DOWN_MASK)); - this.renameItem.addActionListener(event -> gui.startRename(editor)); this.editJavadocItem.addActionListener(event -> gui.startDocChange(editor)); this.showInheritanceItem.addActionListener(event -> gui.showInheritance(editor)); @@ -98,45 +84,61 @@ public EditorPopupMenu(EditorPanel editor, Gui gui) { this.resetZoomItem.addActionListener(event -> editor.resetEditorZoom()); } + public void setKeyBinds() { + this.renameItem.setAccelerator(KeyBinds.EDITOR_RENAME.toKeyStroke()); + // TODO: Uncomment once https://github.com/FabricMC/Enigma/pull/478 is merged + // this.pasteItem.setAccelerator(KeyBinds.EDITOR_PASTE.toKeyStroke()); + this.editJavadocItem.setAccelerator(KeyBinds.EDITOR_EDIT_JAVADOC.toKeyStroke()); + this.showInheritanceItem.setAccelerator(KeyBinds.EDITOR_SHOW_INHERITANCE.toKeyStroke()); + this.showImplementationsItem.setAccelerator(KeyBinds.EDITOR_SHOW_IMPLEMENTATIONS.toKeyStroke()); + this.showCallsItem.setAccelerator(KeyBinds.EDITOR_SHOW_CALLS.toKeyStroke()); + this.showCallsSpecificItem.setAccelerator(KeyBinds.EDITOR_SHOW_CALLS_SPECIFIC.toKeyStroke()); + this.openEntryItem.setAccelerator(KeyBinds.EDITOR_OPEN_ENTRY.toKeyStroke()); + this.openPreviousItem.setAccelerator(KeyBinds.EDITOR_OPEN_PREVIOUS.toKeyStroke()); + this.openNextItem.setAccelerator(KeyBinds.EDITOR_OPEN_NEXT.toKeyStroke()); + this.toggleMappingItem.setAccelerator(KeyBinds.EDITOR_TOGGLE_MAPPING.toKeyStroke()); + this.zoomInItem.setAccelerator(KeyBinds.EDITOR_ZOOM_IN.toKeyStroke()); + this.zoomOutMenu.setAccelerator(KeyBinds.EDITOR_ZOOM_OUT.toKeyStroke()); + } + // TODO have editor redirect key event to menu so that the actions get // triggered without having to hardcode them here, because this // is a hack public boolean handleKeyEvent(KeyEvent event) { - if (event.isControlDown()) { - switch (event.getKeyCode()) { - case KeyEvent.VK_I: - this.showInheritanceItem.doClick(); - return true; - case KeyEvent.VK_M: - this.showImplementationsItem.doClick(); - return true; - case KeyEvent.VK_N: - this.openEntryItem.doClick(); - return true; - case KeyEvent.VK_P: - this.openPreviousItem.doClick(); - return true; - case KeyEvent.VK_E: - this.openNextItem.doClick(); - return true; - case KeyEvent.VK_C: - if (event.isShiftDown()) { - this.showCallsSpecificItem.doClick(); - } else { - this.showCallsItem.doClick(); - } - - return true; - case KeyEvent.VK_O: - this.toggleMappingItem.doClick(); - return true; - case KeyEvent.VK_R: - this.renameItem.doClick(); - return true; - case KeyEvent.VK_D: - this.editJavadocItem.doClick(); - return true; - } + if (KeyBinds.EDITOR_SHOW_INHERITANCE.matches(event)) { + this.showInheritanceItem.doClick(); + return true; + } else if (KeyBinds.EDITOR_SHOW_IMPLEMENTATIONS.matches(event)) { + this.showImplementationsItem.doClick(); + return true; + } else if (KeyBinds.EDITOR_OPEN_ENTRY.matches(event)) { + this.openEntryItem.doClick(); + return true; + } else if (KeyBinds.EDITOR_OPEN_PREVIOUS.matches(event)) { + this.openPreviousItem.doClick(); + return true; + } else if (KeyBinds.EDITOR_OPEN_NEXT.matches(event)) { + this.openNextItem.doClick(); + return true; + } else if (KeyBinds.EDITOR_SHOW_CALLS_SPECIFIC.matches(event)) { + this.showCallsSpecificItem.doClick(); + return true; + } else if (KeyBinds.EDITOR_SHOW_CALLS.matches(event)) { + this.showCallsItem.doClick(); + return true; + } else if (KeyBinds.EDITOR_TOGGLE_MAPPING.matches(event)) { + this.toggleMappingItem.doClick(); + return true; + } else if (KeyBinds.EDITOR_RENAME.matches(event)) { + this.renameItem.doClick(); + return true; + } else if (KeyBinds.EDITOR_EDIT_JAVADOC.matches(event)) { + this.editJavadocItem.doClick(); + return true; + // TODO: Uncomment once https://github.com/FabricMC/Enigma/pull/478 is merged + // } else if (KeyBinds.EDITOR_PASTE.matches(event)) { + // this.pasteItem.doClick(); + // return true; } return false; diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/EditorTabPopupMenu.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/EditorTabPopupMenu.java index 93854818e..f9bcac5f3 100644 --- a/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/EditorTabPopupMenu.java +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/EditorTabPopupMenu.java @@ -1,12 +1,11 @@ package cuchaz.enigma.gui.elements; import java.awt.Component; -import java.awt.event.KeyEvent; import javax.swing.JMenuItem; import javax.swing.JPopupMenu; -import javax.swing.KeyStroke; +import cuchaz.enigma.gui.config.keybind.KeyBinds; import cuchaz.enigma.gui.panels.EditorPanel; import cuchaz.enigma.utils.I18n; @@ -24,7 +23,7 @@ public EditorTabPopupMenu(EditorTabbedPane pane) { this.ui = new JPopupMenu(); this.close = new JMenuItem(); - this.close.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_4, KeyEvent.CTRL_DOWN_MASK)); + this.close.setAccelerator(KeyBinds.EDITOR_CLOSE_TAB.toKeyStroke()); this.close.addActionListener(a -> pane.closeEditor(editor)); this.ui.add(this.close); diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/EditorTabbedPane.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/EditorTabbedPane.java index 7a6290ea6..44ed5f3ec 100644 --- a/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/EditorTabbedPane.java +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/EditorTabbedPane.java @@ -15,6 +15,7 @@ import cuchaz.enigma.analysis.EntryReference; import cuchaz.enigma.classhandle.ClassHandle; import cuchaz.enigma.gui.Gui; +import cuchaz.enigma.gui.config.keybind.KeyBinds; import cuchaz.enigma.gui.events.EditorActionListener; import cuchaz.enigma.gui.panels.ClosableTabTitlePane; import cuchaz.enigma.gui.panels.EditorPanel; @@ -76,7 +77,7 @@ public void onTitleChanged(EditorPanel editor, String title) { ed.getEditor().addKeyListener(new KeyAdapter() { @Override public void keyPressed(KeyEvent e) { - if (e.getKeyCode() == KeyEvent.VK_4 && (e.getModifiersEx() & KeyEvent.CTRL_DOWN_MASK) != 0) { + if (KeyBinds.EDITOR_CLOSE_TAB.matches(e)) { closeEditor(ed); } } @@ -162,4 +163,8 @@ public void retranslateUi() { public Component getUi() { return this.openFiles; } + + public void reloadKeyBinds() { + this.editors.values().forEach(EditorPanel::reloadKeyBinds); + } } diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/MenuBar.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/MenuBar.java index 24a69b65e..388333feb 100644 --- a/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/MenuBar.java +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/MenuBar.java @@ -1,7 +1,5 @@ package cuchaz.enigma.gui.elements; -import java.awt.event.InputEvent; -import java.awt.event.KeyEvent; import java.io.File; import java.io.IOException; import java.nio.file.Files; @@ -19,7 +17,6 @@ import javax.swing.JMenuItem; import javax.swing.JOptionPane; import javax.swing.JRadioButtonMenuItem; -import javax.swing.KeyStroke; import cuchaz.enigma.gui.ConnectionState; import cuchaz.enigma.gui.Gui; @@ -27,6 +24,7 @@ import cuchaz.enigma.gui.config.LookAndFeel; import cuchaz.enigma.gui.config.NetConfig; import cuchaz.enigma.gui.config.UiConfig; +import cuchaz.enigma.gui.config.keybind.KeyBinds; import cuchaz.enigma.gui.dialog.AboutDialog; import cuchaz.enigma.gui.dialog.ChangeDialog; import cuchaz.enigma.gui.dialog.ConnectToServerDialog; @@ -34,6 +32,7 @@ import cuchaz.enigma.gui.dialog.FontDialog; import cuchaz.enigma.gui.dialog.SearchDialog; import cuchaz.enigma.gui.dialog.StatsDialog; +import cuchaz.enigma.gui.dialog.keybind.ConfigureKeyBindsDialog; import cuchaz.enigma.gui.util.GuiUtil; import cuchaz.enigma.gui.util.LanguageUtil; import cuchaz.enigma.gui.util.ScaleUtil; @@ -55,6 +54,7 @@ public class MenuBar { 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 JMenu decompilerMenu = new JMenu(); @@ -112,6 +112,8 @@ public MenuBar(Gui gui) { this.fileMenu.addSeparator(); this.fileMenu.add(this.statsItem); this.fileMenu.addSeparator(); + this.fileMenu.add(this.configureKeyBindsItem); + this.fileMenu.addSeparator(); this.fileMenu.add(this.exitItem); ui.add(this.fileMenu); @@ -137,8 +139,7 @@ public MenuBar(Gui gui) { this.helpMenu.add(this.githubItem); ui.add(this.helpMenu); - this.saveMappingsItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, InputEvent.CTRL_DOWN_MASK)); - this.searchClassItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, InputEvent.SHIFT_DOWN_MASK)); + setKeyBinds(); this.jarOpenItem.addActionListener(_e -> this.onOpenJarClicked()); this.jarCloseItem.addActionListener(_e -> this.gui.getController().closeJar()); @@ -150,6 +151,7 @@ public MenuBar(Gui gui) { this.exportSourceItem.addActionListener(_e -> this.onExportSourceClicked()); this.exportJarItem.addActionListener(_e -> this.onExportJarClicked()); this.statsItem.addActionListener(_e -> StatsDialog.show(this.gui)); + this.configureKeyBindsItem.addActionListener(_e -> ConfigureKeyBindsDialog.show(this.gui)); this.exitItem.addActionListener(_e -> this.gui.close()); this.customScaleItem.addActionListener(_e -> this.onCustomScaleClicked()); this.fontItem.addActionListener(_e -> this.onFontClicked(this.gui)); @@ -162,6 +164,17 @@ public MenuBar(Gui gui) { this.githubItem.addActionListener(_e -> this.onGithubClicked()); } + public void setKeyBinds() { + this.saveMappingsItem.setAccelerator(KeyBinds.SAVE_MAPPINGS.toKeyStroke()); + this.dropMappingsItem.setAccelerator(KeyBinds.DROP_MAPPINGS.toKeyStroke()); + this.reloadMappingsItem.setAccelerator(KeyBinds.RELOAD_MAPPINGS.toKeyStroke()); + this.reloadAllItem.setAccelerator(KeyBinds.RELOAD_ALL.toKeyStroke()); + this.statsItem.setAccelerator(KeyBinds.MAPPING_STATS.toKeyStroke()); + this.searchClassItem.setAccelerator(KeyBinds.SEARCH_CLASS.toKeyStroke()); + this.searchMethodItem.setAccelerator(KeyBinds.SEARCH_METHOD.toKeyStroke()); + this.searchFieldItem.setAccelerator(KeyBinds.SEARCH_FIELD.toKeyStroke()); + } + public void updateUiState() { boolean jarOpen = this.gui.isJarOpen(); ConnectionState connectionState = this.gui.getConnectionState(); @@ -197,6 +210,7 @@ public void retranslateUi() { 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.exitItem.setText(I18n.translate("menu.file.exit")); this.decompilerMenu.setText(I18n.translate("menu.decompiler")); diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/panels/EditorPanel.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/panels/EditorPanel.java index cb74ceca6..65a54cc3a 100644 --- a/enigma-swing/src/main/java/cuchaz/enigma/gui/panels/EditorPanel.java +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/panels/EditorPanel.java @@ -48,6 +48,7 @@ import cuchaz.enigma.gui.config.LookAndFeel; import cuchaz.enigma.gui.config.Themes; import cuchaz.enigma.gui.config.UiConfig; +import cuchaz.enigma.gui.config.keybind.KeyBinds; import cuchaz.enigma.gui.elements.EditorPopupMenu; import cuchaz.enigma.gui.events.EditorActionListener; import cuchaz.enigma.gui.events.ThemeChangeListener; @@ -162,39 +163,20 @@ public void mouseReleased(MouseEvent e) { this.editor.addKeyListener(new KeyAdapter() { @Override public void keyPressed(KeyEvent event) { - if (event.isControlDown()) { - EditorPanel.this.shouldNavigateOnClick = false; + if (EditorPanel.this.popupMenu.handleKeyEvent(event)) return; - if (EditorPanel.this.popupMenu.handleKeyEvent(event)) { - return; - } - - switch (event.getKeyCode()) { - case KeyEvent.VK_F5: - if (EditorPanel.this.classHandle != null) { - EditorPanel.this.classHandle.invalidate(); - } - - break; - - case KeyEvent.VK_F: - // prevent navigating on click when quick find activated - break; - - case KeyEvent.VK_ADD: - case KeyEvent.VK_EQUALS: - case KeyEvent.VK_PLUS: - offsetEditorZoom(2); - break; - case KeyEvent.VK_SUBTRACT: - case KeyEvent.VK_MINUS: - offsetEditorZoom(-2); - break; - - default: - EditorPanel.this.shouldNavigateOnClick = true; // CTRL - break; + if (KeyBinds.EDITOR_RELOAD_CLASS.matches(event)) { + if (EditorPanel.this.classHandle != null) { + EditorPanel.this.classHandle.invalidate(); } + } else if (KeyBinds.EDITOR_QUICK_FIND.matches(event)) { + // prevent navigating on click when quick find activated + } else if (KeyBinds.EDITOR_ZOOM_IN.matches(event)) { + offsetEditorZoom(2); + } else if (KeyBinds.EDITOR_ZOOM_OUT.matches(event)) { + offsetEditorZoom(-2); + } else if (event.isControlDown()) { + EditorPanel.this.shouldNavigateOnClick = true; // CTRL } } @@ -693,6 +675,10 @@ public void retranslateUi() { this.popupMenu.retranslateUi(); } + public void reloadKeyBinds() { + this.popupMenu.setKeyBinds(); + } + private enum DisplayMode { INACTIVE, IN_PROGRESS, diff --git a/enigma/src/main/resources/lang/en_us.json b/enigma/src/main/resources/lang/en_us.json index 4ec2f087e..b5a456b49 100644 --- a/enigma/src/main/resources/lang/en_us.json +++ b/enigma/src/main/resources/lang/en_us.json @@ -31,6 +31,19 @@ "menu.file.stats.top_level_package": "Top-Level Package:", "menu.file.stats.synthetic_parameters": "Include Synthetic Parameters", "menu.file.stats.generate": "Generate Diagram", + "menu.file.configure_keybinds": "Configure Keybinds...", + "menu.file.configure_keybinds.title": "Configure Keybinds", + "menu.file.configure_keybinds.category_title": "Configure %s Keybinds", + "menu.file.configure_keybinds.edit": "Edit Keybinds...", + "menu.file.configure_keybinds.save": "Save Keybinds", + "menu.file.configure_keybinds.edit_keybind": "Edit Keybind...", + "menu.file.configure_keybinds.edit.title": "Edit Keybind", + "menu.file.configure_keybinds.edit.add": "Add Keybind", + "menu.file.configure_keybinds.edit.remove": "Remove Keybind", + "menu.file.configure_keybinds.edit.empty": "", + "menu.file.configure_keybinds.edit.clear": "Clear Keybinds", + "menu.file.configure_keybinds.edit.reset": "Reset to defaults", + "menu.file.configure_keybinds.edit.save": "Save Keybind", "menu.file.exit": "Exit", "menu.decompiler": "Decompiler", "menu.view": "View", @@ -220,5 +233,31 @@ "crash.export": "Export", "crash.ignore": "Ignore", "crash.exit": "Exit", - "crash.exit.warning": "If you choose exit, you will lose any unsaved work." + "crash.exit.warning": "If you choose exit, you will lose any unsaved work.", + + "keybind.category.editor": "Editor", + "keybind.category.menu": "Menu", + "keybind.editor.rename": "Rename entry", + "keybind.editor.paste": "Paste entry name", + "keybind.editor.edit_javadoc": "Edit entry Javadoc", + "keybind.editor.show_inheritance": "Show entry inheritance", + "keybind.editor.show_implementations": "Show entry implementations", + "keybind.editor.show_calls": "Show entry calls", + "keybind.editor.show_calls_specific": "Show entry calls (Specific)", + "keybind.editor.open_entry": "Open entry", + "keybind.editor.open_previous": "Open previous entry", + "keybind.editor.open_next": "Open next entry", + "keybind.editor.toggle_mapping": "Mark as deobfuscated/obfuscated", + "keybind.editor.zoom_in": "Zoom in", + "keybind.editor.zoom_out": "Zoom in", + "keybind.editor.close_tab": "Close editor tab", + "keybind.editor.reload_class": "Reload open class", + "keybind.menu.search_class": "Search for class", + "keybind.menu.search_method": "Search for method", + "keybind.menu.search_field": "Search for field", + "keybind.menu.drop_mappings": "Drop invalid mappings", + "keybind.menu.reload_mappings": "Reload mappings", + "keybind.menu.reload_all": "Reload Jar and mappings", + "keybind.menu.mapping_stats": "Mapping stats", + "keybind.menu.save": "Save mappings" }