shedaniel 5 年之前
父节点
当前提交
ad7fa5cd86
共有 21 个文件被更改,包括 746 次插入86 次删除
  1. 1 1
      gradle.properties
  2. 10 6
      src/main/java/me/shedaniel/clothconfig2/ClothConfigInitializer.java
  3. 5 1
      src/main/java/me/shedaniel/clothconfig2/api/ConfigEntryBuilder.java
  4. 19 0
      src/main/java/me/shedaniel/clothconfig2/api/FakeModifierKeyCodeAdder.java
  5. 141 0
      src/main/java/me/shedaniel/clothconfig2/api/Modifier.java
  6. 62 0
      src/main/java/me/shedaniel/clothconfig2/api/ModifierKeyCode.java
  7. 117 35
      src/main/java/me/shedaniel/clothconfig2/gui/ClothConfigScreen.java
  8. 24 31
      src/main/java/me/shedaniel/clothconfig2/gui/entries/KeyCodeEntry.java
  9. 2 2
      src/main/java/me/shedaniel/clothconfig2/impl/ConfigEntryBuilderImpl.java
  10. 78 0
      src/main/java/me/shedaniel/clothconfig2/impl/FakeKeyBindings.java
  11. 43 0
      src/main/java/me/shedaniel/clothconfig2/impl/FakeModifierKeyCodeAdderImpl.java
  12. 7 0
      src/main/java/me/shedaniel/clothconfig2/impl/GameOptionsHooks.java
  13. 5 0
      src/main/java/me/shedaniel/clothconfig2/impl/KeyBindingHooks.java
  14. 83 0
      src/main/java/me/shedaniel/clothconfig2/impl/ModifierKeyCodeImpl.java
  15. 38 8
      src/main/java/me/shedaniel/clothconfig2/impl/builders/KeyCodeBuilder.java
  16. 52 0
      src/main/java/me/shedaniel/clothconfig2/mixin/MixinControlsOptionsScreen.java
  17. 19 0
      src/main/java/me/shedaniel/clothconfig2/mixin/MixinGameOptions.java
  18. 18 0
      src/main/java/me/shedaniel/clothconfig2/mixin/MixinKeyBinding.java
  19. 4 1
      src/main/resources/assets/cloth-config2/lang/en_us.json
  20. 3 1
      src/main/resources/fabric.mod.json
  21. 15 0
      src/main/resources/mixin.cloth-config2.json

+ 1 - 1
gradle.properties

@@ -2,6 +2,6 @@ minecraft_version=1.15
 yarn_version=1.15+build.1
 fabric_loader_version=0.7.2+build.174
 fabric_version=0.4.20+build.273-1.15
-mod_version=2.5.4
+mod_version=2.6.0
 modmenu_version=1.7.14-unstable.19w42a+build.10
 nec_api_version=1.0.0

+ 10 - 6
src/main/java/me/shedaniel/clothconfig2/ClothConfigInitializer.java

@@ -1,14 +1,12 @@
 package me.shedaniel.clothconfig2;
 
-import me.shedaniel.clothconfig2.api.ConfigBuilder;
-import me.shedaniel.clothconfig2.api.ConfigCategory;
-import me.shedaniel.clothconfig2.api.ConfigEntryBuilder;
-import me.shedaniel.clothconfig2.api.ScissorsHandler;
+import me.shedaniel.clothconfig2.api.*;
 import me.shedaniel.clothconfig2.impl.EasingMethod;
 import me.shedaniel.clothconfig2.impl.EasingMethod.EasingMethodImpl;
 import me.shedaniel.clothconfig2.impl.EasingMethods;
 import me.shedaniel.clothconfig2.impl.builders.DropdownMenuBuilder;
 import net.fabricmc.api.ClientModInitializer;
+import net.fabricmc.fabric.api.client.keybinding.KeyBindingRegistry;
 import net.fabricmc.loader.api.FabricLoader;
 import net.minecraft.client.MinecraftClient;
 import net.minecraft.client.util.InputUtil;
@@ -27,6 +25,7 @@ import java.util.Comparator;
 import java.util.Properties;
 import java.util.stream.Collectors;
 
