Skip to content

Commit

Permalink
Add configurable keybinds (QuiltMC/enigma#8)
Browse files Browse the repository at this point in the history
Closes FabricMC#319 and addresses FabricMC#52
  • Loading branch information
NebelNidas committed Sep 21, 2022
1 parent 5ed0efd commit a95819c
Show file tree
Hide file tree
Showing 21 changed files with 1,043 additions and 136 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
import java.awt.Component;
import java.awt.Container;
import java.awt.Point;
import cuchaz.enigma.gui.config.keybind.KeyBinds;
import de.sciss.syntaxpane.actions.DocumentSearchData;
import de.sciss.syntaxpane.actions.gui.QuickFindDialog;

import javax.swing.text.JTextComponent;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.util.stream.IntStream;
Expand All @@ -12,10 +17,6 @@
import javax.swing.JTextField;
import javax.swing.JToolBar;
import javax.swing.SwingUtilities;
import javax.swing.text.JTextComponent;

import de.sciss.syntaxpane.actions.DocumentSearchData;
import de.sciss.syntaxpane.actions.gui.QuickFindDialog;

public class EnigmaQuickFindDialog extends QuickFindDialog {
public EnigmaQuickFindDialog(JTextComponent target) {
Expand All @@ -28,12 +29,12 @@ public EnigmaQuickFindDialog(JTextComponent target) {
@Override
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();
}
}
});
Expand Down
5 changes: 5 additions & 0 deletions enigma-swing/src/main/java/cuchaz/enigma/gui/Gui.java
Original file line number Diff line number Diff line change
Expand Up @@ -666,4 +666,9 @@ public boolean validateImmediateAction(Consumer<ValidationContext> op) {
public boolean isEditable(EditableType t) {
return this.editableTypes.contains(t);
}

public void reloadKeyBinds() {
this.menuBar.setKeyBinds();
this.editorTabbedPane.reloadKeyBinds();
}
}
3 changes: 3 additions & 0 deletions enigma-swing/src/main/java/cuchaz/enigma/gui/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import joptsimple.OptionSet;
import joptsimple.OptionSpec;
import joptsimple.ValueConverter;
import cuchaz.enigma.gui.config.keybind.KeyBinds;

import cuchaz.enigma.EnigmaProfile;
import cuchaz.enigma.gui.config.Themes;
Expand Down Expand Up @@ -116,6 +117,8 @@ public static void main(String[] args) throws IOException {

Themes.setupTheme();

KeyBinds.loadConfig();

Gui gui = new Gui(parsedProfile, editables);
GuiController controller = gui.getController();

Expand Down
Original file line number Diff line number Diff line change
@@ -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());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package cuchaz.enigma.gui.config.keybind;

import cuchaz.enigma.utils.I18n;

import javax.swing.KeyStroke;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

public record KeyBind(String name, String category, List<Combination> 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<Combination> 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;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package cuchaz.enigma.gui.config.keybind;

import cuchaz.enigma.gui.config.KeyBindsConfig;

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;

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).key(KeyEvent.VK_C).mod(KeyEvent.CTRL_DOWN_MASK | KeyEvent.SHIFT_DOWN_MASK).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<KeyBind> 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<KeyBind> 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<KeyBind> EDITABLE_KEY_BINDS;

private KeyBinds() {
}

public static boolean isConfigurable(KeyBind keyBind) {
return CONFIGURABLE_KEY_BINDS.stream().anyMatch(bind -> bind.isSameKeyBind(keyBind));
}

public static Map<String, List<KeyBind>> 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<KeyBind> getEditableKeyBinds() {
return EDITABLE_KEY_BINDS;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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();
}
}
Expand Down
Loading

0 comments on commit a95819c

Please sign in to comment.