Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Key binding context #3286

Open
wants to merge 15 commits into
base: 1.20.1
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public boolean addCategory(String categoryName) {

@Override
public boolean register(FabricKeyBinding binding) {
return KeyBindingRegistryImpl.registerKeyBinding(binding) != null;
return KeyBindingHelper.registerKeyBinding(binding) != null;
}
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package net.fabricmc.fabric.api.client.keybinding.v1;

import org.jetbrains.annotations.ApiStatus;

import net.minecraft.client.MinecraftClient;
import net.minecraft.client.option.KeyBinding;

import net.fabricmc.fabric.impl.client.keybinding.KeyBindingContextImpl;
import net.fabricmc.fabric.impl.client.keybinding.KeyBindingExtensions;

/**
* {@link KeyBindingContext} decides how {@link KeyBinding} with same bounded key behaves in regard to each other.
*
* <p>Bindings with different context will not conflict with each other even if they have the same bounded key.
*
* <p>Along with the provided generic contexts, mods can also create its own context by implementing this interface.
*/
public interface KeyBindingContext {
/**
* {@link KeyBinding} that used in-game. All vanilla key binds have this context.
*/
KeyBindingContext IN_GAME = KeyBindingContextImpl.IN_GAME;

/**
* {@link KeyBinding} that used when a screen is open.
*/
KeyBindingContext IN_SCREEN = KeyBindingContextImpl.IN_SCREEN;

/**
* {@link KeyBinding} that might be used in any context. This context conflicts with any other context.
*/
KeyBindingContext ALL = KeyBindingContextImpl.ALL;

/**
* Returns the context of the key binding.
*/
static KeyBindingContext of(KeyBinding binding) {
return ((KeyBindingExtensions) binding).fabric_getContext();
}

/**
* Returns whether one context conflicts with the other.
*/
static boolean conflicts(KeyBindingContext left, KeyBindingContext right) {
return left.conflicts(right) || right.conflicts(left);
}

/**
* Returns whether the key bind can be activated in the current state of the game.
* If not, {@link KeyBinding#isPressed()} and {@link KeyBinding#wasPressed()} will always return {@code false}.
*/
boolean isActive(MinecraftClient client);

/**
* Returns whether this context conflict with the other.
*
* <p>Do not call! Use {@link #conflicts(KeyBindingContext, KeyBindingContext)} instead.
*
* <p>Along with the same instance, most custom context implementation should probably conflict with either
* {@link #IN_GAME} or {@link #IN_SCREEN}, unless it needs to be called alongside the generic context.
* <pre>return this == other || other == IN_GAME;</pre>
*/
@ApiStatus.OverrideOnly
boolean conflicts(KeyBindingContext other);
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,21 @@ private KeyBindingHelper() {
* @throws IllegalArgumentException when a key binding with the same ID is already registered
*/
public static KeyBinding registerKeyBinding(KeyBinding keyBinding) {
return registerKeyBinding(keyBinding, KeyBindingContext.IN_GAME);
deirn marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Registers the keybinding and add the keybinding category if required.
*
* @param keyBinding the keybinding
* @param context the keybinding context
* @return the keybinding itself
* @throws IllegalArgumentException when a key binding with the same ID is already registered
*/
public static KeyBinding registerKeyBinding(KeyBinding keyBinding, KeyBindingContext context) {
Objects.requireNonNull(keyBinding, "key binding cannot be null");
deirn marked this conversation as resolved.
Show resolved Hide resolved
return KeyBindingRegistryImpl.registerKeyBinding(keyBinding);
Objects.requireNonNull(context, "context cannot be null");
return KeyBindingRegistryImpl.registerKeyBinding(keyBinding, context);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package net.fabricmc.fabric.impl.client.keybinding;

import net.minecraft.client.MinecraftClient;

import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingContext;

public class KeyBindingContextImpl {
public static final KeyBindingContext IN_GAME = new KeyBindingContext() {
@Override
public boolean isActive(MinecraftClient client) {
return client.currentScreen == null;
}

@Override
public boolean conflicts(KeyBindingContext other) {
return this == other;
}
};

public static final KeyBindingContext IN_SCREEN = new KeyBindingContext() {
@Override
public boolean isActive(MinecraftClient client) {
return client.currentScreen != null;
}

@Override
public boolean conflicts(KeyBindingContext other) {
return this == other;
}
};

public static final KeyBindingContext ALL = new KeyBindingContext() {
@Override
public boolean isActive(MinecraftClient client) {
return true;
}

@Override
public boolean conflicts(KeyBindingContext other) {
return true;
}
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package net.fabricmc.fabric.impl.client.keybinding;

import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingContext;

public interface KeyBindingExtensions {
KeyBindingContext fabric_getContext();

void fabric_setContext(KeyBindingContext context);
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

package net.fabricmc.fabric.impl.client.keybinding;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
Expand All @@ -25,10 +27,13 @@

import net.minecraft.client.MinecraftClient;
import net.minecraft.client.option.KeyBinding;
import net.minecraft.client.util.InputUtil;

import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingContext;
import net.fabricmc.fabric.mixin.client.keybinding.KeyBindingAccessor;

public final class KeyBindingRegistryImpl {
public static final Map<InputUtil.Key, List<KeyBinding>> KEY_TO_BINDINGS = new HashMap<>();
private static final List<KeyBinding> MODDED_KEY_BINDINGS = new ReferenceArrayList<>(); // ArrayList with identity based comparisons for contains/remove/indexOf etc., required for correctly handling duplicate keybinds

private KeyBindingRegistryImpl() {
Expand All @@ -51,7 +56,7 @@ public static boolean addCategory(String categoryTranslationKey) {
return true;
}

public static KeyBinding registerKeyBinding(KeyBinding binding) {
public static KeyBinding registerKeyBinding(KeyBinding binding, KeyBindingContext context) {
if (MinecraftClient.getInstance().options != null) {
throw new IllegalStateException("GameOptions has already been initialised");
}
Expand All @@ -66,6 +71,7 @@ public static KeyBinding registerKeyBinding(KeyBinding binding) {

// This will do nothing if the category already exists.
addCategory(binding.getCategory());
((KeyBindingExtensions) binding).fabric_setContext(context);
MODDED_KEY_BINDINGS.add(binding);
return binding;
}
Expand All @@ -80,4 +86,8 @@ public static KeyBinding[] process(KeyBinding[] keysAll) {
newKeysAll.addAll(MODDED_KEY_BINDINGS);
return newKeysAll.toArray(new KeyBinding[0]);
}

public static void putToMap(InputUtil.Key key, KeyBinding binding) {
KEY_TO_BINDINGS.computeIfAbsent(key, k -> new ArrayList<>()).add(binding);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package net.fabricmc.fabric.mixin.client.keybinding;

import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.Constant;
import org.spongepowered.asm.mixin.injection.ModifyConstant;

import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.screen.option.ControlsListWidget;
import net.minecraft.client.option.KeyBinding;

import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingContext;

@Mixin(ControlsListWidget.KeyBindingEntry.class)
public abstract class KeyBindingEntryMixin {
@Shadow
@Final
private KeyBinding binding;

@ModifyConstant(method = "update", constant = @Constant(stringValue = ", "))
private String makeConflictTextMultiline(String constant) {
return "\n";
}

@ModifyConstant(method = "update", constant = @Constant(stringValue = "controls.keybinds.duplicateKeybinds"))
private String replaceConflictText(String constant) {
for (KeyBinding otherBinding : MinecraftClient.getInstance().options.allKeys) {
if (otherBinding == binding || !binding.equals(otherBinding)) continue;

if (KeyBindingContext.of(binding) != KeyBindingContext.of(otherBinding)) {
return "fabric.keybinding.conflicts";
}
}

return constant;
}
}
Loading