+@SuppressWarnings("unchecked")
 public class ClothConfigInitializer implements ClientModInitializer {
     
     public static final Logger LOGGER = LogManager.getFormatterLogger("ClothConfig");
@@ -65,7 +64,7 @@ public class ClothConfigInitializer implements ClientModInitializer {
             Properties properties = new Properties();
             properties.load(new FileInputStream(file));
             String easing = properties.getProperty("easingMethod", "QUART");
-            for(EasingMethod value : EasingMethods.getMethods()) {
+            for (EasingMethod value : EasingMethods.getMethods()) {
                 if (value.toString().equalsIgnoreCase(easing)) {
                     easingMethod = value;
                     break;
@@ -123,7 +122,7 @@ public class ClothConfigInitializer implements ClientModInitializer {
                         ConfigCategory scrolling = builder.getOrCreateCategory("Scrolling");
                         ConfigEntryBuilder entryBuilder = ConfigEntryBuilder.create();
                         scrolling.addEntry(entryBuilder.startDropdownMenu("Easing Method", DropdownMenuBuilder.TopCellElementBuilder.of(easingMethod, str -> {
-                            for(EasingMethod m : EasingMethods.getMethods())
+                            for (EasingMethod m : EasingMethods.getMethods())
                                 if (m.toString().equals(str))
                                     return m;
                             return null;
@@ -136,6 +135,7 @@ public class ClothConfigInitializer implements ClientModInitializer {
                         ConfigCategory testing = builder.getOrCreateCategory("Testing");
                         testing.addEntry(entryBuilder.startDropdownMenu("lol apple", DropdownMenuBuilder.TopCellElementBuilder.ofItemObject(Items.APPLE), DropdownMenuBuilder.CellCreatorBuilder.ofItemObject()).setDefaultValue(Items.APPLE).setSelections(Registry.ITEM.stream().sorted(Comparator.comparing(Item::toString)).collect(Collectors.toSet())).setSaveConsumer(item -> System.out.println("save this " + item)).build());
                         testing.addEntry(entryBuilder.startKeyCodeField("Cool Key", InputUtil.UNKNOWN_KEYCODE).setDefaultValue(InputUtil.UNKNOWN_KEYCODE).build());
+                        testing.addEntry(entryBuilder.startModifierKeyCodeField("Cool Modifier Key", ModifierKeyCode.of(InputUtil.Type.KEYSYM.createFromCode(79), Modifier.of(false, true, false))).setDefaultValue(ModifierKeyCode.of(InputUtil.Type.KEYSYM.createFromCode(79), Modifier.of(false, true, false))).build());
                         builder.setSavingRunnable(() -> {
                             saveConfig();
                         });
@@ -148,6 +148,10 @@ public class ClothConfigInitializer implements ClientModInitializer {
             } catch (Exception e) {
                 ClothConfigInitializer.LOGGER.error("[ClothConfig] Failed to add test config override for ModMenu!", e);
             }
+            KeyBindingRegistry.INSTANCE.addCategory("Cloth Config");
+            FakeModifierKeyCodeAdder.INSTANCE.registerModifierKeyCode("Cloth Config", "unknown key lol", ModifierKeyCode.unknown(), keyCode -> {
+                System.out.println("new");
+            });
         }
     }
     

+ 5 - 1
src/main/java/me/shedaniel/clothconfig2/api/ConfigEntryBuilder.java

@@ -62,7 +62,11 @@ public interface ConfigEntryBuilder {
     
     LongSliderBuilder startLongSlider(String fieldNameKey, long value, long min, long max);
     
-    KeyCodeBuilder startKeyCodeField(String fieldNameKey, InputUtil.KeyCode value);
+    KeyCodeBuilder startModifierKeyCodeField(String fieldNameKey, ModifierKeyCode value);
+    
+    default KeyCodeBuilder startKeyCodeField(String fieldNameKey, InputUtil.KeyCode value) {
+        return startModifierKeyCodeField(fieldNameKey, ModifierKeyCode.of(value, Modifier.none())).setAllowModifiers(false);
+    }
     
     default KeyCodeBuilder fillKeybindingField(String fieldNameKey, KeyBinding value) {
         return startKeyCodeField(fieldNameKey, ((KeyCodeAccessor) value).getKeyCode()).setDefaultValue(value.getDefaultKeyCode()).setSaveConsumer(code -> {

+ 19 - 0
src/main/java/me/shedaniel/clothconfig2/api/FakeModifierKeyCodeAdder.java

@@ -0,0 +1,19 @@
+package me.shedaniel.clothconfig2.api;
+
+import me.shedaniel.clothconfig2.impl.FakeModifierKeyCodeAdderImpl;
+import net.minecraft.client.options.KeyBinding;
+
+import java.util.List;
+import java.util.function.Consumer;
+
+public interface FakeModifierKeyCodeAdder {
+    FakeModifierKeyCodeAdder INSTANCE = new FakeModifierKeyCodeAdderImpl();
+    
+    void registerModifierKeyCode(String category, String translationKey, ModifierKeyCode keyCode, ModifierKeyCode defaultKeyCode, Consumer<ModifierKeyCode> onChanged);
+    
+    default void registerModifierKeyCode(String category, String translationKey, ModifierKeyCode keyCode, Consumer<ModifierKeyCode> onChanged) {
+        registerModifierKeyCode(category, translationKey, keyCode, keyCode, onChanged);
+    }
+    
+    List<KeyBinding> getFakeBindings();
+}

+ 141 - 0
src/main/java/me/shedaniel/clothconfig2/api/Modifier.java

@@ -0,0 +1,141 @@
+package me.shedaniel.clothconfig2.api;
+
+import net.minecraft.client.gui.screen.Screen;
+
+import java.util.Objects;
+
+/**
+ * Taken from amecs for a lightweight modifers api:
+ * https://github.com/Siphalor/amecs
+ *
+ * @author Siphalor
+ */
+public class Modifier {
+    private short value;
+    
+    /**
+     * Constructs a new modifier object by a raw value
+     *
+     * @param value the raw value with flags set
+     */
+    private Modifier(short value) {
+        this.value = value;
+    }
+    
+    public static Modifier none() {
+        return of((short) 0);
+    }
+    
+    /**
+     * Constructs a new modifier object by all modifier bits
+     *
+     * @param alt     sets whether the alt flag should be set
+     * @param control sets whether the control flag should be set
+     * @param shift   sets whether the shift flag should be set
+     */
+    public static Modifier of(boolean alt, boolean control, boolean shift) {
+        short value = setFlag((short) 0, (short) 1, alt);
+        value = setFlag(value, (short) 2, control);
+        value = setFlag(value, (short) 4, shift);
+        return of(value);
+    }
+    
+    public static Modifier of(short value) {
+        return new Modifier(value);
+    }
+    
+    public static Modifier current() {
+        return Modifier.of(Screen.hasAltDown(), Screen.hasControlDown(), Screen.hasShiftDown());
+    }
+    
+    /**
+     * Compares this object with the current pressed keys
+     *
+     * @return whether the modifiers match in the current context
+     */
+    public boolean matchesCurrent() {
+        return equals(current());
+    }
+    
+    /**
+     * Gets the raw value
+     *
+     * @return the value with all flags set
+     */
+    public short getValue() {
+        return value;
+    }
+    
+    /**
+     * Gets the state of the alt flag
+     *
+     * @return whether the alt key needs to be pressed
+     */
+    public boolean hasAlt() {
+        return getFlag(value, (short) 1);
+    }
+    
+    /**
+     * Gets the state of the control flag
+     *
+     * @return whether the control key needs to be pressed
+     */
+    public boolean hasControl() {
+        return getFlag(value, (short) 2);
+    }
+    
+    /**
+     * Gets the state of the shift flag
+     *
+     * @return whether the shift key needs to be pressed
+     */
+    public boolean hasShift() {
+        return getFlag(value, (short) 4);
+    }
+    
+    /**
+     * Returns whether no flag is set
+     *
+     * @return value == 0
+     */
+    public boolean isEmpty() {
+        return value == 0;
+    }
+    
+    /**
+     * Returns whether this object equals another one
+     *
+     * @param other another modifier object
+     * @return whether both values are equal
+     */
+    @Override
+    public boolean equals(Object other) {
+        if (other == null)
+            return false;
+        if (!(other instanceof Modifier))
+            return false;
+        return value == ((Modifier) other).value;
+    }
+    
+    @Override
+    public int hashCode() {
+        return Objects.hash(value);
+    }
+    
+    private static short setFlag(short base, short flag, boolean val) {
+        return val ? setFlag(base, flag) : removeFlag(base, flag);
+    }
+    
+    private static short setFlag(short base, short flag) {
+        return (short) (base | flag);
+    }
+    
+    private static short removeFlag(short base, short flag) {
+        return (short) (base & (~flag));
+    }
+    
+    private static boolean getFlag(short base, short flag) {
+        return (base & flag) != 0;
+    }
+    
+}

+ 62 - 0
src/main/java/me/shedaniel/clothconfig2/api/ModifierKeyCode.java

@@ -0,0 +1,62 @@
+package me.shedaniel.clothconfig2.api;
+
+import me.shedaniel.clothconfig2.impl.ModifierKeyCodeImpl;
+import net.minecraft.client.util.InputUtil;
+
+public interface ModifierKeyCode {
+    InputUtil.KeyCode getKeyCode();
+    
+    default InputUtil.Type getType() {
+        return getKeyCode().getCategory();
+    }
+    
+    Modifier getModifier();
+    
+    default boolean matchesMouse(int button) {
+        return getType() == InputUtil.Type.MOUSE && getKeyCode().getKeyCode() == button && getModifier().matchesCurrent();
+    }
+    
+    default boolean matchesKey(int keyCode, int scanCode) {
+        if (keyCode == InputUtil.UNKNOWN_KEYCODE.getKeyCode()) {
+            return getType() == InputUtil.Type.SCANCODE && getKeyCode().getKeyCode() == scanCode && getModifier().matchesCurrent();
+        } else {
+            return getType() == InputUtil.Type.KEYSYM && getKeyCode().getKeyCode() == keyCode && getModifier().matchesCurrent();
+        }
+    }
+    
+    ModifierKeyCode setKeyCode(InputUtil.KeyCode keyCode);
+    
+    ModifierKeyCode setModifier(Modifier modifier);
+    
+    default ModifierKeyCode setKeyCodeAndModifier(InputUtil.KeyCode keyCode, Modifier modifier) {
+        setKeyCode(keyCode);
+        setModifier(modifier);
+        return this;
+    }
+    
+    default ModifierKeyCode clearModifier() {
+        return setModifier(Modifier.none());
+    }
+    
+    static ModifierKeyCode of(InputUtil.KeyCode keyCode, Modifier modifier) {
+        return new ModifierKeyCodeImpl().setKeyCodeAndModifier(keyCode, modifier);
+    }
+    
+    static ModifierKeyCode copyOf(ModifierKeyCode code) {
+        return of(code.getKeyCode(), code.getModifier());
+    }
+    
+    static ModifierKeyCode unknown() {
+        return of(InputUtil.UNKNOWN_KEYCODE, Modifier.none());
+    }
+    
+    String toString();
+    
+    default String getLocalizedName() {
+        return toString();
+    }
+    
+    default boolean isUnknown() {
+        return getKeyCode().equals(InputUtil.UNKNOWN_KEYCODE);
+    }
+}

+ 117 - 35
src/main/java/me/shedaniel/clothconfig2/gui/ClothConfigScreen.java

@@ -5,10 +5,7 @@ import com.google.common.collect.Maps;
 import com.google.common.util.concurrent.AtomicDouble;
 import com.mojang.blaze3d.systems.RenderSystem;
 import it.unimi.dsi.fastutil.booleans.BooleanConsumer;
-import me.shedaniel.clothconfig2.api.AbstractConfigEntry;
-import me.shedaniel.clothconfig2.api.AbstractConfigListEntry;
-import me.shedaniel.clothconfig2.api.QueuedTooltip;
-import me.shedaniel.clothconfig2.api.ScissorsHandler;
+import me.shedaniel.clothconfig2.api.*;
 import me.shedaniel.clothconfig2.gui.entries.KeyCodeEntry;
 import me.shedaniel.clothconfig2.gui.widget.DynamicElementListWidget;
 import me.shedaniel.math.api.Rectangle;
@@ -39,12 +36,12 @@ import java.util.Map;
 import java.util.Optional;
 import java.util.stream.Collectors;
 
-@SuppressWarnings("deprecation")
+@SuppressWarnings({"deprecation", "rawtypes", "unchecked", "DuplicatedCode"})
 public abstract class ClothConfigScreen extends Screen {
     
     private static final Identifier CONFIG_TEX = new Identifier("cloth-config2", "textures/gui/cloth_config.png");
     private final List<QueuedTooltip> queuedTooltips = Lists.newArrayList();
-    public KeyCodeEntry focusedBinding;
+    private KeyCodeEntry focusedBinding;
     public int nextTabIndex;
     public int selectedTabIndex;
     public double tabsScrollVelocity = 0d;
@@ -81,7 +78,7 @@ public abstract class ClothConfigScreen extends Screen {
         this.defaultBackgroundLocation = defaultBackgroundLocation;
         o.forEach((tab, pairs) -> {
             List<AbstractConfigEntry> list = Lists.newArrayList();
-            for(Pair<String, Object> pair : pairs) {
+            for (Pair<String, Object> pair : pairs) {
                 if (pair.getRight() instanceof AbstractConfigListEntry) {
                     list.add((AbstractConfigListEntry) pair.getRight());
                 } else {
@@ -95,7 +92,7 @@ public abstract class ClothConfigScreen extends Screen {
         this.tabs = tabbedEntries.keySet().stream().map(s -> new Pair<>(s, textRenderer.getStringWidth(I18n.translate(s)) + 8)).collect(Collectors.toList());
         this.nextTabIndex = 0;
         this.selectedTabIndex = 0;
-        for(int i = 0; i < tabs.size(); i++) {
+        for (int i = 0; i < tabs.size(); i++) {
             Pair<String, Integer> pair = tabs.get(i);
             if (pair.getLeft().equals(getFallbackCategory())) {
                 this.nextTabIndex = i;
@@ -148,7 +145,7 @@ public abstract class ClothConfigScreen extends Screen {
     @Override
     public void tick() {
         super.tick();
-        for(Element child : children())
+        for (Element child : children())
             if (child instanceof Tickable)
                 ((Tickable) child).tick();
     }
@@ -195,8 +192,8 @@ public abstract class ClothConfigScreen extends Screen {
     }
     
     public void saveAll(boolean openOtherScreens) {
-        for(List<AbstractConfigEntry> entries : Lists.newArrayList(tabbedEntries.values()))
-            for(AbstractConfigEntry entry : entries)
+        for (List<AbstractConfigEntry> entries : Lists.newArrayList(tabbedEntries.values()))
+            for (AbstractConfigEntry entry : entries)
                 entry.save();
         save();
         if (openOtherScreens) {
@@ -235,8 +232,8 @@ public abstract class ClothConfigScreen extends Screen {
             public void render(int int_1, int int_2, float float_1) {
                 boolean hasErrors = false;
                 if (displayErrors)
-                    for(List<AbstractConfigEntry> entries : Lists.newArrayList(tabbedEntries.values())) {
-                        for(AbstractConfigEntry entry : entries)
+                    for (List<AbstractConfigEntry> entries : Lists.newArrayList(tabbedEntries.values())) {
+                        for (AbstractConfigEntry entry : entries)
                             if (entry.getConfigError().isPresent()) {
                                 hasErrors = true;
                                 break;
@@ -274,11 +271,11 @@ public abstract class ClothConfigScreen extends Screen {
                 }
             });
             int j = 0;
-            for(Pair<String, Integer> tab : tabs) {
+            for (Pair<String, Integer> tab : tabs) {
                 tabButtons.add(new ClothConfigTabButton(this, j, -100, 43, tab.getRight(), 20, I18n.translate(tab.getLeft())));
                 j++;
             }
-            tabButtons.forEach(children::add);
+            children.addAll(tabButtons);
             children.add(buttonRightTab = new AbstractPressableButtonWidget(width - 16, 44, 12, 18, "") {
                 @Override
                 public void onPress() {
@@ -331,7 +328,7 @@ public abstract class ClothConfigScreen extends Screen {
     
     public void clampTabsScrolled() {
         int xx = 0;
-        for(ClothConfigTabButton tabButton : tabButtons)
+        for (ClothConfigTabButton tabButton : tabButtons)
             xx += tabButton.getWidth() + 2;
         if (xx > width - 40)
             tabsScrollProgress = MathHelper.clamp(tabsScrollProgress, 0, getTabsMaximumScrolled() - width + 40);
@@ -361,7 +358,7 @@ public abstract class ClothConfigScreen extends Screen {
                 clampTabsScrolled();
             }
             int xx = 24 - (int) tabsScrollProgress;
-            for(ClothConfigTabButton tabButton : tabButtons) {
+            for (ClothConfigTabButton tabButton : tabButtons) {
                 tabButton.x = xx;
                 xx += tabButton.getWidth() + 2;
             }
@@ -375,7 +372,7 @@ public abstract class ClothConfigScreen extends Screen {
         }
         listWidget.render(int_1, int_2, float_1);
         ScissorsHandler.INSTANCE.scissor(new Rectangle(listWidget.left, listWidget.top, listWidget.width, listWidget.bottom - listWidget.top));
-        for(AbstractConfigEntry child : listWidget.children())
+        for (AbstractConfigEntry child : listWidget.children())
             child.lateRender(int_1, int_2, float_1);
         ScissorsHandler.INSTANCE.removeLastScissor();
         if (isShowingTabs()) {
@@ -396,8 +393,8 @@ public abstract class ClothConfigScreen extends Screen {
         
         if (displayErrors && isEditable()) {
             List<String> errors = Lists.newArrayList();
-            for(List<AbstractConfigEntry> entries : Lists.newArrayList(tabbedEntries.values()))
-                for(AbstractConfigEntry entry : entries)
+            for (List<AbstractConfigEntry> entries : Lists.newArrayList(tabbedEntries.values()))
+                for (AbstractConfigEntry entry : entries)
                     if (entry.getConfigError().isPresent())
                         errors.add(((Optional<String>) entry.getConfigError()).get());
             if (errors.size() > 0) {
@@ -474,32 +471,118 @@ public abstract class ClothConfigScreen extends Screen {
         tessellator.draw();
     }
     
+    public void setFocusedBinding(KeyCodeEntry focusedBinding) {
+        this.focusedBinding = focusedBinding;
+        if (focusedBinding != null) {
+            startedKeyCode = this.focusedBinding.getValue();
+            startedKeyCode.setKeyCodeAndModifier(InputUtil.UNKNOWN_KEYCODE, Modifier.none());
+        } else
+            startedKeyCode = null;
+    }
+    
+    public KeyCodeEntry getFocusedBinding() {
+        return focusedBinding;
+    }
+    
+    private ModifierKeyCode startedKeyCode = null;
+    
+    @Override
+    public boolean mouseReleased(double double_1, double double_2, int int_1) {
+        if (this.focusedBinding != null && this.startedKeyCode != null && !this.startedKeyCode.isUnknown() && focusedBinding.isAllowMouse()) {
+            focusedBinding.setValue(startedKeyCode);
+            setFocusedBinding(null);
+            return true;
+        }
+        return super.mouseReleased(double_1, double_2, int_1);
+    }
+    
+    @Override
+    public boolean keyReleased(int int_1, int int_2, int int_3) {
+        if (this.focusedBinding != null && this.startedKeyCode != null && focusedBinding.isAllowKey()) {
+            focusedBinding.setValue(startedKeyCode);
+            setFocusedBinding(null);
+            return true;
+        }
+        return super.keyReleased(int_1, int_2, int_3);
+    }
+    
     @Override
     public boolean mouseClicked(double double_1, double double_2, int int_1) {
-        if (this.focusedBinding != null) {
-            if (focusedBinding.isAllowMouse())
-                focusedBinding.setValue(InputUtil.Type.MOUSE.createFromCode(int_1));
-            else
-                focusedBinding.setValue(InputUtil.UNKNOWN_KEYCODE);
-            this.focusedBinding = null;
+        if (this.focusedBinding != null && this.startedKeyCode != null && focusedBinding.isAllowMouse()) {
+            if (startedKeyCode.isUnknown())
+                startedKeyCode.setKeyCode(InputUtil.Type.MOUSE.createFromCode(int_1));
+            else if (focusedBinding.isAllowModifiers()) {
+                if (startedKeyCode.getType() == InputUtil.Type.KEYSYM) {
+                    int code = startedKeyCode.getKeyCode().getKeyCode();
+                    if (MinecraftClient.IS_SYSTEM_MAC ? (code == 343 || code == 347) : (code == 341 || code == 345)) {
+                        Modifier modifier = startedKeyCode.getModifier();
+                        startedKeyCode.setModifier(Modifier.of(modifier.hasAlt(), true, modifier.hasShift()));
+                        startedKeyCode.setKeyCode(InputUtil.Type.MOUSE.createFromCode(int_1));
+                        return true;
+                    } else if (code == 344 || code == 340) {
+                        Modifier modifier = startedKeyCode.getModifier();
+                        startedKeyCode.setModifier(Modifier.of(modifier.hasAlt(), modifier.hasControl(), true));
+                        startedKeyCode.setKeyCode(InputUtil.Type.MOUSE.createFromCode(int_1));
+                        return true;
+                    } else if (code == 342 || code == 346) {
+                        Modifier modifier = startedKeyCode.getModifier();
+                        startedKeyCode.setModifier(Modifier.of(true, modifier.hasControl(), modifier.hasShift()));
+                        startedKeyCode.setKeyCode(InputUtil.Type.MOUSE.createFromCode(int_1));
+                        return true;
+                    }
+                }
+            }
             return true;
         } else {
+            if (this.focusedBinding != null)
+                return true;
             return super.mouseClicked(double_1, double_2, int_1);
         }
     }
     
     @Override
     public boolean keyPressed(int int_1, int int_2, int int_3) {
-        if (this.focusedBinding != null) {
-            if (int_1 == 256 || !focusedBinding.isAllowKey()) {
-                focusedBinding.setValue(InputUtil.UNKNOWN_KEYCODE);
-            } else {
-                focusedBinding.setValue(InputUtil.getKeyCode(int_1, int_2));
+        if (this.focusedBinding != null && focusedBinding.isAllowKey()) {
+            if (startedKeyCode.isUnknown())
+                startedKeyCode.setKeyCode(InputUtil.getKeyCode(int_1, int_2));
+            else if (focusedBinding.isAllowModifiers()) {
+                if (startedKeyCode.getType() == InputUtil.Type.KEYSYM) {
+                    int code = startedKeyCode.getKeyCode().getKeyCode();
+                    if (MinecraftClient.IS_SYSTEM_MAC ? (code == 343 || code == 347) : (code == 341 || code == 345)) {
+                        Modifier modifier = startedKeyCode.getModifier();
+                        startedKeyCode.setModifier(Modifier.of(modifier.hasAlt(), true, modifier.hasShift()));
+                        startedKeyCode.setKeyCode(InputUtil.getKeyCode(int_1, int_2));
+                        return true;
+                    } else if (code == 344 || code == 340) {
+                        Modifier modifier = startedKeyCode.getModifier();
+                        startedKeyCode.setModifier(Modifier.of(modifier.hasAlt(), modifier.hasControl(), true));
+                        startedKeyCode.setKeyCode(InputUtil.getKeyCode(int_1, int_2));
+                        return true;
+                    } else if (code == 342 || code == 346) {
+                        Modifier modifier = startedKeyCode.getModifier();
+                        startedKeyCode.setModifier(Modifier.of(true, modifier.hasControl(), modifier.hasShift()));
+                        startedKeyCode.setKeyCode(InputUtil.getKeyCode(int_1, int_2));
+                        return true;
+                    }
+                }
+                if (MinecraftClient.IS_SYSTEM_MAC ? (int_1 == 343 || int_1 == 347) : (int_1 == 341 || int_1 == 345)) {
+                    Modifier modifier = startedKeyCode.getModifier();
+                    startedKeyCode.setModifier(Modifier.of(modifier.hasAlt(), true, modifier.hasShift()));
+                    return true;
+                } else if (int_1 == 344 || int_1 == 340) {
+                    Modifier modifier = startedKeyCode.getModifier();
+                    startedKeyCode.setModifier(Modifier.of(modifier.hasAlt(), modifier.hasControl(), true));
+                    return true;
+                } else if (int_1 == 342 || int_1 == 346) {
+                    Modifier modifier = startedKeyCode.getModifier();
+                    startedKeyCode.setModifier(Modifier.of(true, modifier.hasControl(), modifier.hasShift()));
+                    return true;
+                }
             }
-            
-            this.focusedBinding = null;
             return true;
         }
+        if (this.focusedBinding != null)
+            return true;
         if (int_1 == 256 && this.shouldCloseOnEsc()) {
             if (confirmSave && edited)
                 minecraft.openScreen(new ConfirmScreen(new QuitSaveConsumer(), new TranslatableText("text.cloth-config.quit_config"), new TranslatableText("text.cloth-config.quit_config_sure"), I18n.translate("text.cloth-config.quit_discard"), I18n.translate("gui.cancel")));
@@ -529,7 +612,6 @@ public abstract class ClothConfigScreen extends Screen {
                 minecraft.openScreen(ClothConfigScreen.this);
             else
                 minecraft.openScreen(parent);
-            return;
         }
     }
     
@@ -564,7 +646,7 @@ public abstract class ClothConfigScreen extends Screen {
             if (!this.isMouseOver(double_1, double_2)) {
                 return false;
             } else {
-                for(R entry : children()) {
+                for (R entry : children()) {
                     if (entry.mouseClicked(double_1, double_2, int_1)) {
                         this.setFocused(entry);
                         this.setDragging(true);

+ 24 - 31
src/main/java/me/shedaniel/clothconfig2/gui/entries/KeyCodeEntry.java

@@ -1,47 +1,51 @@
 package me.shedaniel.clothconfig2.gui.entries;
 
 import com.google.common.collect.Lists;
+import me.shedaniel.clothconfig2.api.ModifierKeyCode;
 import net.minecraft.client.MinecraftClient;
 import net.minecraft.client.gui.Element;
 import net.minecraft.client.gui.widget.ButtonWidget;
 import net.minecraft.client.resource.language.I18n;
-import net.minecraft.client.util.InputUtil;
 import net.minecraft.client.util.Window;
 import net.minecraft.util.Formatting;
 
 import java.util.List;
-import java.util.Objects;
 import java.util.Optional;
 import java.util.function.Consumer;
 import java.util.function.Supplier;
 
-public class KeyCodeEntry extends TooltipListEntry<InputUtil.KeyCode> {
+@SuppressWarnings("DuplicatedCode")
+public class KeyCodeEntry extends TooltipListEntry<ModifierKeyCode> {
     
-    private InputUtil.KeyCode value;
+    private ModifierKeyCode value;
     private ButtonWidget buttonWidget, resetButton;
-    private Consumer<InputUtil.KeyCode> saveConsumer;
-    private Supplier<InputUtil.KeyCode> defaultValue;
+    private Consumer<ModifierKeyCode> saveConsumer;
+    private Supplier<ModifierKeyCode> defaultValue;
     private List<Element> widgets;
-    private boolean allowMouse = true, allowKey = true;
+    private boolean allowMouse = true, allowKey = true, allowModifiers = true;
     
     @Deprecated
-    public KeyCodeEntry(String fieldName, InputUtil.KeyCode value, String resetButtonKey, Supplier<InputUtil.KeyCode> defaultValue, Consumer<InputUtil.KeyCode> saveConsumer, Supplier<Optional<String[]>> tooltipSupplier, boolean requiresRestart) {
+    public KeyCodeEntry(String fieldName, ModifierKeyCode value, String resetButtonKey, Supplier<ModifierKeyCode> defaultValue, Consumer<ModifierKeyCode> saveConsumer, Supplier<Optional<String[]>> tooltipSupplier, boolean requiresRestart) {
         super(fieldName, tooltipSupplier, requiresRestart);
         this.defaultValue = defaultValue;
         this.value = value;
         this.buttonWidget = new ButtonWidget(0, 0, 150, 20, "", widget -> {
-            getScreen().focusedBinding = this;
+            getScreen().setFocusedBinding(this);
             getScreen().setEdited(true, isRequiresRestart());
         });
         this.resetButton = new ButtonWidget(0, 0, MinecraftClient.getInstance().textRenderer.getStringWidth(I18n.translate(resetButtonKey)) + 6, 20, I18n.translate(resetButtonKey), widget -> {
             KeyCodeEntry.this.value = getDefaultValue().get();
-            getScreen().focusedBinding = null;
+            getScreen().setFocusedBinding(null);
             getScreen().setEdited(true, isRequiresRestart());
         });
         this.saveConsumer = saveConsumer;
         this.widgets = Lists.newArrayList(buttonWidget, resetButton);
     }
     
+    public void setAllowModifiers(boolean allowModifiers) {
+        this.allowModifiers = allowModifiers;
+    }
+    
     public void setAllowKey(boolean allowKey) {
         this.allowKey = allowKey;
     }
@@ -50,6 +54,10 @@ public class KeyCodeEntry extends TooltipListEntry<InputUtil.KeyCode> {
         this.allowMouse = allowMouse;
     }
     
+    public boolean isAllowModifiers() {
+        return allowModifiers;
+    }
+    
     public boolean isAllowKey() {
         return allowKey;
     }
@@ -58,7 +66,7 @@ public class KeyCodeEntry extends TooltipListEntry<InputUtil.KeyCode> {
         return allowMouse;
     }
     
-    public void setValue(InputUtil.KeyCode value) {
+    public void setValue(ModifierKeyCode value) {
         this.value = value;
     }
     
@@ -69,31 +77,17 @@ public class KeyCodeEntry extends TooltipListEntry<InputUtil.KeyCode> {
     }
     
     @Override
-    public InputUtil.KeyCode getValue() {
+    public ModifierKeyCode getValue() {
         return value;
     }
     
     @Override
-    public Optional<InputUtil.KeyCode> getDefaultValue() {
+    public Optional<ModifierKeyCode> getDefaultValue() {
         return Optional.ofNullable(defaultValue).map(Supplier::get);
     }
     
     private String getLocalizedName() {
-        String string_1 = this.value.getName();
-        int int_1 = this.value.getKeyCode();
-        String string_2 = null;
-        switch (this.value.getCategory()) {
-            case KEYSYM:
-                string_2 = InputUtil.getKeycodeName(int_1);
-                break;
-            case SCANCODE:
-                string_2 = InputUtil.getScancodeName(int_1);
-                break;
-            case MOUSE:
-                String string_3 = I18n.translate(string_1);
-                string_2 = Objects.equals(string_3, string_1) ? I18n.translate(InputUtil.Type.MOUSE.getName(), int_1 + 1) : string_3;
-        }
-        return string_2 == null ? I18n.translate(string_1) : string_2;
+        return this.value.getLocalizedName();
     }
     
     @Override
@@ -105,19 +99,18 @@ public class KeyCodeEntry extends TooltipListEntry<InputUtil.KeyCode> {
         this.buttonWidget.active = isEditable();
         this.buttonWidget.y = y;
         this.buttonWidget.setMessage(getLocalizedName());
-        if (getScreen().focusedBinding == this)
+        if (getScreen().getFocusedBinding() == this)
             this.buttonWidget.setMessage(Formatting.WHITE + "> " + Formatting.YELLOW + this.buttonWidget.getMessage() + Formatting.WHITE + " <");
         if (MinecraftClient.getInstance().textRenderer.isRightToLeft()) {
             MinecraftClient.getInstance().textRenderer.drawWithShadow(I18n.translate(getFieldName()), window.getScaledWidth() - x - MinecraftClient.getInstance().textRenderer.getStringWidth(I18n.translate(getFieldName())), y + 5, 16777215);
             this.resetButton.x = x;
             this.buttonWidget.x = x + resetButton.getWidth() + 2;
-            this.buttonWidget.setWidth(150 - resetButton.getWidth() - 2);
         } else {
             MinecraftClient.getInstance().textRenderer.drawWithShadow(I18n.translate(getFieldName()), x, y + 5, getPreferredTextColor());
             this.resetButton.x = x + entryWidth - resetButton.getWidth();
             this.buttonWidget.x = x + entryWidth - 150;
-            this.buttonWidget.setWidth(150 - resetButton.getWidth() - 2);
         }
+        this.buttonWidget.setWidth(150 - resetButton.getWidth() - 2);
         resetButton.render(mouseX, mouseY, delta);
         buttonWidget.render(mouseX, mouseY, delta);
     }

+ 2 - 2
src/main/java/me/shedaniel/clothconfig2/impl/ConfigEntryBuilderImpl.java

@@ -2,10 +2,10 @@ package me.shedaniel.clothconfig2.impl;
 
 import me.shedaniel.clothconfig2.api.AbstractConfigListEntry;
 import me.shedaniel.clothconfig2.api.ConfigEntryBuilder;
+import me.shedaniel.clothconfig2.api.ModifierKeyCode;
 import me.shedaniel.clothconfig2.gui.entries.DropdownBoxEntry.SelectionCellCreator;
 import me.shedaniel.clothconfig2.gui.entries.DropdownBoxEntry.SelectionTopCellElement;
 import me.shedaniel.clothconfig2.impl.builders.*;
-import net.minecraft.client.util.InputUtil;
 
 import java.util.List;
 import java.util.UUID;
@@ -139,7 +139,7 @@ public class ConfigEntryBuilderImpl implements ConfigEntryBuilder {
     }
     
     @Override
-    public KeyCodeBuilder startKeyCodeField(String fieldNameKey, InputUtil.KeyCode value) {
+    public KeyCodeBuilder startModifierKeyCodeField(String fieldNameKey, ModifierKeyCode value) {
         return new KeyCodeBuilder(resetButtonKey, fieldNameKey, value);
     }
     

+ 78 - 0
src/main/java/me/shedaniel/clothconfig2/impl/FakeKeyBindings.java

@@ -0,0 +1,78 @@
+package me.shedaniel.clothconfig2.impl;
+
+import me.shedaniel.clothconfig2.api.ModifierKeyCode;
+import net.fabricmc.fabric.mixin.client.keybinding.KeyCodeAccessor;
+import net.minecraft.client.options.KeyBinding;
+import net.minecraft.client.util.InputUtil;
+
+import java.util.UUID;
+import java.util.function.Consumer;
+
+public class FakeKeyBindings extends KeyBinding {
+    private UUID uuid;
+    private ModifierKeyCode keyCode;
+    private ModifierKeyCode defaultKeyCode;
+    private Consumer<ModifierKeyCode> onChanged;
+    
+    public FakeKeyBindings(String key, ModifierKeyCode keyCode, ModifierKeyCode defaultKeyCode, String category, Consumer<ModifierKeyCode> onChanged) {
+        super(UUID.randomUUID().toString(), InputUtil.Type.KEYSYM, -1, category);
+        uuid = UUID.fromString(getId());
+        ((KeyBindingHooks) this).cloth_setId("ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ" + key);
+        this.keyCode = keyCode;
+        this.defaultKeyCode = ModifierKeyCode.copyOf(defaultKeyCode);
+        this.onChanged = onChanged;
+    }
+    
+    @Override
+    public InputUtil.KeyCode getDefaultKeyCode() {
+        return defaultKeyCode.getKeyCode();
+    }
+    
+    @Override
+    public void setKeyCode(InputUtil.KeyCode inputUtil$KeyCode_1) {
+        keyCode.setKeyCode(inputUtil$KeyCode_1);
+        onChanged.accept(keyCode);
+    }
+    
+    @Override
+    public boolean equals(KeyBinding keyBinding_1) {
+        return keyCode.getKeyCode().equals(((KeyCodeAccessor) keyBinding_1).getKeyCode());
+    }
+    
+    @Override
+    public boolean isNotBound() {
+        return keyCode.isUnknown();
+    }
+    
+    @Override
+    public boolean matchesKey(int int_1, int int_2) {
+        return keyCode.matchesKey(int_1, int_2);
+    }
+    
+    @Override
+    public boolean matchesMouse(int int_1) {
+        return keyCode.matchesMouse(int_1);
+    }
+    
+    @Override
+    public String getLocalizedName() {
+        return keyCode.getLocalizedName();
+    }
+    
+    @Override
+    public boolean isDefault() {
+        return keyCode.equals(defaultKeyCode);
+    }
+    
+    @Override
+    public String getName() {
+        return keyCode.getLocalizedName();
+    }
+    
+    @Override
+    public String getId() {
+        if (uuid == null)
+            return super.getId();
+        return super.getId().substring(77);
+    }
+}

+ 43 - 0
src/main/java/me/shedaniel/clothconfig2/impl/FakeModifierKeyCodeAdderImpl.java

@@ -0,0 +1,43 @@
+package me.shedaniel.clothconfig2.impl;
+
+import me.shedaniel.clothconfig2.api.ModifierKeyCode;
+import me.shedaniel.clothconfig2.api.FakeModifierKeyCodeAdder;
+import net.minecraft.client.options.KeyBinding;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+
+public class FakeModifierKeyCodeAdderImpl implements FakeModifierKeyCodeAdder {
+    private List<Entry> entryList = new ArrayList<>();
+    
+    @Override
+    public void registerModifierKeyCode(String category, String translationKey, ModifierKeyCode keyCode, ModifierKeyCode defaultKeyCode, Consumer<ModifierKeyCode> onChanged) {
+        entryList.add(new Entry(category, translationKey, keyCode, defaultKeyCode, onChanged));
+    }
+    
+    @Override
+    public List<KeyBinding> getFakeBindings() {
+        List<KeyBinding> keyBindings = new ArrayList<>();
+        for (Entry entry : entryList) {
+            keyBindings.add(new FakeKeyBindings(entry.translationKey, entry.keyCode, entry.defaultKeyCode, entry.category, entry.onChanged));
+        }
+        return keyBindings;
+    }
+    
+    private class Entry {
+        private String category;
+        private String translationKey;
+        private ModifierKeyCode keyCode;
+        private ModifierKeyCode defaultKeyCode;
+        private Consumer<ModifierKeyCode> onChanged;
+        
+        private Entry(String category, String translationKey, ModifierKeyCode keyCode, ModifierKeyCode defaultKeyCode, Consumer<ModifierKeyCode> onChanged) {
+            this.category = category;
+            this.translationKey = translationKey;
+            this.keyCode = keyCode;
+            this.defaultKeyCode = defaultKeyCode;
+            this.onChanged = onChanged;
+        }
+    }
+}

+ 7 - 0
src/main/java/me/shedaniel/clothconfig2/impl/GameOptionsHooks.java

@@ -0,0 +1,7 @@
+package me.shedaniel.clothconfig2.impl;
+
+import net.minecraft.client.options.KeyBinding;
+
+public interface GameOptionsHooks {
+    void cloth_setKeysAll(KeyBinding[] all);
+}

+ 5 - 0
src/main/java/me/shedaniel/clothconfig2/impl/KeyBindingHooks.java

@@ -0,0 +1,5 @@
+package me.shedaniel.clothconfig2.impl;
+
+public interface KeyBindingHooks {
+    void cloth_setId(String id);
+}

+ 83 - 0
src/main/java/me/shedaniel/clothconfig2/impl/ModifierKeyCodeImpl.java

@@ -0,0 +1,83 @@
+package me.shedaniel.clothconfig2.impl;
+
+import me.shedaniel.clothconfig2.api.Modifier;
+import me.shedaniel.clothconfig2.api.ModifierKeyCode;
+import net.minecraft.client.resource.language.I18n;
+import net.minecraft.client.util.InputUtil;
+
+import java.util.Objects;
+
+public class ModifierKeyCodeImpl implements ModifierKeyCode {
+    private InputUtil.KeyCode keyCode;
+    private Modifier modifier;
+    
+    public ModifierKeyCodeImpl() {
+    }
+    
+    @Override
+    public InputUtil.KeyCode getKeyCode() {
+        return keyCode;
+    }
+    
+    @Override
+    public Modifier getModifier() {
+        return modifier;
+    }
+    
+    @Override
+    public ModifierKeyCode setKeyCode(InputUtil.KeyCode keyCode) {
+        this.keyCode = keyCode.getCategory().createFromCode(keyCode.getKeyCode());
+        if (keyCode.equals(InputUtil.UNKNOWN_KEYCODE))
+            setModifier(Modifier.none());
+        return this;
+    }
+    
+    @Override
+    public ModifierKeyCode setModifier(Modifier modifier) {
+        this.modifier = Modifier.of(modifier.getValue());
+        return this;
+    }
+    
+    @Override
+    public String toString() {
+        String string_1 = this.keyCode.getName();
+        int int_1 = this.keyCode.getKeyCode();
+        String string_2 = null;
+        switch (this.keyCode.getCategory()) {
+            case KEYSYM:
+                string_2 = InputUtil.getKeycodeName(int_1);
+                break;
+            case SCANCODE:
+                string_2 = InputUtil.getScancodeName(int_1);
+                break;
+            case MOUSE:
+                String string_3 = I18n.translate(string_1);
+                string_2 = Objects.equals(string_3, string_1) ? I18n.translate(InputUtil.Type.MOUSE.getName(), int_1 + 1) : string_3;
+        }
+        String base = string_2 == null ? I18n.translate(string_1) : string_2;
+        if (modifier.hasShift())
+            base = I18n.translate("modifier.cloth-config.shift", base);
+        if (modifier.hasControl())
+            base = I18n.translate("modifier.cloth-config.ctrl", base);
+        if (modifier.hasAlt())
+            base = I18n.translate("modifier.cloth-config.alt", base);
+        return base;
+    }
+    
+    @Override
+    public boolean equals(Object o) {
+        if (this == o)
+            return true;
+        if (!(o instanceof ModifierKeyCode))
+            return false;
+        ModifierKeyCode that = (ModifierKeyCode) o;
+        return keyCode.equals(that.getKeyCode()) && modifier.equals(that.getModifier());
+    }
+    
+    @Override
+    public int hashCode() {
+        int result = keyCode != null ? keyCode.hashCode() : 0;
+        result = 31 * result + (modifier != null ? modifier.hashCode() : 0);
+        return result;
+    }
+}

+ 38 - 8
src/main/java/me/shedaniel/clothconfig2/impl/builders/KeyCodeBuilder.java

@@ -1,5 +1,7 @@
 package me.shedaniel.clothconfig2.impl.builders;
 
+import me.shedaniel.clothconfig2.api.Modifier;
+import me.shedaniel.clothconfig2.api.ModifierKeyCode;
 import me.shedaniel.clothconfig2.gui.entries.KeyCodeEntry;
 import net.minecraft.client.util.InputUtil;
 
@@ -10,16 +12,23 @@ import java.util.function.Consumer;
 import java.util.function.Function;
 import java.util.function.Supplier;
 
-public class KeyCodeBuilder extends FieldBuilder<InputUtil.KeyCode, KeyCodeEntry> {
+public class KeyCodeBuilder extends FieldBuilder<ModifierKeyCode, KeyCodeEntry> {
     
-    @Nullable private Consumer<InputUtil.KeyCode> saveConsumer = null;
-    @Nonnull private Function<InputUtil.KeyCode, Optional<String[]>> tooltipSupplier = bool -> Optional.empty();
-    private InputUtil.KeyCode value;
-    private boolean allowKey = true, allowMouse = true;
+    @Nullable private Consumer<ModifierKeyCode> saveConsumer = null;
+    @Nonnull private Function<ModifierKeyCode, Optional<String[]>> tooltipSupplier = bool -> Optional.empty();
+    private ModifierKeyCode value;
+    private boolean allowKey = true, allowMouse = true, allowModifiers = true;
     
-    public KeyCodeBuilder(String resetButtonKey, String fieldNameKey, InputUtil.KeyCode value) {
+    public KeyCodeBuilder(String resetButtonKey, String fieldNameKey, ModifierKeyCode value) {
         super(resetButtonKey, fieldNameKey);
-        this.value = value;
+        this.value = ModifierKeyCode.copyOf(value);
+    }
+    
+    public KeyCodeBuilder setAllowModifiers(boolean allowModifiers) {
+        this.allowModifiers = allowModifiers;
+        if (!allowModifiers)
+            value.setModifier(Modifier.none());
+        return this;
     }
     
     public KeyCodeBuilder setAllowKey(boolean allowKey) {
@@ -37,6 +46,10 @@ public class KeyCodeBuilder extends FieldBuilder<InputUtil.KeyCode, KeyCodeEntry
     }
     
     public KeyCodeBuilder setErrorSupplier(@Nullable Function<InputUtil.KeyCode, Optional<String>> errorSupplier) {
+        return setModifierErrorSupplier(keyCode -> errorSupplier.apply(keyCode.getKeyCode()));
+    }
+    
+    public KeyCodeBuilder setModifierErrorSupplier(@Nullable Function<ModifierKeyCode, Optional<String>> errorSupplier) {
         this.errorSupplier = errorSupplier;
         return this;
     }
@@ -47,21 +60,37 @@ public class KeyCodeBuilder extends FieldBuilder<InputUtil.KeyCode, KeyCodeEntry
     }
     
     public KeyCodeBuilder setSaveConsumer(Consumer<InputUtil.KeyCode> saveConsumer) {
+        return setModifierSaveConsumer(keyCode -> saveConsumer.accept(keyCode.getKeyCode()));
+    }
+    
+    public KeyCodeBuilder setDefaultValue(Supplier<InputUtil.KeyCode> defaultValue) {
+        return setModifierDefaultValue(() -> ModifierKeyCode.of(defaultValue.get(), Modifier.none()));
+    }
+    
+    public KeyCodeBuilder setModifierSaveConsumer(Consumer<ModifierKeyCode> saveConsumer) {
         this.saveConsumer = saveConsumer;
         return this;
     }
     
-    public KeyCodeBuilder setDefaultValue(Supplier<InputUtil.KeyCode> defaultValue) {
+    public KeyCodeBuilder setModifierDefaultValue(Supplier<ModifierKeyCode> defaultValue) {
         this.defaultValue = defaultValue;
         return this;
     }
     
     public KeyCodeBuilder setDefaultValue(InputUtil.KeyCode defaultValue) {
+        return setDefaultValue(ModifierKeyCode.of(defaultValue, Modifier.none()));
+    }
+    
+    public KeyCodeBuilder setDefaultValue(ModifierKeyCode defaultValue) {
         this.defaultValue = () -> defaultValue;
         return this;
     }
     
     public KeyCodeBuilder setTooltipSupplier(@Nonnull Function<InputUtil.KeyCode, Optional<String[]>> tooltipSupplier) {
+        return setModifierTooltipSupplier(keyCode -> tooltipSupplier.apply(keyCode.getKeyCode()));
+    }
+    
+    public KeyCodeBuilder setModifierTooltipSupplier(@Nonnull Function<ModifierKeyCode, Optional<String[]>> tooltipSupplier) {
         this.tooltipSupplier = tooltipSupplier;
         return this;
     }
@@ -89,6 +118,7 @@ public class KeyCodeBuilder extends FieldBuilder<InputUtil.KeyCode, KeyCodeEntry
             entry.setErrorSupplier(() -> errorSupplier.apply(entry.getValue()));
         entry.setAllowKey(allowKey);
         entry.setAllowMouse(allowMouse);
+        entry.setAllowModifiers(allowModifiers);
         return entry;
     }
     

+ 52 - 0
src/main/java/me/shedaniel/clothconfig2/mixin/MixinControlsOptionsScreen.java

@@ -0,0 +1,52 @@
+package me.shedaniel.clothconfig2.mixin;
+
+import me.shedaniel.clothconfig2.api.FakeModifierKeyCodeAdder;
+import me.shedaniel.clothconfig2.impl.FakeKeyBindings;
+import me.shedaniel.clothconfig2.impl.GameOptionsHooks;
+import net.minecraft.client.gui.screen.Screen;
+import net.minecraft.client.gui.screen.options.ControlsOptionsScreen;
+import net.minecraft.client.gui.screen.options.GameOptionsScreen;
+import net.minecraft.client.options.GameOptions;
+import net.minecraft.client.options.KeyBinding;
+import net.minecraft.text.Text;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Mixin(ControlsOptionsScreen.class)
+public class MixinControlsOptionsScreen extends GameOptionsScreen {
+    public MixinControlsOptionsScreen(Screen screen_1, GameOptions gameOptions_1, Text text_1) {
+        super(screen_1, gameOptions_1, text_1);
+    }
+    
+    @Inject(method = "init", at = @At("HEAD"))
+    private void initHead(CallbackInfo info) {
+        List<KeyBinding> newKeysAll = new ArrayList<>();
+        KeyBinding[] var3 = minecraft.options.keysAll;
+        
+        for (KeyBinding binding : var3) {
+            if (!(binding instanceof FakeKeyBindings)) {
+                newKeysAll.add(binding);
+            }
+        }
+        
+        newKeysAll.addAll(FakeModifierKeyCodeAdder.INSTANCE.getFakeBindings());
+        ((GameOptionsHooks) minecraft.options).cloth_setKeysAll(newKeysAll.toArray(new KeyBinding[0]));
+    }
+    
+    @Inject(method = "init", at = @At("RETURN"))
+    private void initReturn(CallbackInfo info) {
+        List<KeyBinding> newKeysAll = new ArrayList<>();
+        KeyBinding[] var3 = minecraft.options.keysAll;
+        for (KeyBinding binding : var3) {
+            if (!(binding instanceof FakeKeyBindings)) {
+                newKeysAll.add(binding);
+            }
+        }
+        ((GameOptionsHooks) minecraft.options).cloth_setKeysAll(newKeysAll.toArray(new KeyBinding[0]));
+    }
+}

+ 19 - 0
src/main/java/me/shedaniel/clothconfig2/mixin/MixinGameOptions.java

@@ -0,0 +1,19 @@
+package me.shedaniel.clothconfig2.mixin;
+
+import me.shedaniel.clothconfig2.impl.GameOptionsHooks;
+import net.minecraft.client.options.GameOptions;
+import net.minecraft.client.options.KeyBinding;
+import org.spongepowered.asm.mixin.Final;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Mutable;
+import org.spongepowered.asm.mixin.Shadow;
+
+@Mixin(GameOptions.class)
+public class MixinGameOptions implements GameOptionsHooks {
+    @Shadow @Mutable @Final public KeyBinding[] keysAll;
+    
+    @Override
+    public void cloth_setKeysAll(KeyBinding[] all) {
+        keysAll = all;
+    }
+}

+ 18 - 0
src/main/java/me/shedaniel/clothconfig2/mixin/MixinKeyBinding.java

@@ -0,0 +1,18 @@
+package me.shedaniel.clothconfig2.mixin;
+
+import me.shedaniel.clothconfig2.impl.KeyBindingHooks;
+import net.minecraft.client.options.KeyBinding;
+import org.spongepowered.asm.mixin.Final;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Mutable;
+import org.spongepowered.asm.mixin.Shadow;
+
+@Mixin(KeyBinding.class)
+public class MixinKeyBinding implements KeyBindingHooks {
+    @Shadow @Mutable @Final private String id;
+    
+    @Override
+    public void cloth_setId(String id) {
+        this.id = id;
+    }
+}

+ 4 - 1
src/main/resources/assets/cloth-config2/lang/en_us.json

@@ -24,5 +24,8 @@
   "text.cloth-config.ignore_restart": "Ignore Restart",
   "text.cloth-config.boolean.value.true": "§aYes",
   "text.cloth-config.boolean.value.false": "§cNo",
-  "text.cloth-config.dropdown.value.unknown": "§cNo suggestions"
+  "text.cloth-config.dropdown.value.unknown": "§cNo suggestions",
+  "modifier.cloth-config.alt": "Alt + %s",
+  "modifier.cloth-config.ctrl": "Ctrl + %s",
+  "modifier.cloth-config.shift": "Shift + %s"
 }

+ 3 - 1
src/main/resources/fabric.mod.json

@@ -20,11 +20,13 @@
       "me.shedaniel.clothconfig2.ClothConfigInitializer"
     ]
   },
+  "mixins": [
+    "mixin.cloth-config2.json"
+  ],
   "depends": {
     "fabricloader": ">=0.4.0"
   },
   "custom": {
-    "modmenu:api": true,
     "modmenu:clientsideOnly": true
   }
 }

+ 15 - 0
src/main/resources/mixin.cloth-config2.json

@@ -0,0 +1,15 @@
+{
+  "required": true,
+  "package": "me.shedaniel.clothconfig2.mixin",
+  "minVersion": "0.7.11",
+  "compatibilityLevel": "JAVA_8",
+  "mixins": [],
+  "client": [
+    "MixinControlsOptionsScreen",
+    "MixinGameOptions",
+    "MixinKeyBinding"
+  ],
+  "injectors": {
+    "defaultRequire": 1
+  }
+}