1
0
forked from Clones/Controlify

🧹 Refactor gui package

This commit is contained in:
isXander
2023-05-30 14:23:47 +01:00
parent 5639436197
commit 361ceefd6f
14 changed files with 21 additions and 26 deletions

View File

@ -12,7 +12,6 @@ import net.minecraft.network.chat.CommonComponents;
import net.minecraft.network.chat.Component;
public class BetaNoticeScreen extends Screen {
private MultiLineTextWidget textWidget;
public BetaNoticeScreen() {
super(Component.translatable("controlify.beta.title"));
@ -20,7 +19,7 @@ public class BetaNoticeScreen extends Screen {
@Override
protected void init() {
textWidget = new AccessibilityOnboardingTextWidget(
MultiLineTextWidget textWidget = new AccessibilityOnboardingTextWidget(
font,
Component.translatable("controlify.beta.message",
Component.translatable("controlify.beta.message.link")

View File

@ -0,0 +1,349 @@
package dev.isxander.controlify.gui.screen;
import com.google.common.collect.ImmutableList;
import dev.isxander.controlify.Controlify;
import dev.isxander.controlify.ControllerManager;
import dev.isxander.controlify.controller.Controller;
import dev.isxander.controlify.controller.sdl2.SDL2NativesManager;
import dev.isxander.controlify.utils.Animator;
import net.minecraft.ChatFormatting;
import net.minecraft.client.gui.ComponentPath;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.components.Renderable;
import net.minecraft.client.gui.components.events.AbstractContainerEventHandler;
import net.minecraft.client.gui.components.events.GuiEventListener;
import net.minecraft.client.gui.layouts.FrameLayout;
import net.minecraft.client.gui.layouts.GridLayout;
import net.minecraft.client.gui.narration.NarratableEntry;
import net.minecraft.client.gui.narration.NarratedElementType;
import net.minecraft.client.gui.narration.NarrationElementOutput;
import net.minecraft.client.gui.navigation.FocusNavigationEvent;
import net.minecraft.client.gui.navigation.ScreenRectangle;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.client.gui.screens.worldselection.CreateWorldScreen;
import net.minecraft.network.chat.CommonComponents;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.FastColor;
import net.minecraft.util.Mth;
import org.jetbrains.annotations.Nullable;
import java.util.List;
public class ControllerCarouselScreen extends Screen {
public static final ResourceLocation CHECKMARK = new ResourceLocation("textures/gui/checkmark.png");
private final Screen parent;
private List<CarouselEntry> carouselEntries = null;
private int carouselIndex;
private Animator.AnimationInstance carouselAnimation = null;
private ControllerCarouselScreen(Screen parent) {
super(Component.translatable("controlify.gui.carousel.title"));
this.parent = parent;
this.carouselIndex = Controlify.instance().getCurrentController().map(c -> ControllerManager.getConnectedControllers().indexOf(c)).orElse(0);
}
public static Screen createConfigScreen(Screen parent) {
var controlify = Controlify.instance();
if (!controlify.config().globalSettings().vibrationOnboarded) {
return new SDLOnboardingScreen(() -> new ControllerCarouselScreen(parent), yes -> {
if (yes) {
SDL2NativesManager.initialise();
if (controlify.config().globalSettings().delegateSetup) {
controlify.discoverControllers();
controlify.config().globalSettings().delegateSetup = false;
controlify.config().save();
}
}
});
} else if (Controlify.instance().config().globalSettings().delegateSetup) {
controlify.discoverControllers();
controlify.config().globalSettings().delegateSetup = false;
controlify.config().save();
}
return new ControllerCarouselScreen(parent);
}
@Override
protected void init() {
refreshControllers();
GridLayout grid = new GridLayout().columnSpacing(10);
GridLayout.RowHelper rowHelper = grid.createRowHelper(2);
rowHelper.addChild(Button.builder(Component.translatable("controlify.gui.global_settings.title"), btn -> minecraft.setScreen(GlobalSettingsScreenFactory.createGlobalSettingsScreen(this))).build());
rowHelper.addChild(Button.builder(CommonComponents.GUI_DONE, btn -> this.onClose()).build());
grid.visitWidgets(widget -> {
widget.setTabOrderGroup(1);
this.addRenderableWidget(widget);
});
grid.arrangeElements();
FrameLayout.centerInRectangle(grid, 0, this.height - 36, this.width, 36);
}
public void refreshControllers() {
Controller<?, ?> prevSelectedController;
if (carouselEntries != null) {
carouselEntries.forEach(this::removeWidget);
prevSelectedController = carouselEntries.get(carouselIndex).controller;
} else {
prevSelectedController = null;
}
carouselEntries = ControllerManager.getConnectedControllers().stream()
.map(c -> new CarouselEntry(c, this.width / 3, this.height - 66))
.peek(this::addRenderableWidget)
.toList();
carouselIndex = carouselEntries.stream()
.filter(e -> e.controller == prevSelectedController)
.findFirst()
.map(carouselEntries::indexOf)
.orElse(Controlify.instance().getCurrentController()
.map(c -> ControllerManager.getConnectedControllers().indexOf(c))
.orElse(0)
);
if (!carouselEntries.isEmpty())
carouselEntries.get(carouselIndex).overlayColor = 0;
float offsetX = (this.width / 2f) * -(carouselIndex - 1) - this.width / 6f;
for (int i = 0; i < carouselEntries.size(); i++) {
CarouselEntry entry = carouselEntries.get(i);
entry.setX(offsetX + (this.width / 2f) * i);
entry.setY(i == carouselIndex ? 20 : 10);
}
}
@Override
public void render(GuiGraphics graphics, int mouseX, int mouseY, float delta) {
renderDirtBackground(graphics);
int footerY = Mth.roundToward(this.height - 36 - 2, 2);
graphics.blit(CreateWorldScreen.FOOTER_SEPERATOR, 0, footerY, 0.0F, 0.0F, this.width, 2, 32, 2);
graphics.setColor(0.5f, 0.5f, 0.5f, 1f);
graphics.blit(CreateWorldScreen.LIGHT_DIRT_BACKGROUND, 0, 0, 0, 0f, 0f, this.width, footerY, 32, 32);
graphics.setColor(1f, 1f, 1f, 1f);
if (carouselEntries.isEmpty()) {
graphics.drawCenteredString(font, Component.translatable("controlify.gui.carousel.no_controllers"), this.width / 2, (this.height - 36) / 2 - 10, 0xFFAAAAAA);
}
super.render(graphics, mouseX, mouseY, delta);
}
@Override
public void renderDirtBackground(GuiGraphics graphics) {
int scale = 32;
graphics.blit(CreateWorldScreen.LIGHT_DIRT_BACKGROUND, 0, 0, 0, 0.0F, 0.0F, this.width, this.height, scale, scale);
}
public void focusOnEntry(int index) {
if (carouselAnimation != null && !carouselAnimation.isDone())
return;
int diff = index - carouselIndex;
if (diff == 0) return;
carouselIndex = index;
carouselAnimation = new Animator.AnimationInstance(10, x -> x < 0.5f ? 4 * x * x * x : 1 - (float)Math.pow(-2 * x + 2, 3) / 2);
for (CarouselEntry entry : carouselEntries) {
boolean selected = carouselEntries.indexOf(entry) == index;
carouselAnimation.addConsumer(entry::setX, entry.getX(), entry.getX() + -diff * (this.width / 2f));
carouselAnimation.addConsumer(entry::setY, entry.getY(), selected ? 20f : 10f);
carouselAnimation.addConsumer(t -> entry.overlayColor = FastColor.ARGB32.lerp(t, entry.overlayColor, selected ? 0 : 0x90000000), 0f, 1f);
}
Animator.INSTANCE.play(carouselAnimation);
}
@Override
public void onClose() {
minecraft.setScreen(parent);
}
private class CarouselEntry extends AbstractContainerEventHandler implements Renderable, NarratableEntry {
private int x, y;
private final int width, height;
private float translationX, translationY;
private final Controller<?, ?> controller;
private final boolean hasNickname;
private final Button useControllerButton;
private final Button settingsButton;
private final ImmutableList<? extends GuiEventListener> children;
private boolean prevUse;
private float currentlyUsedPos;
private Animator.AnimationInstance currentlyUsedAnimation;
private int overlayColor = 0x90000000;
private boolean hovered = false;
private CarouselEntry(Controller<?, ?> controller, int width, int height) {
this.width = width;
this.height = height;
this.controller = controller;
this.hasNickname = this.controller.config().customName != null;
this.settingsButton = Button.builder(Component.translatable("controlify.gui.carousel.entry.settings"), btn -> minecraft.setScreen(ControllerConfigScreenFactory.generateConfigScreen(ControllerCarouselScreen.this, controller))).width((getWidth() - 2) / 2 - 2).build();
this.useControllerButton = Button.builder(Component.translatable("controlify.gui.carousel.entry.use"), btn -> Controlify.instance().setCurrentController(controller)).width(settingsButton.getWidth()).build();
this.children = ImmutableList.of(settingsButton, useControllerButton);
this.prevUse = isCurrentlyUsed();
this.currentlyUsedPos = prevUse ? 0 : -1;
}
@Override
public void render(GuiGraphics graphics, int mouseX, int mouseY, float delta) {
hovered = isMouseOver(mouseX, mouseY);
graphics.enableScissor(x, y, x + width + (translationX > 0 ? 1 : 0), y + height + (translationY > 0 ? 1 : 0));
graphics.pose().pushPose();
graphics.pose().translate(translationX, translationY, 0);
graphics.blit(CreateWorldScreen.LIGHT_DIRT_BACKGROUND, x, y, 0, 0f, 0f, width, height, 32, 32);
graphics.renderOutline(x, y, width, height, 0x5AFFFFFF);
useControllerButton.render(graphics, mouseX, mouseY, delta);
settingsButton.render(graphics, mouseX, mouseY, delta);
graphics.drawCenteredString(font, controller.name(), x + width / 2, y + height - 26 - font.lineHeight - (hasNickname ? font.lineHeight + 1 : 0), 0xFFFFFF);
if (hasNickname) {
String nickname = controller.config().customName;
controller.config().customName = null;
graphics.drawCenteredString(font, controller.name(), x + width / 2, y + height - 26 - font.lineHeight, 0xAAAAAA);
controller.config().customName = nickname;
}
Component currentlyInUseText = Component.translatable("controlify.gui.carousel.entry.in_use").withStyle(ChatFormatting.GREEN);
graphics.pose().pushPose();
graphics.pose().translate((4 + 9 + 4 + font.width(currentlyInUseText)) * currentlyUsedPos, 0, 0);
if (currentlyUsedPos > -1) {
graphics.blit(CHECKMARK, x + 4, y + 4, 0f, 0f, 9, 8, 9, 8);
graphics.drawString(font, currentlyInUseText, x + 17, y + 4, -1);
}
graphics.pose().popPose();
int iconWidth = width - 6;
// buttons 4px padding top currently in use controller name image padding
int iconHeight = height - 22 - 4 - font.lineHeight - 8 - (font.lineHeight * (hasNickname ? 2 : 1) + 1) - 6;
int iconSize = Mth.roundToward(Math.min(iconHeight, iconWidth), 2);
graphics.pose().pushPose();
graphics.pose().translate(x + width / 2 - iconSize / 2, y + font.lineHeight + 12 + iconHeight / 2 - iconSize / 2, 0);
graphics.pose().scale(iconSize / 64f, iconSize / 64f, 1);
graphics.blit(controller.icon(), 0, 0, 0f, 0f, 64, 64, 64, 64);
graphics.pose().popPose();
graphics.pose().translate(0, 0, 1);
graphics.fill(x, y, x + width, y + height, overlayColor);
graphics.pose().popPose();
graphics.disableScissor();
if (prevUse != isCurrentlyUsed()) {
if (currentlyUsedAnimation != null)
currentlyUsedAnimation.finish();
currentlyUsedAnimation = Animator.INSTANCE.play(new Animator.AnimationInstance(20, t -> 1 - (float)Math.pow(1 - t, 5))
.addConsumer(t -> currentlyUsedPos = t, currentlyUsedPos, isCurrentlyUsed() ? 0 : -1));
}
prevUse = isCurrentlyUsed();
}
@Override
public boolean mouseClicked(double mouseX, double mouseY, int button) {
if (mouseX >= x && mouseX <= x + width && mouseY >= y && mouseY <= y + height) {
int index = carouselEntries.indexOf(this);
if (index != carouselIndex) {
if (carouselAnimation == null || carouselAnimation.isDone())
focusOnEntry(index);
return true;
}
}
return super.mouseClicked(mouseX, mouseY, button);
}
@Override
public void setFocused(boolean focused) {
super.setFocused(focused);
if (focused) {
focusOnEntry(carouselEntries.indexOf(this));
}
}
@Override
public List<? extends GuiEventListener> children() {
return children;
}
public void setX(float x) {
this.x = (int)x;
this.settingsButton.setX((int)x + 2);
this.useControllerButton.setX(this.settingsButton.getX() + this.settingsButton.getWidth() + 2);
this.translationX = x - (int)x;
}
public void setY(float y) {
this.y = (int)y;
this.useControllerButton.setY((int)y + getHeight() - 20 - 2);
this.settingsButton.setY(this.useControllerButton.getY());
this.translationY = y - (int)y;
}
public int getX() {
return this.x;
}
public int getY() {
return this.y;
}
public int getWidth() {
return this.width;
}
public int getHeight() {
return this.height;
}
@Nullable
@Override
public ComponentPath nextFocusPath(FocusNavigationEvent event) {
if (carouselAnimation != null && !carouselAnimation.isDone())
return null;
return super.nextFocusPath(event);
}
@Override
public ScreenRectangle getRectangle() {
return new ScreenRectangle(x, y, width, height);
}
public boolean isCurrentlyUsed() {
return Controlify.instance().getCurrentController().orElse(null) == controller;
}
@Override
public NarrationPriority narrationPriority() {
return isFocused() ? NarrationPriority.FOCUSED : hovered ? NarrationPriority.HOVERED : NarrationPriority.NONE;
}
@Override
public void updateNarration(NarrationElementOutput builder) {
builder.add(NarratedElementType.TITLE, controller.name());
builder.add(NarratedElementType.USAGE, Component.literal("Left arrow to go to previous controller, right arrow to go to next controller."));
}
}
}

View File

@ -0,0 +1,465 @@
package dev.isxander.controlify.gui.screen;
import dev.isxander.controlify.Controlify;
import dev.isxander.controlify.api.bind.ControllerBinding;
import dev.isxander.controlify.bindings.BindContext;
import dev.isxander.controlify.bindings.EmptyBind;
import dev.isxander.controlify.controller.Controller;
import dev.isxander.controlify.controller.ControllerConfig;
import dev.isxander.controlify.controller.gamepad.GamepadController;
import dev.isxander.controlify.controller.joystick.SingleJoystickController;
import dev.isxander.controlify.controller.joystick.mapping.JoystickMapping;
import dev.isxander.controlify.gui.controllers.AbstractBindController;
import dev.isxander.controlify.rumble.BasicRumbleEffect;
import dev.isxander.controlify.rumble.RumbleSource;
import dev.isxander.controlify.rumble.RumbleState;
import dev.isxander.yacl.api.*;
import dev.isxander.yacl.api.controller.BooleanControllerBuilder;
import dev.isxander.yacl.api.controller.FloatSliderControllerBuilder;
import dev.isxander.yacl.api.controller.StringControllerBuilder;
import dev.isxander.yacl.api.controller.TickBoxControllerBuilder;
import net.minecraft.ChatFormatting;
import net.minecraft.Util;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.network.chat.CommonComponents;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class ControllerConfigScreenFactory {
private static final Function<Float, Component> percentFormatter = v -> Component.literal(String.format("%.0f%%", v*100));
private static final Function<Float, Component> percentOrOffFormatter = v -> v == 0 ? CommonComponents.OPTION_OFF : percentFormatter.apply(v);
public static Screen generateConfigScreen(Screen parent, Controller<?, ?> controller) {
ControllerConfig def = controller.defaultConfig();
ControllerConfig config = controller.config();
return YetAnotherConfigLib.createBuilder()
.title(Component.literal("Controlify"))
.category(createBasicCategory(controller, def, config))
.category(createAdvancedCategory(controller))
.category(createBindsCategory(controller))
.save(() -> Controlify.instance().config().save())
.build().generateScreen(parent);
}
private static ConfigCategory createBasicCategory(Controller<?, ?> controller, ControllerConfig def, ControllerConfig config) {
return ConfigCategory.createBuilder()
.name(Component.translatable("controlify.gui.config.category.basic"))
.option(Option.<String>createBuilder()
.name(Component.translatable("controlify.gui.custom_name"))
.description(OptionDescription.of(Component.translatable("controlify.gui.custom_name.tooltip")))
.binding(def.customName == null ? "" : def.customName, () -> config.customName == null ? "" : config.customName, v -> config.customName = (v.equals("") ? null : v))
.controller(StringControllerBuilder::create)
.build())
.group(makeSensitivityGroup(controller, def, config))
.group(makeControlsGroup(controller, def, config))
.group(makeAccessibilityGroup(controller, controller.defaultConfig(), controller.config()))
.group(makeDeadzoneGroup(controller, controller.defaultConfig(), controller.config()))
.build();
}
private static OptionGroup makeSensitivityGroup(Controller<?, ?> controller, ControllerConfig def, ControllerConfig config) {
return OptionGroup.createBuilder()
.name(Component.translatable("controlify.gui.config.group.sensitivity"))
.option(Option.<Float>createBuilder()
.name(Component.translatable("controlify.gui.horizontal_look_sensitivity"))
.description(OptionDescription.createBuilder()
.text(Component.translatable("controlify.gui.horizontal_look_sensitivity.tooltip"))
.build())
.binding(def.horizontalLookSensitivity, () -> config.horizontalLookSensitivity, v -> config.horizontalLookSensitivity = v)
.controller(opt -> FloatSliderControllerBuilder.create(opt)
.range(0.1f, 2f).step(0.05f).valueFormatter(percentFormatter))
.build())
.option(Option.<Float>createBuilder()
.name(Component.translatable("controlify.gui.vertical_look_sensitivity"))
.description(OptionDescription.createBuilder()
.text(Component.translatable("controlify.gui.vertical_look_sensitivity.tooltip"))
.build())
.binding(def.verticalLookSensitivity, () -> config.verticalLookSensitivity, v -> config.verticalLookSensitivity = v)
.controller(opt -> FloatSliderControllerBuilder.create(opt)
.range(0.1f, 2f).step(0.05f).valueFormatter(percentFormatter))
.build())
.option(Option.<Float>createBuilder()
.name(Component.translatable("controlify.gui.vmouse_sensitivity"))
.description(OptionDescription.createBuilder()
.text(Component.translatable("controlify.gui.vmouse_sensitivity.tooltip"))
.build())
.binding(def.virtualMouseSensitivity, () -> config.virtualMouseSensitivity, v -> config.virtualMouseSensitivity = v)
.controller(opt -> FloatSliderControllerBuilder.create(opt)
.range(0.1f, 2f).step(0.05f).valueFormatter(percentFormatter))
.build())
.option(Option.<Boolean>createBuilder()
.name(Component.translatable("controlify.gui.reduce_aiming_sensitivity"))
.description(OptionDescription.createBuilder()
.text(Component.translatable("controlify.gui.reduce_aiming_sensitivity.tooltip"))
.webpImage(screenshot("reduce-aim-sensitivity.webp"))
.build())
.binding(def.reduceAimingSensitivity, () -> config.reduceAimingSensitivity, v -> config.reduceAimingSensitivity = v)
.controller(TickBoxControllerBuilder::create)
.build())
.build();
}
private static OptionGroup makeControlsGroup(Controller<?, ?> controller, ControllerConfig def, ControllerConfig config) {
Function<Boolean, Component> holdToggleFormatter = v -> Component.translatable("controlify.gui.format.hold_toggle." + (v ? "toggle" : "hold"));
return OptionGroup.createBuilder()
.name(Component.translatable("controlify.gui.config.group.controls"))
.option(Option.<Boolean>createBuilder()
.name(Component.translatable("controlify.gui.toggle_sprint"))
.description(OptionDescription.createBuilder()
.text(Component.translatable("controlify.gui.toggle_sprint.tooltip"))
.build())
.binding(def.toggleSprint, () -> config.toggleSprint, v -> config.toggleSprint = v)
.controller(opt -> BooleanControllerBuilder.create(opt)
.valueFormatter(holdToggleFormatter)
.coloured(false))
.build())
.option(Option.<Boolean>createBuilder()
.name(Component.translatable("controlify.gui.toggle_sneak"))
.description(OptionDescription.createBuilder()
.text(Component.translatable("controlify.gui.toggle_sneak.tooltip"))
.build())
.binding(def.toggleSneak, () -> config.toggleSneak, v -> config.toggleSneak = v)
.controller(opt -> BooleanControllerBuilder.create(opt)
.valueFormatter(holdToggleFormatter)
.coloured(false))
.build())
.option(Option.<Boolean>createBuilder()
.name(Component.translatable("controlify.gui.auto_jump"))
.description(OptionDescription.createBuilder()
.text(Component.translatable("controlify.gui.auto_jump.tooltip"))
.build())
.binding(def.autoJump, () -> config.autoJump, v -> config.autoJump = v)
.controller(opt -> BooleanControllerBuilder.create(opt)
.onOffFormatter())
.build())
.build();
}
private static OptionGroup makeAccessibilityGroup(Controller<?, ?> controller, ControllerConfig def, ControllerConfig config) {
return OptionGroup.createBuilder()
.name(Component.translatable("controlify.config.group.accessibility"))
.option(Option.<Boolean>createBuilder()
.name(Component.translatable("controlify.gui.show_ingame_guide"))
.description(OptionDescription.createBuilder()
.text(Component.translatable("controlify.gui.show_ingame_guide.tooltip"))
.image(screenshot("ingame-button-guide.png"), 961, 306)
.build())
.binding(def.showIngameGuide, () -> config.showIngameGuide, v -> config.showIngameGuide = v)
.controller(TickBoxControllerBuilder::create)
.build())
.option(Option.<Boolean>createBuilder()
.name(Component.translatable("controlify.gui.show_screen_guide"))
.description(OptionDescription.createBuilder()
.text(Component.translatable("controlify.gui.show_screen_guide.tooltip"))
.webpImage(screenshot("screen-button-guide.webp"))
.build())
.binding(def.showScreenGuide, () -> config.showScreenGuide, v -> config.showScreenGuide = v)
.controller(TickBoxControllerBuilder::create)
.build())
.option(Option.<Float>createBuilder()
.name(Component.translatable("controlify.gui.chat_screen_offset"))
.description(OptionDescription.createBuilder()
.text(Component.translatable("controlify.gui.chat_screen_offset.tooltip"))
.build())
.binding(def.chatKeyboardHeight, () -> config.chatKeyboardHeight, v -> config.chatKeyboardHeight = v)
.controller(opt -> FloatSliderControllerBuilder.create(opt)
.range(0f, 8f).step(0.1f).valueFormatter(percentFormatter))
.build())
.build();
}
private static OptionGroup makeDeadzoneGroup(Controller<?, ?> controller, ControllerConfig def, ControllerConfig config) {
var group = OptionGroup.createBuilder()
.name(Component.translatable("controlify.config.group.deadzones"));
if (controller instanceof GamepadController gamepad) {
var gpCfg = gamepad.config();
var gpCfgDef = gamepad.defaultConfig();
group
.option(Option.<Float>createBuilder()
.name(Component.translatable("controlify.gui.axis_deadzone", Component.translatable("controlify.gui.left_stick")))
.description(OptionDescription.createBuilder()
.text(Component.translatable("controlify.gui.axis_deadzone.tooltip", Component.translatable("controlify.gui.left_stick")))
.text(Component.translatable("controlify.gui.stickdrift_warning").withStyle(ChatFormatting.RED))
.build())
.binding(
Math.max(gpCfgDef.leftStickDeadzoneX, gpCfgDef.leftStickDeadzoneY),
() -> Math.max(gpCfg.leftStickDeadzoneX, gpCfgDef.leftStickDeadzoneY),
v -> gpCfg.leftStickDeadzoneX = gpCfg.leftStickDeadzoneY = v
)
.controller(opt -> FloatSliderControllerBuilder.create(opt)
.range(0f, 1f).step(0.01f)
.valueFormatter(percentFormatter))
.build())
.option(Option.<Float>createBuilder()
.name(Component.translatable("controlify.gui.axis_deadzone", Component.translatable("controlify.gui.right_stick")))
.description(OptionDescription.createBuilder()
.text(Component.translatable("controlify.gui.axis_deadzone.tooltip", Component.translatable("controlify.gui.right_stick")))
.text(Component.translatable("controlify.gui.stickdrift_warning").withStyle(ChatFormatting.RED))
.build())
.binding(
Math.max(gpCfgDef.rightStickDeadzoneX, gpCfgDef.rightStickDeadzoneY),
() -> Math.max(gpCfg.rightStickDeadzoneX, gpCfgDef.rightStickDeadzoneY),
v -> gpCfg.rightStickDeadzoneX = gpCfg.rightStickDeadzoneY = v
)
.controller(opt -> FloatSliderControllerBuilder.create(opt)
.range(0f, 1f).step(0.01f)
.valueFormatter(percentFormatter))
.build());
} else if (controller instanceof SingleJoystickController joystick) {
JoystickMapping.Axis[] axes = joystick.mapping().axes();
Collection<Integer> deadzoneAxes = IntStream.range(0, axes.length)
.filter(i -> axes[i].requiresDeadzone())
.boxed()
.collect(Collectors.toMap(
i -> axes[i].identifier(),
i -> i,
(x, y) -> x,
LinkedHashMap::new
))
.values();
var jsCfg = joystick.config();
var jsCfgDef = joystick.defaultConfig();
for (int i : deadzoneAxes) {
var axis = axes[i];
group.option(Option.<Float>createBuilder()
.name(Component.translatable("controlify.gui.joystick_axis_deadzone", axis.name()))
.description(OptionDescription.createBuilder()
.text(Component.translatable("controlify.gui.joystick_axis_deadzone.tooltip", axis.name()))
.text(Component.translatable("controlify.gui.stickdrift_warning").withStyle(ChatFormatting.RED))
.build())
.binding(jsCfgDef.getDeadzone(i), () -> jsCfg.getDeadzone(i), v -> jsCfg.setDeadzone(i, v))
.controller(opt -> FloatSliderControllerBuilder.create(opt)
.range(0f, 1f).step(0.01f)
.valueFormatter(percentFormatter))
.build());
}
}
group.option(Option.<Float>createBuilder()
.name(Component.translatable("controlify.gui.button_activation_threshold"))
.description(OptionDescription.createBuilder()
.text(Component.translatable("controlify.gui.button_activation_threshold.tooltip"))
.build())
.binding(def.buttonActivationThreshold, () -> config.buttonActivationThreshold, v -> config.buttonActivationThreshold = v)
.controller(opt -> FloatSliderControllerBuilder.create(opt)
.range(0f, 1f).step(0.01f)
.valueFormatter(percentFormatter))
.build());
group.option(ButtonOption.createBuilder()
.name(Component.translatable("controlify.gui.auto_calibration"))
.description(OptionDescription.createBuilder()
.text(Component.translatable("controlify.gui.auto_calibration.tooltip"))
.build())
.action((screen, button) -> Minecraft.getInstance().setScreen(new ControllerDeadzoneCalibrationScreen(controller, screen)))
.build());
return group.build();
}
private static ConfigCategory createAdvancedCategory(Controller<?, ?> controller) {
return ConfigCategory.createBuilder()
.name(Component.translatable("controlify.config.category.advanced"))
.group(makeVibrationGroup(controller))
.group(makeGyroGroup(controller))
.build();
}
private static ConfigCategory createBindsCategory(Controller<?, ?> controller) {
var category = ConfigCategory.createBuilder()
.name(Component.translatable("controlify.gui.group.controls"));
List<OptionBindPair> optionBinds = new ArrayList<>();
groupBindings(controller.bindings().registry().values()).forEach((categoryName, bindGroup) -> {
var controlsGroup = OptionGroup.createBuilder()
.name(categoryName);
controlsGroup.options(bindGroup.stream().map(binding -> {
Option.Builder<?> option = binding.startYACLOption()
.listener((opt, val) -> updateConflictingBinds(optionBinds));
Option<?> built = option.build();
optionBinds.add(new OptionBindPair(built, binding));
return built;
}).toList());
category.group(controlsGroup.build());
});
updateConflictingBinds(optionBinds);
return category.build();
}
private static void updateConflictingBinds(List<OptionBindPair> all) {
all.forEach(pair -> ((AbstractBindController<?>) pair.option().controller()).setConflicting(false));
for (OptionBindPair opt : all) {
var ctxs = BindContext.flatten(opt.binding().contexts());
List<OptionBindPair> conflicting = all.stream()
.filter(pair -> pair.binding() != opt.binding())
.filter(pair -> {
boolean contextsMatch = BindContext.flatten(pair.binding().contexts())
.stream()
.anyMatch(ctxs::contains);
boolean bindMatches = pair.option().pendingValue().equals(opt.option().pendingValue());
boolean bindIsNotEmpty = !(pair.option().pendingValue() instanceof EmptyBind<?>);
return contextsMatch && bindMatches && bindIsNotEmpty;
}).toList();
conflicting.forEach(conflict -> ((AbstractBindController<?>) conflict.option().controller()).setConflicting(true));
}
}
private static OptionGroup makeVibrationGroup(Controller<?, ?> controller) {
boolean canRumble = controller.supportsRumble();
var config = controller.config();
var def = controller.defaultConfig();
var vibrationGroup = OptionGroup.createBuilder()
.name(Component.translatable("controlify.gui.group.vibration"))
.description(OptionDescription.createBuilder()
.text(Component.translatable("controlify.gui.group.vibration.tooltip"))
.build());
if (canRumble) {
List<Option<Float>> strengthOptions = new ArrayList<>();
Option<Boolean> allowVibrationOption;
vibrationGroup.option(allowVibrationOption = Option.<Boolean>createBuilder()
.name(Component.translatable("controlify.gui.allow_vibrations"))
.description(OptionDescription.createBuilder()
.text(Component.translatable("controlify.gui.allow_vibrations.tooltip"))
.build())
.binding(def.allowVibrations, () -> config.allowVibrations, v -> config.allowVibrations = v)
.listener((opt, allowVibration) -> strengthOptions.forEach(so -> so.setAvailable(allowVibration)))
.controller(TickBoxControllerBuilder::create)
.build());
for (RumbleSource source : RumbleSource.values()) {
var option = Option.<Float>createBuilder()
.name(Component.translatable("controlify.vibration_strength." + source.id().getNamespace() + "." + source.id().getPath()))
.description(OptionDescription.createBuilder()
.text(Component.translatable("controlify.vibration_strength." + source.id().getNamespace() + "." + source.id().getPath() + ".tooltip"))
.build())
.binding(
def.getRumbleStrength(source),
() -> config.getRumbleStrength(source),
v -> config.setRumbleStrength(source, v)
)
.controller(opt -> FloatSliderControllerBuilder.create(opt)
.range(0f, 2f)
.step(0.05f)
.valueFormatter(percentOrOffFormatter))
.available(allowVibrationOption.pendingValue())
.build();
strengthOptions.add(option);
vibrationGroup.option(option);
}
vibrationGroup.option(ButtonOption.createBuilder()
.name(Component.translatable("controlify.gui.test_vibration"))
.description(OptionDescription.of(Component.translatable("controlify.gui.test_vibration.tooltip")))
.action((screen, btn) -> {
controller.rumbleManager().play(
RumbleSource.MASTER,
BasicRumbleEffect.byTime(t -> new RumbleState(0f, t), 20)
.join(BasicRumbleEffect.byTime(t -> new RumbleState(0f, 1 - t), 20))
.repeat(3)
.join(BasicRumbleEffect.constant(1f, 0f, 5)
.join(BasicRumbleEffect.constant(0f, 1f, 5))
.repeat(10)
)
.earlyFinish(BasicRumbleEffect.finishOnScreenChange())
);
})
.build());
} else {
vibrationGroup.option(LabelOption.create(Component.translatable("controlify.gui.allow_vibrations.not_available").withStyle(ChatFormatting.RED)));
}
return vibrationGroup.build();
}
private static OptionGroup makeGyroGroup(Controller<?, ?> controller) {
GamepadController gamepad = (controller instanceof GamepadController) ? (GamepadController) controller : null;
boolean hasGyro = gamepad != null && gamepad.hasGyro();
var gpCfg = gamepad != null ? gamepad.config() : null;
var gpCfgDef = gamepad != null ? gamepad.defaultConfig() : null;
Option<Float> gyroSensitivity;
List<Option<?>> gyroOptions = new ArrayList<>();
var gyroGroup = OptionGroup.createBuilder()
.name(Component.translatable("controlify.gui.group.gyro"))
.description(OptionDescription.createBuilder()
.text(Component.translatable("controlify.gui.group.gyro.tooltip"))
.build())
.collapsed(!hasGyro);
if (hasGyro) {
gyroGroup.option(gyroSensitivity = Option.<Float>createBuilder()
.name(Component.translatable("controlify.gui.gyro_look_sensitivity"))
.description(OptionDescription.createBuilder()
.text(Component.translatable("controlify.gui.gyro_look_sensitivity.tooltip"))
.build())
.binding(gpCfgDef.gyroLookSensitivity, () -> gpCfg.gyroLookSensitivity, v -> gpCfg.gyroLookSensitivity = v)
.controller(opt -> FloatSliderControllerBuilder.create(opt)
.range(0f, 1f)
.step(0.05f)
.valueFormatter(percentOrOffFormatter))
.listener((opt, sensitivity) -> gyroOptions.forEach(o -> {
o.setAvailable(sensitivity > 0);
o.requestSetDefault();
}))
.build());
gyroGroup.option(Util.make(() -> {
var opt = Option.<Boolean>createBuilder()
.name(Component.translatable("controlify.gui.gyro_requires_button"))
.description(OptionDescription.createBuilder()
.text(Component.translatable("controlify.gui.gyro_requires_button.tooltip"))
.build())
.binding(gpCfgDef.gyroRequiresButton, () -> gpCfg.gyroRequiresButton, v -> gpCfg.gyroRequiresButton = v)
.controller(TickBoxControllerBuilder::create)
.available(gyroSensitivity.pendingValue() > 0)
.build();
gyroOptions.add(opt);
return opt;
}));
gyroGroup.option(Util.make(() -> {
var opt = Option.<Boolean>createBuilder()
.name(Component.translatable("controlify.gui.flick_stick"))
.description(OptionDescription.createBuilder()
.text(Component.translatable("controlify.gui.flick_stick.tooltip"))
.build())
.binding(gpCfgDef.flickStick, () -> gpCfg.flickStick, v -> gpCfg.flickStick = v)
.controller(TickBoxControllerBuilder::create)
.available(gyroSensitivity.pendingValue() > 0)
.build();
gyroOptions.add(opt);
return opt;
}));
} else {
gyroGroup.option(LabelOption.create(Component.translatable("controlify.gui.group.gyro.no_gyro.tooltip").withStyle(ChatFormatting.RED)));
}
return gyroGroup.build();
}
private static Map<Component, List<ControllerBinding>> groupBindings(Collection<ControllerBinding> bindings) {
return bindings.stream()
.collect(Collectors.groupingBy(ControllerBinding::category, LinkedHashMap::new, Collectors.toList()));
}
private static ResourceLocation screenshot(String filename) {
return Controlify.id("textures/screenshots/" + filename);
}
private record OptionBindPair(Option<?> option, ControllerBinding binding) {
}
}

View File

@ -1,7 +1,5 @@
package dev.isxander.controlify.gui.screen;
import com.mojang.blaze3d.systems.RenderSystem;
import com.mojang.blaze3d.vertex.PoseStack;
import dev.isxander.controlify.Controlify;
import dev.isxander.controlify.controller.Controller;
import net.minecraft.ChatFormatting;

View File

@ -0,0 +1,87 @@
package dev.isxander.controlify.gui.screen;
import dev.isxander.controlify.Controlify;
import dev.isxander.controlify.config.GlobalSettings;
import dev.isxander.controlify.reacharound.ReachAroundMode;
import dev.isxander.yacl.api.*;
import dev.isxander.yacl.api.controller.BooleanControllerBuilder;
import dev.isxander.yacl.api.controller.EnumControllerBuilder;
import dev.isxander.yacl.api.controller.TickBoxControllerBuilder;
import net.minecraft.ChatFormatting;
import net.minecraft.Util;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
public class GlobalSettingsScreenFactory {
public static Screen createGlobalSettingsScreen(Screen parent) {
var globalSettings = Controlify.instance().config().globalSettings();
return YetAnotherConfigLib.createBuilder()
.title(Component.translatable("controlify.gui.global_settings.title"))
.category(ConfigCategory.createBuilder()
.name(Component.translatable("controlify.gui.global_settings.title"))
.option(Option.<Boolean>createBuilder()
.name(Component.translatable("controlify.gui.load_vibration_natives"))
.description(OptionDescription.createBuilder()
.text(Component.translatable("controlify.gui.load_vibration_natives.tooltip"))
.text(Component.translatable("controlify.gui.load_vibration_natives.tooltip.warning").withStyle(ChatFormatting.RED))
.build())
.binding(true, () -> globalSettings.loadVibrationNatives, v -> globalSettings.loadVibrationNatives = v)
.controller(opt -> BooleanControllerBuilder.create(opt).yesNoFormatter())
.flag(OptionFlag.GAME_RESTART)
.build())
.option(Option.<ReachAroundMode>createBuilder()
.name(Component.translatable("controlify.gui.reach_around"))
.description(state -> OptionDescription.createBuilder()
.webpImage(screenshot("reach-around-placement.webp"))
.text(Component.translatable("controlify.gui.reach_around.tooltip"))
.text(Component.translatable("controlify.gui.reach_around.tooltip.parity").withStyle(ChatFormatting.GRAY))
.text(state == ReachAroundMode.EVERYWHERE ? Component.translatable("controlify.gui.reach_around.tooltip.warning").withStyle(ChatFormatting.RED) : Component.empty())
.build())
.binding(GlobalSettings.DEFAULT.reachAround, () -> globalSettings.reachAround, v -> globalSettings.reachAround = v)
.controller(opt -> EnumControllerBuilder.create(opt).enumClass(ReachAroundMode.class))
.build())
.option(Option.<Boolean>createBuilder()
.name(Component.translatable("controlify.gui.ui_sounds"))
.description(OptionDescription.createBuilder()
.text(Component.translatable("controlify.gui.ui_sounds.tooltip"))
.build())
.binding(GlobalSettings.DEFAULT.uiSounds, () -> globalSettings.uiSounds, v -> globalSettings.uiSounds = v)
.controller(TickBoxControllerBuilder::create)
.build())
.option(Option.<Boolean>createBuilder()
.name(Component.translatable("controlify.gui.notify_low_battery"))
.description(OptionDescription.createBuilder()
.text(Component.translatable("controlify.gui.notify_low_battery.tooltip"))
.build())
.binding(GlobalSettings.DEFAULT.notifyLowBattery, () -> globalSettings.notifyLowBattery, v -> globalSettings.notifyLowBattery = v)
.controller(TickBoxControllerBuilder::create)
.build())
.option(Option.<Boolean>createBuilder()
.name(Component.translatable("controlify.gui.out_of_focus_input"))
.description(OptionDescription.createBuilder()
.text(Component.translatable("controlify.gui.out_of_focus_input.tooltip"))
.build())
.binding(GlobalSettings.DEFAULT.outOfFocusInput, () -> globalSettings.outOfFocusInput, v -> globalSettings.outOfFocusInput = v)
.controller(TickBoxControllerBuilder::create)
.build())
.option(Option.<Boolean>createBuilder()
.name(Component.translatable("controlify.gui.keyboard_movement"))
.description(OptionDescription.createBuilder()
.text(Component.translatable("controlify.gui.keyboard_movement.tooltip"))
.build())
.binding(GlobalSettings.DEFAULT.keyboardMovement, () -> globalSettings.keyboardMovement, v -> globalSettings.keyboardMovement = v)
.controller(TickBoxControllerBuilder::create)
.build())
.option(ButtonOption.createBuilder()
.name(Component.translatable("controlify.gui.open_issue_tracker"))
.action((screen, button) -> Util.getPlatform().openUri("https://github.com/isxander/controlify/issues"))
.build())
.build())
.build().generateScreen(parent);
}
private static ResourceLocation screenshot(String filename) {
return Controlify.id("textures/screenshots/" + filename);
}
}