From 768c21ec2248aed6ecd523d877f0d1c2262bb83e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nico=20M=C3=BCller?= Date: Tue, 27 Jul 2021 17:31:41 +0200 Subject: [PATCH] v1.2.0 --- build.gradle.kts | 139 ++++----------- gradle.properties | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- settings.gradle | 2 - settings.gradle.kts | 14 ++ ...{Borderless.java => BorderlessWindow.java} | 36 ++-- .../borderless/FullscreenModeHolder.java | 90 ---------- .../FullscreenWindowEventListener.java | 48 ----- .../de/nekeras/borderless/ReflectionUtil.java | 59 ------- .../{ => client}/DesktopEnvironment.java | 47 ++++- .../client/FullscreenDisplayModeHolder.java | 84 +++++++++ .../nekeras/borderless/client/GlfwUtils.java | 101 +++++++++++ .../client/GlfwWindowAttribute.java | 41 +++++ .../borderless/client/ReflectionUtils.java | 99 +++++++++++ .../BorderlessFullscreenDisplay.java | 69 ++++++++ .../fullscreen/FullscreenDisplayMode.java | 61 +++++++ .../fullscreen/NativeFullscreenDisplay.java | 13 ++ .../NativeNonIconifyFullscreenDisplay.java | 23 +++ .../NativeWindowedFullscreenDisplay.java | 65 +++++++ .../borderless/client/gui/ButtonOption.java | 30 ++++ .../borderless/client/gui/ConfigScreen.java | 164 ++++++++++++++++++ .../client/gui/ConfigScreenOption.java | 47 +++++ .../borderless/client/gui/EnumOption.java | 60 +++++++ .../SizeChangedWindowEventListener.java | 53 ++++++ .../listener/VideoSettingsListener.java | 56 ++++++ .../de/nekeras/borderless/config/Config.java | 82 +++++++-- .../borderless/config/FocusLossConfig.java | 31 ++-- .../config/FullscreenModeConfig.java | 22 ++- .../borderless/config/gui/ConfigScreen.java | 116 ------------- .../config/gui/ConfigScreenOption.java | 26 --- .../borderless/config/gui/EnumOption.java | 64 ------- .../config/value/ConfigValueHolder.java | 46 ----- .../borderless/config/value/ValueHolder.java | 24 --- .../fullscreen/BorderlessFullscreen.java | 40 ----- .../borderless/fullscreen/FullscreenMode.java | 131 -------------- .../fullscreen/NativeFullscreen.java | 23 --- .../NativeNonIconifyFullscreen.java | 21 --- .../fullscreen/NativeWindowedFullscreen.java | 56 ------ .../util/AccessibleFieldDelegate.java | 114 ++++++++++++ .../nekeras/borderless/util/Translatable.java | 16 ++ src/main/resources/META-INF/mods.toml | 2 +- .../assets/borderless/lang/en_us.json | 8 +- 42 files changed, 1315 insertions(+), 912 deletions(-) delete mode 100644 settings.gradle create mode 100644 settings.gradle.kts rename src/main/java/de/nekeras/borderless/{Borderless.java => BorderlessWindow.java} (62%) delete mode 100644 src/main/java/de/nekeras/borderless/FullscreenModeHolder.java delete mode 100644 src/main/java/de/nekeras/borderless/FullscreenWindowEventListener.java delete mode 100644 src/main/java/de/nekeras/borderless/ReflectionUtil.java rename src/main/java/de/nekeras/borderless/{ => client}/DesktopEnvironment.java (53%) create mode 100644 src/main/java/de/nekeras/borderless/client/FullscreenDisplayModeHolder.java create mode 100644 src/main/java/de/nekeras/borderless/client/GlfwUtils.java create mode 100644 src/main/java/de/nekeras/borderless/client/GlfwWindowAttribute.java create mode 100644 src/main/java/de/nekeras/borderless/client/ReflectionUtils.java create mode 100644 src/main/java/de/nekeras/borderless/client/fullscreen/BorderlessFullscreenDisplay.java create mode 100644 src/main/java/de/nekeras/borderless/client/fullscreen/FullscreenDisplayMode.java create mode 100644 src/main/java/de/nekeras/borderless/client/fullscreen/NativeFullscreenDisplay.java create mode 100644 src/main/java/de/nekeras/borderless/client/fullscreen/NativeNonIconifyFullscreenDisplay.java create mode 100644 src/main/java/de/nekeras/borderless/client/fullscreen/NativeWindowedFullscreenDisplay.java create mode 100644 src/main/java/de/nekeras/borderless/client/gui/ButtonOption.java create mode 100644 src/main/java/de/nekeras/borderless/client/gui/ConfigScreen.java create mode 100644 src/main/java/de/nekeras/borderless/client/gui/ConfigScreenOption.java create mode 100644 src/main/java/de/nekeras/borderless/client/gui/EnumOption.java create mode 100644 src/main/java/de/nekeras/borderless/client/listener/SizeChangedWindowEventListener.java create mode 100644 src/main/java/de/nekeras/borderless/client/listener/VideoSettingsListener.java delete mode 100644 src/main/java/de/nekeras/borderless/config/gui/ConfigScreen.java delete mode 100644 src/main/java/de/nekeras/borderless/config/gui/ConfigScreenOption.java delete mode 100644 src/main/java/de/nekeras/borderless/config/gui/EnumOption.java delete mode 100644 src/main/java/de/nekeras/borderless/config/value/ConfigValueHolder.java delete mode 100644 src/main/java/de/nekeras/borderless/config/value/ValueHolder.java delete mode 100644 src/main/java/de/nekeras/borderless/fullscreen/BorderlessFullscreen.java delete mode 100644 src/main/java/de/nekeras/borderless/fullscreen/FullscreenMode.java delete mode 100644 src/main/java/de/nekeras/borderless/fullscreen/NativeFullscreen.java delete mode 100644 src/main/java/de/nekeras/borderless/fullscreen/NativeNonIconifyFullscreen.java delete mode 100644 src/main/java/de/nekeras/borderless/fullscreen/NativeWindowedFullscreen.java create mode 100644 src/main/java/de/nekeras/borderless/util/AccessibleFieldDelegate.java create mode 100644 src/main/java/de/nekeras/borderless/util/Translatable.java diff --git a/build.gradle.kts b/build.gradle.kts index 2a50ce2..487798d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,134 +1,71 @@ -import net.minecraftforge.gradle.common.util.MinecraftExtension -import java.time.Instant -import java.time.ZoneId -import java.time.format.DateTimeFormatter - val minecraftVersion: String by extra val modVersion: String by extra val forgeVersion: String by extra -buildscript { - repositories { - jcenter() - maven("https://files.minecraftforge.net/maven") - } - dependencies { - classpath("net.minecraftforge.gradle:ForgeGradle:3.+") - } -} - plugins { java - `maven-publish` + id("net.minecraftforge.gradle") } -apply(plugin = "net.minecraftforge.gradle") - -val mainSourceSet: SourceSet = java.sourceSets.findByName("main")!! - -fun DependencyHandlerScope.minecraft(): ExternalModuleDependency = - create( - group = "net.minecraftforge", - name = "forge", - version = "${minecraftVersion}-${forgeVersion}" - ).apply { add("minecraft", this) } - group = "de.nekeras" -version = "${minecraftVersion}-${modVersion}" - -java.sourceCompatibility = JavaVersion.VERSION_1_8 -java.targetCompatibility = JavaVersion.VERSION_1_8 - -tasks.withType { - sourceCompatibility = JavaVersion.VERSION_1_8.toString() - targetCompatibility = JavaVersion.VERSION_1_8.toString() -} +version = "$minecraftVersion-$modVersion" repositories { mavenCentral() } dependencies { - minecraft() + minecraft(group = "net.minecraftforge", name = "forge", version = "$minecraftVersion-$forgeVersion") +} + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 } -configure { +minecraft { mappings("official", minecraftVersion) - runs { - "client" { - workingDirectory(project.file("run")) + val client by runs.creating { + workingDirectory(project.file("run")) - property("forge.logging.markers", "SCAN,REGISTRIES,REGISTRYDUMP") - property("forge.logging.console.level", "debug") + property("forge.logging.markers", "SCAN,REGISTRIES,REGISTRYDUMP") + property("forge.logging.console.level", "debug") - mods { - "borderless" { - source(mainSourceSet) - } - } + val borderless by mods.creating { + source(sourceSets.main.get()) } } -} - -tasks.withType { - manifest { - attributes( - mapOf( - "Specification-Title" to project.name, - "Specification-Vendor" to "Nekeras", - "Specification-Version" to project.version, - "Implementation-Title" to project.name, - "Implementation-Version" to project.version, - "Implementation-Vendor" to "Nekeras", - "Implementation-Timestamp" to DateTimeFormatter - .ofPattern("yyyy-MM-dd'T'HH:mm:ssZ") - .withZone(ZoneId.systemDefault()) - .format(Instant.now()) - ) - ) - } -} -val forceModsTomlRefresh = tasks.register("forceModsTomlRefresh", Delete::class.java) { - delete(File(mainSourceSet.output.resourcesDir, "/META-INF/mods.toml")) -}.get() + val server by runs.creating { + workingDirectory(project.file("run")) -tasks.withType { - dependsOn(forceModsTomlRefresh) + property("forge.logging.markers", "SCAN,REGISTRIES,REGISTRYDUMP") + property("forge.logging.console.level", "debug") - from(mainSourceSet.resources.srcDirs) { - include("META-INF/mods.toml") - expand( - mapOf( - "version" to project.version, - "forge_version_major" to forgeVersion.split(".")[0] - ) - ) + val borderless by mods.creating { + source(sourceSets.main.get()) + } } } -tasks.withType { - source(mainSourceSet.allJava) +tasks.processResources { + from(sourceSets.main.get().resources.srcDirs) { + duplicatesStrategy = DuplicatesStrategy.INCLUDE + include("META-INF/mods.toml") + expand("version" to project.version) + } } -val sourcesJar = tasks.register("sourcesJar", Jar::class.java) { - classifier = "sources" - from(mainSourceSet.allJava) -}.get() - -val javadocJar = tasks.register("javadocJar", Jar::class.java) { - classifier = "javadoc" - from(tasks.getByName("javadoc")) -}.get() - -publishing { - publications { - create("maven") { - from(components["java"]) - artifact(sourcesJar) - artifact(javadocJar) - } +tasks.jar { + manifest { + attributes( + "Specification-Title" to project.name, + "Specification-Vendor" to "Nekeras", + "Specification-Version" to project.version, + "Implementation-Title" to project.name, + "Implementation-Version" to project.version, + "Implementation-Vendor" to "Nekeras" + ) } } - diff --git a/gradle.properties b/gradle.properties index 55414b3..9abfe4a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,5 +3,5 @@ org.gradle.jvmargs=-Xmx3G org.gradle.daemon=false minecraftVersion=1.16.5 -modVersion=1.1.1 +modVersion=1.2.0 forgeVersion=36.0.0 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 1cb8722..ace69b1 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,4 +1,4 @@ -distributionUrl=https\://services.gradle.org/distributions/gradle-4.9-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.1.1-all.zip distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStorePath=wrapper/dists diff --git a/settings.gradle b/settings.gradle deleted file mode 100644 index 7087cb3..0000000 --- a/settings.gradle +++ /dev/null @@ -1,2 +0,0 @@ -rootProject.name = 'borderless' - diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..ce50728 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,14 @@ +pluginManagement { + repositories { + maven("https://maven.minecraftforge.net") + } + resolutionStrategy { + eachPlugin { + when (requested.id.id) { + "net.minecraftforge.gradle" -> useModule("net.minecraftforge.gradle:ForgeGradle:5.1.+") + } + } + } +} + +rootProject.name = "BorderlessWindow" diff --git a/src/main/java/de/nekeras/borderless/Borderless.java b/src/main/java/de/nekeras/borderless/BorderlessWindow.java similarity index 62% rename from src/main/java/de/nekeras/borderless/Borderless.java rename to src/main/java/de/nekeras/borderless/BorderlessWindow.java index f8bb9a5..35834c1 100644 --- a/src/main/java/de/nekeras/borderless/Borderless.java +++ b/src/main/java/de/nekeras/borderless/BorderlessWindow.java @@ -1,10 +1,10 @@ package de.nekeras.borderless; +import de.nekeras.borderless.client.gui.ConfigScreen; import de.nekeras.borderless.config.Config; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.OnlyIn; import net.minecraftforge.eventbus.api.SubscribeEvent; -import net.minecraftforge.fml.DeferredWorkQueue; import net.minecraftforge.fml.ExtensionPoint; import net.minecraftforge.fml.ModLoadingContext; import net.minecraftforge.fml.common.Mod; @@ -16,52 +16,46 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import javax.annotation.Nullable; +import javax.annotation.Nonnull; /** * The main Forge mod class. */ -@Mod(Borderless.MOD_ID) +@Mod(BorderlessWindow.MOD_ID) @Mod.EventBusSubscriber(value = Dist.CLIENT, bus = Bus.MOD) -public class Borderless { +public class BorderlessWindow { /** * The mod id of this Forge mod. */ - public static final String MOD_ID = "borderless"; + public static final String MOD_ID = "borderlesswindow"; private static final Logger log = LogManager.getLogger(); - public Borderless() { + public BorderlessWindow() { + log.info("Creating mod instance"); ModLoadingContext context = ModLoadingContext.get(); - // Client dist only, make sure server is always compatible with this mod + log.info("Enable server compatibility"); context.registerExtensionPoint(ExtensionPoint.DISPLAYTEST, () -> Pair.of( () -> FMLNetworkConstants.IGNORESERVERONLY, (a, b) -> true)); - // Register the config + log.info("Register client configuration"); context.registerConfig(ModConfig.Type.CLIENT, Config.CONFIG_SPEC); } - @SuppressWarnings("deprecation") @OnlyIn(Dist.CLIENT) @SubscribeEvent - public static void onClientSetup(@Nullable FMLClientSetupEvent event) { + public static void onClientSetup(@Nonnull FMLClientSetupEvent event) { + log.info("Initializing from client context"); ModLoadingContext context = ModLoadingContext.get(); - // Config registration - context.registerExtensionPoint(ExtensionPoint.CONFIGGUIFACTORY, - () -> (mc, modListScreen) -> new de.nekeras.borderless.config.gui.ConfigScreen(modListScreen)); + log.info("Register client configuration screen"); + context.registerExtensionPoint(ExtensionPoint.CONFIGGUIFACTORY, () -> + (mc, modListScreen) -> new ConfigScreen(modListScreen)); log.info("Enqueue initialization work to main thread"); - DeferredWorkQueue.runLater(FullscreenModeHolder::initMinecraft); - } - - @OnlyIn(Dist.CLIENT) - @SubscribeEvent - public void onConfigReload(ModConfig.Reloading event) { - de.nekeras.borderless.FullscreenModeHolder.setFullscreenMode( - de.nekeras.borderless.fullscreen.FullscreenMode.fromConfig()); + event.enqueueWork(de.nekeras.borderless.client.FullscreenDisplayModeHolder::initMinecraft); } } diff --git a/src/main/java/de/nekeras/borderless/FullscreenModeHolder.java b/src/main/java/de/nekeras/borderless/FullscreenModeHolder.java deleted file mode 100644 index 779d0ff..0000000 --- a/src/main/java/de/nekeras/borderless/FullscreenModeHolder.java +++ /dev/null @@ -1,90 +0,0 @@ -package de.nekeras.borderless; - -import de.nekeras.borderless.fullscreen.FullscreenMode; -import net.minecraft.client.MainWindow; -import net.minecraft.client.Minecraft; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.lwjgl.glfw.GLFW; - -import javax.annotation.Nonnull; -import java.util.Objects; - -public class FullscreenModeHolder { - - private static final Logger log = LogManager.getLogger(); - private static FullscreenMode fullscreenMode; - - /** - * Initializes the Minecraft environment to make it compatible with this mod. - */ - public static void initMinecraft() { - log.info("Reading fullscreen mode from config"); - fullscreenMode = FullscreenMode.fromConfig(); - log.info("Overwriting Minecraft WindowEventListener"); - Minecraft minecraft = Minecraft.getInstance(); - MainWindow window = minecraft.getWindow(); - ReflectionUtil.updateWindowEventListener(window, FullscreenWindowEventListener::new); - log.info("Overwrite finished"); - FullscreenModeHolder.forceFullscreenModeUpdate(); - } - - /** - * Checks whether the {@link MainWindow} is currently in native fullscreen. This - * may return different results than {@link MainWindow#isFullscreen()}. The window is considered - * in native fullscreen if {@link GLFW#glfwGetWindowMonitor(long)} returns non-null and - * {@link MainWindow#isFullscreen()} returns true. - * - * @return true if the window is currently in native fullscreen, otherwise - * false - */ - public static boolean isInNativeFullscreen(@Nonnull MainWindow window) { - return window.isFullscreen() && GLFW.glfwGetWindowMonitor(window.getWindow()) != 0; - } - - /** - * The fullscreen mode that is applied instead of the native fullscreen once the user hits - * F11 or switches to fullscreen in the video settings. - * - * @return The fullscreen mode - */ - public static FullscreenMode getFullscreenMode() { - return fullscreenMode; - } - - /** - * The fullscreen mode that is applied instead of the native fullscreen once the user hits - * F11 or switches to fullscreen in the video settings. - * - * @param fullscreenMode The fullscreen mode - */ - public static void setFullscreenMode(@Nonnull FullscreenMode fullscreenMode) { - Objects.requireNonNull(fullscreenMode); - - Minecraft minecraft = Minecraft.getInstance(); - MainWindow window = minecraft.getWindow(); - FullscreenMode currentFullscreenMode = getFullscreenMode(); - - log.info("Resetting fullscreen mode '{}' - Window fullscreen: {}; Native fullscreen: {}", - currentFullscreenMode == null ? null : currentFullscreenMode.getClass().getName(), - window.isFullscreen(), - isInNativeFullscreen(window)); - - if (currentFullscreenMode != null) { - currentFullscreenMode.reset(window); - } - - if (window.isFullscreen()) { - log.info("Applying fullscreen mode '{}'", fullscreenMode.getClass().getName()); - FullscreenModeHolder.fullscreenMode = fullscreenMode; - fullscreenMode.apply(window); - } - } - - /** - * Triggers an update for current {@link FullscreenMode}. - */ - public static void forceFullscreenModeUpdate() { - setFullscreenMode(getFullscreenMode()); - } -} diff --git a/src/main/java/de/nekeras/borderless/FullscreenWindowEventListener.java b/src/main/java/de/nekeras/borderless/FullscreenWindowEventListener.java deleted file mode 100644 index 635dc46..0000000 --- a/src/main/java/de/nekeras/borderless/FullscreenWindowEventListener.java +++ /dev/null @@ -1,48 +0,0 @@ -package de.nekeras.borderless; - -import de.nekeras.borderless.fullscreen.FullscreenMode; -import net.minecraft.client.MainWindow; -import net.minecraft.client.renderer.IWindowEventListener; - -import javax.annotation.Nonnull; -import java.util.Objects; - -/** - * A custom {@link IWindowEventListener} that will call all original methods of the supplied - * default event listener. In addition, this method will apply or reset the current fullscreen mode, - * once {@link MainWindow#isFullscreen()} returns true or false - * respectively. - * - * @see FullscreenModeHolder#getFullscreenMode() - * @see FullscreenMode#apply(MainWindow) - * @see FullscreenMode#reset(MainWindow) - */ -public class FullscreenWindowEventListener implements IWindowEventListener { - - private final IWindowEventListener defaultWindowEventListener; - - public FullscreenWindowEventListener(@Nonnull IWindowEventListener defaultWindowEventListener) { - this.defaultWindowEventListener = Objects.requireNonNull(defaultWindowEventListener); - } - - @Override - public void setWindowActive(boolean focused) { - defaultWindowEventListener.setWindowActive(focused); - } - - @Override - public void resizeDisplay() { - defaultWindowEventListener.resizeDisplay(); - - if (ReflectionUtil.isCalledByGlfwCallback()) { - return; - } - - FullscreenModeHolder.forceFullscreenModeUpdate(); - } - - @Override - public void cursorEntered() { - defaultWindowEventListener.cursorEntered(); - } -} diff --git a/src/main/java/de/nekeras/borderless/ReflectionUtil.java b/src/main/java/de/nekeras/borderless/ReflectionUtil.java deleted file mode 100644 index 0164694..0000000 --- a/src/main/java/de/nekeras/borderless/ReflectionUtil.java +++ /dev/null @@ -1,59 +0,0 @@ -package de.nekeras.borderless; - -import java.lang.reflect.Field; -import java.util.Arrays; -import java.util.function.Function; - -import org.lwjgl.glfw.GLFWFramebufferSizeCallbackI; -import org.lwjgl.glfw.GLFWWindowSizeCallbackI; - -import net.minecraft.client.MainWindow; -import net.minecraft.client.renderer.IWindowEventListener; - -/** - * Minecraft native code reflection util for easier access of some fields and methods. - */ -public final class ReflectionUtil { - - private ReflectionUtil() {} - - /** - * Updates the {@link IWindowEventListener} in the {@link MainWindow} instance with a custom - * window event listener. - * - * @param window The main window instance that should be updated - * @param updateSupplier A function that accepts the original value of the - * {@link IWindowEventListener} field in the {@link MainWindow} instance - * and returns a new value that should be assigned - */ - public static void updateWindowEventListener(MainWindow window, - Function updateSupplier) { - - Field oldListenerField = Arrays.stream(MainWindow.class.getDeclaredFields()) - .filter(field -> field.getType() == IWindowEventListener.class) - .findFirst() - .orElseThrow(() -> new IllegalStateException( - "Could not find event listener, is this the correct Minecraft version?")); - - try { - oldListenerField.setAccessible(true); - IWindowEventListener oldListener = (IWindowEventListener) oldListenerField.get(window); - oldListenerField.set(window, updateSupplier.apply(oldListener)); - } catch (IllegalAccessException e) { - throw new IllegalStateException("Could not access or update old listener", e); - } - } - - /** - * Checks if this method was called by either the {@link GLFWFramebufferSizeCallbackI} or - * {@link GLFWWindowSizeCallbackI}. - * - * @return true if called by a GLFW callback, otherwise false - */ - public static boolean isCalledByGlfwCallback() { - return Arrays.stream(Thread.currentThread().getStackTrace()).anyMatch(e -> - e.getClassName().equals(GLFWFramebufferSizeCallbackI.class.getName()) - || e.getClassName().equals(GLFWWindowSizeCallbackI.class.getName())); - } - -} diff --git a/src/main/java/de/nekeras/borderless/DesktopEnvironment.java b/src/main/java/de/nekeras/borderless/client/DesktopEnvironment.java similarity index 53% rename from src/main/java/de/nekeras/borderless/DesktopEnvironment.java rename to src/main/java/de/nekeras/borderless/client/DesktopEnvironment.java index 1e28648..d103b4f 100644 --- a/src/main/java/de/nekeras/borderless/DesktopEnvironment.java +++ b/src/main/java/de/nekeras/borderless/client/DesktopEnvironment.java @@ -1,35 +1,64 @@ -package de.nekeras.borderless; +package de.nekeras.borderless.client; +import de.nekeras.borderless.client.fullscreen.FullscreenDisplayMode; +import de.nekeras.borderless.client.fullscreen.NativeFullscreenDisplay; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.lwjgl.system.Platform; -import de.nekeras.borderless.fullscreen.NativeFullscreen; +import javax.annotation.Nonnull; /** * The desktop environment we are running on, retrievable by {@link #get()}. */ +@OnlyIn(Dist.CLIENT) public enum DesktopEnvironment { /** * The Windows desktop environment. */ - WINDOWS, + WINDOWS(FullscreenDisplayMode.BORDERLESS), /** * X11 window system used by Linux distributions. */ - X11, + X11(FullscreenDisplayMode.NATIVE_NON_ICONIFY), /** - * An unknown desktop environment, this should always use {@link NativeFullscreen}. + * Wayland window server used by Linux distributions. */ - GENERIC; + WAYLAND(FullscreenDisplayMode.NATIVE), + + /** + * An unknown desktop environment, this should always use {@link NativeFullscreenDisplay}. + */ + GENERIC(FullscreenDisplayMode.NATIVE); + private static final String LINUX_WINDOW_SYSTEM_VARIABLE = "XDG_SESSION_TYPE"; + private static final String X11_NAME = "x11"; + private static final String WAYLAND_NAME = "wayland"; private static final DesktopEnvironment CURRENT; private static final Logger log = LogManager.getLogger(); + private final FullscreenDisplayMode bestFullscreenDisplayMode; + + DesktopEnvironment(@Nonnull FullscreenDisplayMode bestFullscreenDisplayMode) { + this.bestFullscreenDisplayMode = bestFullscreenDisplayMode; + } + + /** + * The best {@link FullscreenDisplayMode} for this desktop environment. + * + * @return The best mode + */ + @Nonnull + public FullscreenDisplayMode getBestFullscreenDisplayMode() { + return bestFullscreenDisplayMode; + } + static { log.info("Determining desktop environment"); @@ -42,10 +71,11 @@ public enum DesktopEnvironment { case LINUX: String result = System.getenv(LINUX_WINDOW_SYSTEM_VARIABLE); - if ("x11".equalsIgnoreCase(result)) { + if (X11_NAME.equalsIgnoreCase(result)) { current = X11; + } else if (WAYLAND_NAME.equalsIgnoreCase(result)) { + current = WAYLAND; } else { - // Also treats Wayland as unknown as this seems buggy with LWJGL current = GENERIC; log.warn("Unknown window system: {}", result); } @@ -65,6 +95,7 @@ public enum DesktopEnvironment { * * @return The desktop environment */ + @Nonnull public static DesktopEnvironment get() { return CURRENT; } diff --git a/src/main/java/de/nekeras/borderless/client/FullscreenDisplayModeHolder.java b/src/main/java/de/nekeras/borderless/client/FullscreenDisplayModeHolder.java new file mode 100644 index 0000000..457a4f6 --- /dev/null +++ b/src/main/java/de/nekeras/borderless/client/FullscreenDisplayModeHolder.java @@ -0,0 +1,84 @@ +package de.nekeras.borderless.client; + +import de.nekeras.borderless.client.fullscreen.FullscreenDisplayMode; +import de.nekeras.borderless.client.listener.SizeChangedWindowEventListener; +import de.nekeras.borderless.config.Config; +import net.minecraft.client.MainWindow; +import net.minecraft.client.Minecraft; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Optional; + +/** + * Singleton class holding the current fullscreen display mode. + */ +@OnlyIn(Dist.CLIENT) +public class FullscreenDisplayModeHolder { + + private static final Logger log = LogManager.getLogger(); + private static final MainWindow window = Minecraft.getInstance().getWindow(); + private static FullscreenDisplayMode currentMode; + + /** + * Initializes the Minecraft environment to make it compatible with this mod. + */ + public static void initMinecraft() { + log.info("Overwriting Minecraft WindowEventListener"); + ReflectionUtils.updateWindowEventListener(window, oldListener -> new SizeChangedWindowEventListener( + oldListener, FullscreenDisplayModeHolder::setFullscreenDisplayModeFromConfig)); + + log.info("Overwrite finished"); + FullscreenDisplayModeHolder.setFullscreenDisplayModeFromConfig(); + } + + /** + * The fullscreen mode that is applied instead of the native fullscreen once the user hits + * F11 or switches to fullscreen in the video settings. + * + * @return The fullscreen mode + */ + @Nonnull + public static Optional getFullscreenDisplayMode() { + return Optional.ofNullable(currentMode); + } + + /** + * The fullscreen mode that is applied instead of the native fullscreen once the user hits + * F11 or switches to fullscreen in the video settings. + * + * @param newMode The fullscreen mode + */ + public static void setFullscreenDisplayMode(@Nullable FullscreenDisplayMode newMode) { + log.info("Detected fullscreen mode change from {} to {}", currentMode, newMode); + + if (currentMode != null) { + currentMode.reset(window); + } + + log.info("Refreshing {}", newMode); + + if (newMode != null) { + if (window.isFullscreen()) { + newMode.apply(window); + } else { + newMode.reset(window); + } + } + + currentMode = newMode; + } + + /** + * Re-applies the current fullscreen display mode set in the {@link de.nekeras.borderless.config.Config}. + */ + public static void setFullscreenDisplayModeFromConfig() { + FullscreenDisplayMode configMode = Config.getFullscreenDisplayMode(); + log.info("Refreshing fullscreen mode from config to {}", configMode); + setFullscreenDisplayMode(configMode); + } +} diff --git a/src/main/java/de/nekeras/borderless/client/GlfwUtils.java b/src/main/java/de/nekeras/borderless/client/GlfwUtils.java new file mode 100644 index 0000000..bf62e6e --- /dev/null +++ b/src/main/java/de/nekeras/borderless/client/GlfwUtils.java @@ -0,0 +1,101 @@ +package de.nekeras.borderless.client; + +import net.minecraft.client.MainWindow; +import net.minecraft.client.Monitor; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.lwjgl.glfw.GLFW; + +import javax.annotation.Nonnull; +import java.util.Optional; + +/** + * Helper class for access for various GLFW functions with logging support. + */ +@OnlyIn(Dist.CLIENT) +public final class GlfwUtils { + + private static final Logger log = LogManager.getLogger(); + + private GlfwUtils() { + } + + /** + * Retrieves the monitor's name from GLFW. + * + * @param monitor The monitor + * @return The monitor's name + */ + @Nonnull + public static String getMonitorName(@Nonnull Monitor monitor) { + String name = GLFW.glfwGetMonitorName(monitor.getMonitor()); + + if (name == null) { + log.warn("Could not retrieve monitor name for {}", monitor.getMonitor()); + return "- ERROR -"; + } else { + return name; + } + } + + /** + * Tries to get the monitor for the window. If it could be found and is non-null, it will be returned. Otherwise an + * error is printed. + * + * @param window The window + * @return The monitor wrapped in an {@link Optional}. + */ + @Nonnull + public static Optional tryGetMonitor(@Nonnull MainWindow window) { + Monitor monitor = window.findBestMonitor(); + + if (monitor == null) { + log.error("Window's current monitor could not be retrieved"); + } + + return Optional.ofNullable(monitor); + } + + /** + * Enables a single window attribute. + * + * @param window The window + * @param attribute The attribute + */ + public static void enableWindowAttribute(@Nonnull MainWindow window, @Nonnull GlfwWindowAttribute attribute) { + log.info("Enable window attribute {}", attribute.name()); + GLFW.glfwSetWindowAttrib(window.getWindow(), attribute.getBit(), GLFW.GLFW_TRUE); + } + + /** + * Disables a single window attribute. + * + * @param window The window + * @param attribute The attribute + */ + public static void disableWindowAttribute(@Nonnull MainWindow window, @Nonnull GlfwWindowAttribute attribute) { + log.info("Enable window attribute {}", attribute.name()); + GLFW.glfwSetWindowAttrib(window.getWindow(), attribute.getBit(), GLFW.GLFW_FALSE); + } + + /** + * Restores all default window attributes that are supported by the {@link GlfwWindowAttribute} enum. + * + * @param window The window + */ + public static void applyDefaultWindowAttributes(@Nonnull MainWindow window) { + log.info("Resetting window attributes"); + + for (GlfwWindowAttribute attribute : GlfwWindowAttribute.values()) { + if (attribute.isEnabledByDefault()) { + enableWindowAttribute(window, attribute); + } else { + disableWindowAttribute(window, attribute); + } + } + + log.info("Done resetting window attributes"); + } +} diff --git a/src/main/java/de/nekeras/borderless/client/GlfwWindowAttribute.java b/src/main/java/de/nekeras/borderless/client/GlfwWindowAttribute.java new file mode 100644 index 0000000..a40a12b --- /dev/null +++ b/src/main/java/de/nekeras/borderless/client/GlfwWindowAttribute.java @@ -0,0 +1,41 @@ +package de.nekeras.borderless.client; + +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; +import org.lwjgl.glfw.GLFW; + +/** + * Supported window attributes that are used by Borderless Window for the fullscreen display modes. + */ +@OnlyIn(Dist.CLIENT) +public enum GlfwWindowAttribute { + + DECORATED(GLFW.GLFW_DECORATED, true), + AUTO_ICONIFY(GLFW.GLFW_AUTO_ICONIFY, true); + + private final int bit; + private final boolean enabledByDefault; + + GlfwWindowAttribute(int bit, boolean enabledByDefault) { + this.bit = bit; + this.enabledByDefault = enabledByDefault; + } + + /** + * The GLFW bit used for unique identification. + * + * @return The bit value + */ + public int getBit() { + return bit; + } + + /** + * Whether the attribute is enabled by default. + * + * @return The default value + */ + public boolean isEnabledByDefault() { + return enabledByDefault; + } +} diff --git a/src/main/java/de/nekeras/borderless/client/ReflectionUtils.java b/src/main/java/de/nekeras/borderless/client/ReflectionUtils.java new file mode 100644 index 0000000..01d68b6 --- /dev/null +++ b/src/main/java/de/nekeras/borderless/client/ReflectionUtils.java @@ -0,0 +1,99 @@ +package de.nekeras.borderless.client; + +import de.nekeras.borderless.util.AccessibleFieldDelegate; +import net.minecraft.client.MainWindow; +import net.minecraft.client.gui.screen.VideoSettingsScreen; +import net.minecraft.client.gui.widget.list.OptionsRowList; +import net.minecraft.client.renderer.IWindowEventListener; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; +import org.lwjgl.glfw.GLFWFramebufferSizeCallbackI; +import org.lwjgl.glfw.GLFWWindowSizeCallbackI; + +import javax.annotation.Nonnull; +import java.util.Arrays; +import java.util.Optional; +import java.util.function.Function; + +/** + * Minecraft native code reflection util for easier access of some fields and methods. + */ +@OnlyIn(Dist.CLIENT) +public final class ReflectionUtils { + + private static final AccessibleFieldDelegate windowEventListenerAccessor = + makeFieldAccessible(MainWindow.class, IWindowEventListener.class); + private static final AccessibleFieldDelegate optionsRowListAccessor = + tryMakeFieldAccessible(VideoSettingsScreen.class, OptionsRowList.class, thisRef -> null); + + private ReflectionUtils() { + } + + /** + * Makes a field of type T in object thisRef accessible through reflection. + * + * @param inObject The object to lookup for the field + * @param fieldType The field to access + * @throws IllegalStateException If there is no such field of type F + */ + @Nonnull + private static AccessibleFieldDelegate makeFieldAccessible(@Nonnull Class inClass, @Nonnull Class fieldType) { + try { + return new AccessibleFieldDelegate<>(inClass, fieldType); + } catch (NoSuchFieldException e) { + throw new IllegalStateException(String.format("Expected field of type %s in class %s", + inClass.getName(), fieldType.getName()), e); + } + } + + /** + * Makes a field of type T in object thisRef accessible through reflection. + * + * @param inObject The object to lookup for the field + * @param fieldType The field to access + * @param defaultSupplier The supplier which is called in when retrieving the value if the field was not found + */ + @Nonnull + private static AccessibleFieldDelegate tryMakeFieldAccessible(@Nonnull Class inClass, @Nonnull Class fieldType, @Nonnull Function defaultSupplier) { + return new AccessibleFieldDelegate(inClass, fieldType, defaultSupplier); + } + + /** + * Checks if this method was called by either the {@link GLFWFramebufferSizeCallbackI} or + * {@link GLFWWindowSizeCallbackI}. + * + * @return true if called by a GLFW callback, otherwise false + */ + public static boolean isCalledByGlfwCallback() { + return Arrays.stream(Thread.currentThread().getStackTrace()).anyMatch(e -> + e.getClassName().equals(GLFWFramebufferSizeCallbackI.class.getName()) + || e.getClassName().equals(GLFWWindowSizeCallbackI.class.getName())); + } + + /** + * Updates the {@link IWindowEventListener} in the {@link MainWindow} instance with a custom + * window event listener. + * + * @param window The window to update the listener for + * @param updateSupplier A function that accepts the original value of the + * {@link IWindowEventListener} field in the {@link MainWindow} instance + * and returns a new value that should be assigned + */ + public static void updateWindowEventListener(@Nonnull MainWindow window, @Nonnull Function updateSupplier) { + IWindowEventListener oldListener = windowEventListenerAccessor.getValue(window); + IWindowEventListener newListener = updateSupplier.apply(oldListener); + windowEventListenerAccessor.setValue(window, newListener); + } + + /** + * Retrieves the optionsRowList field in the {@link VideoSettingsScreen}, if available. If might become + * unavailable due to OptiFine or some other mod replacing the original {@link VideoSettingsScreen}. + * + * @param screen The screen + * @return The options row list if it exists + */ + @Nonnull + public static Optional getOptionsRowList(@Nonnull VideoSettingsScreen screen) { + return Optional.ofNullable(optionsRowListAccessor.getValue(screen)); + } +} diff --git a/src/main/java/de/nekeras/borderless/client/fullscreen/BorderlessFullscreenDisplay.java b/src/main/java/de/nekeras/borderless/client/fullscreen/BorderlessFullscreenDisplay.java new file mode 100644 index 0000000..a5c23c2 --- /dev/null +++ b/src/main/java/de/nekeras/borderless/client/fullscreen/BorderlessFullscreenDisplay.java @@ -0,0 +1,69 @@ +package de.nekeras.borderless.client.fullscreen; + +import de.nekeras.borderless.client.GlfwUtils; +import de.nekeras.borderless.client.GlfwWindowAttribute; +import net.minecraft.client.MainWindow; +import net.minecraft.client.Monitor; +import net.minecraft.client.renderer.VideoMode; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.lwjgl.glfw.GLFW; + +import javax.annotation.Nonnull; + +/** + * Switches into a windowed mode and removes the borders of the window. After that, maximizes the + * window on the current monitor as returned by {@link MainWindow#findBestMonitor()}. + */ +@OnlyIn(Dist.CLIENT) +public class BorderlessFullscreenDisplay implements FullscreenDisplayMode { + + private static final Logger log = LogManager.getLogger(); + + @Override + public void apply(@Nonnull MainWindow window) { + FullscreenDisplayMode.super.apply(window); + + Monitor monitor = GlfwUtils.tryGetMonitor(window).orElse(null); + + if (monitor == null) { + log.error("Window's monitor could not be retrieved"); + } else { + VideoMode videoMode = monitor.getCurrentMode(); + String name = GlfwUtils.getMonitorName(monitor); + int x = monitor.getX(); + int y = monitor.getY(); + int width = videoMode.getWidth(); + int height = videoMode.getHeight(); + + log.info("Apply on monitor {} at ({}|{}) size ({} x {})", name, x, y, width, height); + + GlfwUtils.disableWindowAttribute(window, GlfwWindowAttribute.DECORATED); + GLFW.glfwSetWindowMonitor(window.getWindow(), 0, x, y, width, height, GLFW.GLFW_DONT_CARE); + } + } + + @Override + public void reset(@Nonnull MainWindow window) { + FullscreenDisplayMode.super.reset(window); + + if (window.isFullscreen()) { + Monitor monitor = GlfwUtils.tryGetMonitor(window).orElse(null); + + if (monitor == null) { + log.error("Window's monitor could not be retrieved"); + } else { + VideoMode videoMode = monitor.getCurrentMode(); + String name = GlfwUtils.getMonitorName(monitor); + int width = videoMode.getWidth(); + int height = videoMode.getHeight(); + + log.info("Reset on monitor {} at size ({} x {})", name, width, height); + + GLFW.glfwSetWindowMonitor(window.getWindow(), monitor.getMonitor(), 0, 0, width, height, GLFW.GLFW_DONT_CARE); + } + } + } +} diff --git a/src/main/java/de/nekeras/borderless/client/fullscreen/FullscreenDisplayMode.java b/src/main/java/de/nekeras/borderless/client/fullscreen/FullscreenDisplayMode.java new file mode 100644 index 0000000..84d627f --- /dev/null +++ b/src/main/java/de/nekeras/borderless/client/fullscreen/FullscreenDisplayMode.java @@ -0,0 +1,61 @@ +package de.nekeras.borderless.client.fullscreen; + +import de.nekeras.borderless.client.GlfwUtils; +import net.minecraft.client.MainWindow; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import javax.annotation.Nonnull; + +/** + * A fullscreen mode that can be applied for the Minecraft {@link MainWindow}. The {@link + * #apply(MainWindow)} method + * will be called every time the window enters fullscreen, the {@link #reset(MainWindow)} method + * every time the window + * leaves fullscreen. + */ +@OnlyIn(Dist.CLIENT) +public interface FullscreenDisplayMode { + + /** + * Switches into a windowed mode and removes the borders of the window. After that, maximizes + * the window on the current monitor as returned by {@link MainWindow#get()}. + */ + FullscreenDisplayMode BORDERLESS = new BorderlessFullscreenDisplay(); + + /** + * The native fullscreen mode, this fullscreen mode can be used to disable this mod, as this + * mode will not affect the {@link MainWindow}. + */ + FullscreenDisplayMode NATIVE = new NativeFullscreenDisplay(); + + /** + * The native fullscreen mode, but without automatic iconify on focus loss of the window. + */ + FullscreenDisplayMode NATIVE_NON_ICONIFY = new NativeNonIconifyFullscreenDisplay(); + + /** + * The same as {@link NativeFullscreenDisplay}, but switches to normal windowed mode on focus loss of + * the window. + */ + FullscreenDisplayMode NATIVE_SWITCH_TO_WINDOWED = new NativeWindowedFullscreenDisplay(); + + /** + * Applies this fullscreen mode on the supplied window. + * + * @param window The window + */ + default void apply(@Nonnull MainWindow window) { + GlfwUtils.applyDefaultWindowAttributes(window); + } + + /** + * Resets this fullscreen mode on the supplied window, reverting any changes that were made in + * {@link #apply(MainWindow)}. + * + * @param window The window + */ + default void reset(@Nonnull MainWindow window) { + GlfwUtils.applyDefaultWindowAttributes(window); + } +} diff --git a/src/main/java/de/nekeras/borderless/client/fullscreen/NativeFullscreenDisplay.java b/src/main/java/de/nekeras/borderless/client/fullscreen/NativeFullscreenDisplay.java new file mode 100644 index 0000000..f9461db --- /dev/null +++ b/src/main/java/de/nekeras/borderless/client/fullscreen/NativeFullscreenDisplay.java @@ -0,0 +1,13 @@ +package de.nekeras.borderless.client.fullscreen; + +import net.minecraft.client.MainWindow; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +/** + * The native fullscreen mode, this fullscreen mode can be used to disable this mod, as this mode + * will not affect the {@link MainWindow}. + */ +@OnlyIn(Dist.CLIENT) +public class NativeFullscreenDisplay implements FullscreenDisplayMode { +} diff --git a/src/main/java/de/nekeras/borderless/client/fullscreen/NativeNonIconifyFullscreenDisplay.java b/src/main/java/de/nekeras/borderless/client/fullscreen/NativeNonIconifyFullscreenDisplay.java new file mode 100644 index 0000000..0eab743 --- /dev/null +++ b/src/main/java/de/nekeras/borderless/client/fullscreen/NativeNonIconifyFullscreenDisplay.java @@ -0,0 +1,23 @@ +package de.nekeras.borderless.client.fullscreen; + +import de.nekeras.borderless.client.GlfwUtils; +import de.nekeras.borderless.client.GlfwWindowAttribute; +import net.minecraft.client.MainWindow; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import javax.annotation.Nonnull; + +/** + * The native fullscreen mode, but without automatic iconify on focus loss of the window. + */ +@OnlyIn(Dist.CLIENT) +public class NativeNonIconifyFullscreenDisplay implements FullscreenDisplayMode { + + @Override + public void apply(@Nonnull MainWindow window) { + FullscreenDisplayMode.super.apply(window); + + GlfwUtils.disableWindowAttribute(window, GlfwWindowAttribute.AUTO_ICONIFY); + } +} diff --git a/src/main/java/de/nekeras/borderless/client/fullscreen/NativeWindowedFullscreenDisplay.java b/src/main/java/de/nekeras/borderless/client/fullscreen/NativeWindowedFullscreenDisplay.java new file mode 100644 index 0000000..7838153 --- /dev/null +++ b/src/main/java/de/nekeras/borderless/client/fullscreen/NativeWindowedFullscreenDisplay.java @@ -0,0 +1,65 @@ +package de.nekeras.borderless.client.fullscreen; + +import de.nekeras.borderless.client.GlfwUtils; +import de.nekeras.borderless.client.GlfwWindowAttribute; +import net.minecraft.client.MainWindow; +import net.minecraft.client.Monitor; +import net.minecraft.client.renderer.VideoMode; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.lwjgl.glfw.GLFW; + +import javax.annotation.Nonnull; + +/** + * The same as {@link NativeFullscreenDisplay}, but switches to normal windowed mode on focus loss of + * the window. + */ +@OnlyIn(Dist.CLIENT) +public class NativeWindowedFullscreenDisplay implements FullscreenDisplayMode { + + private static final Logger log = LogManager.getLogger(); + + @Override + public void apply(@Nonnull MainWindow window) { + FullscreenDisplayMode.super.apply(window); + + GlfwUtils.disableWindowAttribute(window, GlfwWindowAttribute.AUTO_ICONIFY); + GLFW.glfwSetWindowAttrib(window.getWindow(), GLFW.GLFW_AUTO_ICONIFY, GLFW.GLFW_FALSE); + GLFW.glfwSetWindowFocusCallback(window.getWindow(), (win, focused) -> { + if (focused) { + onFocusGained(window); + } else { + onFocusLost(window); + } + }); + } + + @Override + public void reset(@Nonnull MainWindow window) { + FullscreenDisplayMode.super.reset(window); + + GLFW.glfwSetWindowFocusCallback(window.getWindow(), null); + } + + private void onFocusGained(@Nonnull MainWindow window) { + Monitor monitor = GlfwUtils.tryGetMonitor(window).orElse(null); + + if (monitor == null) { + log.error("Could not find monitor for window"); + } else { + VideoMode videoMode = window.getPreferredFullscreenVideoMode().orElseGet(monitor::getCurrentMode); + + GLFW.glfwSetWindowMonitor(window.getWindow(), monitor.getMonitor(), 0, 0, + videoMode.getWidth(), videoMode.getHeight(), videoMode.getRefreshRate()); + } + } + + private void onFocusLost(@Nonnull MainWindow window) { + GLFW.glfwSetWindowMonitor(window.getWindow(), 0, + window.getX(), window.getY(), + window.getWidth(), window.getHeight(), GLFW.GLFW_DONT_CARE); + } +} diff --git a/src/main/java/de/nekeras/borderless/client/gui/ButtonOption.java b/src/main/java/de/nekeras/borderless/client/gui/ButtonOption.java new file mode 100644 index 0000000..5751ab0 --- /dev/null +++ b/src/main/java/de/nekeras/borderless/client/gui/ButtonOption.java @@ -0,0 +1,30 @@ +package de.nekeras.borderless.client.gui; + +import net.minecraft.client.AbstractOption; +import net.minecraft.client.GameSettings; +import net.minecraft.client.gui.widget.Widget; +import net.minecraft.client.gui.widget.button.Button; +import net.minecraft.client.gui.widget.button.OptionButton; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import javax.annotation.Nonnull; + +@OnlyIn(Dist.CLIENT) +public class ButtonOption extends AbstractOption { + + private static final int BUTTON_HEIGHT = 20; + + private final Button.IPressable onClick; + + public ButtonOption(@Nonnull String title, @Nonnull Button.IPressable onClick) { + super(title); + this.onClick = onClick; + } + + @Nonnull + @Override + public Widget createButton(@Nonnull GameSettings settings, int x, int y, int width) { + return new OptionButton(x, y, width, BUTTON_HEIGHT, this, getCaption(), onClick); + } +} diff --git a/src/main/java/de/nekeras/borderless/client/gui/ConfigScreen.java b/src/main/java/de/nekeras/borderless/client/gui/ConfigScreen.java new file mode 100644 index 0000000..16c9046 --- /dev/null +++ b/src/main/java/de/nekeras/borderless/client/gui/ConfigScreen.java @@ -0,0 +1,164 @@ +package de.nekeras.borderless.client.gui; + +import com.mojang.blaze3d.matrix.MatrixStack; +import de.nekeras.borderless.client.FullscreenDisplayModeHolder; +import de.nekeras.borderless.config.Config; +import de.nekeras.borderless.config.FocusLossConfig; +import de.nekeras.borderless.config.FullscreenModeConfig; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.DialogTexts; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.widget.Widget; +import net.minecraft.client.gui.widget.button.Button; +import net.minecraft.util.text.ITextComponent; +import net.minecraft.util.text.TranslationTextComponent; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import javax.annotation.Nonnull; + +@OnlyIn(Dist.CLIENT) +public class ConfigScreen extends Screen { + + private static final String DESCRIPTION_KEY_BASE = "borderless.%s"; + private static final int LAYOUT_MAX_WIDTH = 250; + private static final int WHITE = 0xffffff; + private static final int YELLOW = 0xffff00; + private static final int RED = 0xff0000; + private static final int LINE_HEIGHT = 25; + private static final ITextComponent titleText = new TranslationTextComponent("borderless.config.title"); + private static final ITextComponent applyText = new TranslationTextComponent("borderless.config.apply"); + private static final ITextComponent changedWarningText = new TranslationTextComponent("borderless.config.changed"); + private static final ITextComponent disabledText = new TranslationTextComponent("borderless.config.disabled.description"); + private static final Logger log = LogManager.getLogger(); + + private final Screen parent; + + private Widget enabledButton; + private Widget fullscreenModeButton; + private Widget focusLossButton; + + public ConfigScreen(@Nonnull Screen parent) { + super(titleText); + this.parent = parent; + } + + @Override + protected void init() { + super.init(); + + boolean initialEnabledState = Config.GENERAL.enabled.get(); + FullscreenModeConfig initialFullscreenMode = Config.GENERAL.fullscreenMode.get(); + FocusLossConfig initialFocusLossMode = Config.GENERAL.focusLoss.get(); + + Minecraft minecraft = Minecraft.getInstance(); + int x = getHorizontalLayoutStart(width); + + enabledButton = ConfigScreenOption.enabled.createButton(minecraft.options, x, LINE_HEIGHT * 2, LAYOUT_MAX_WIDTH); + fullscreenModeButton = ConfigScreenOption.fullscreenMode.createButton(minecraft.options, x, LINE_HEIGHT * 3, LAYOUT_MAX_WIDTH); + focusLossButton = ConfigScreenOption.focusLoss.createButton(minecraft.options, x, LINE_HEIGHT * 4, LAYOUT_MAX_WIDTH); + + Button applyButton = new Button(width / 2 - 125, height - 75, 100, 20, applyText, btn -> { + log.info("Apply button in Borderless Window Config Screen pressed"); + FullscreenDisplayModeHolder.setFullscreenDisplayModeFromConfig(); + onClose(); + }); + + Button cancelButton = new Button(width / 2 + 25, height - 75, 100, 20, DialogTexts.GUI_CANCEL, btn -> { + log.info("Cancel button in Borderless Window Config Screen pressed, resetting to {}, {}, {}", + initialEnabledState, initialFullscreenMode, initialFocusLossMode); + Config.GENERAL.enabled.set(initialEnabledState); + Config.GENERAL.fullscreenMode.set(initialFullscreenMode); + Config.GENERAL.focusLoss.set(initialFocusLossMode); + onClose(); + }); + + addButton(enabledButton); + addButton(fullscreenModeButton); + addButton(focusLossButton); + addButton(applyButton); + addButton(cancelButton); + + refreshButtonStates(); + } + + @Override + public void tick() { + super.tick(); + + refreshButtonStates(); + } + + @Override + public void render(@Nonnull MatrixStack matrixStack, int mouseX, int mouseY, float frameTime) { + Minecraft minecraft = Minecraft.getInstance(); + + this.renderBackground(matrixStack); + + renderTitle(matrixStack, minecraft, width); + renderDescription(minecraft, width); + renderChangedWarning(minecraft, width, height); + + super.render(matrixStack, mouseX, mouseY, frameTime); + } + + @Override + public void onClose() { + super.onClose(); + Minecraft.getInstance().setScreen(parent); + } + + private void renderTitle(@Nonnull MatrixStack matrixStack, @Nonnull Minecraft minecraft, int width) { + drawCenteredString(matrixStack, minecraft.font, title, width / 2, 20, WHITE); + } + + private void renderDescription(@Nonnull Minecraft minecraft, int width) { + int x = getHorizontalLayoutStart(width); + int y = LINE_HEIGHT * 5; + + if (ConfigScreenOption.enabled.get(minecraft.options)) { + minecraft.font.drawWordWrap(new TranslationTextComponent(getDescriptionKey()), x, y, LAYOUT_MAX_WIDTH, WHITE); + } else { + minecraft.font.drawWordWrap(disabledText, x, y, LAYOUT_MAX_WIDTH, RED); + } + } + + private void renderChangedWarning(@Nonnull Minecraft minecraft, int width, int height) { + int x = getHorizontalLayoutStart(width); + int y = height - 50; + + minecraft.font.drawWordWrap(changedWarningText, x, y, LAYOUT_MAX_WIDTH, YELLOW); + } + + private String getDescriptionKey() { + Minecraft minecraft = Minecraft.getInstance(); + + FullscreenModeConfig mode = ConfigScreenOption.fullscreenMode.getValue(minecraft.options); + String modeKey = String.format(DESCRIPTION_KEY_BASE, mode.name().toLowerCase()); + + if (mode != FullscreenModeConfig.NATIVE) { + return modeKey; + } else { + FocusLossConfig focusLoss = ConfigScreenOption.focusLoss.getValue(minecraft.options); + + return String.format("%s.%s", modeKey, focusLoss.name().toLowerCase()); + } + } + + private void refreshButtonStates() { + Minecraft minecraft = Minecraft.getInstance(); + + boolean enabled = ConfigScreenOption.enabled.get(minecraft.options); + + fullscreenModeButton.visible = enabled; + focusLossButton.visible = enabled && + ConfigScreenOption.fullscreenMode.getValue(minecraft.options) == FullscreenModeConfig.NATIVE; + } + + private int getHorizontalLayoutStart(int width) { + return (width - LAYOUT_MAX_WIDTH) / 2; + } + +} diff --git a/src/main/java/de/nekeras/borderless/client/gui/ConfigScreenOption.java b/src/main/java/de/nekeras/borderless/client/gui/ConfigScreenOption.java new file mode 100644 index 0000000..9716d4b --- /dev/null +++ b/src/main/java/de/nekeras/borderless/client/gui/ConfigScreenOption.java @@ -0,0 +1,47 @@ +package de.nekeras.borderless.client.gui; + +import de.nekeras.borderless.config.Config; +import de.nekeras.borderless.config.FocusLossConfig; +import de.nekeras.borderless.config.FullscreenModeConfig; +import de.nekeras.borderless.util.Translatable; +import net.minecraft.client.GameSettings; +import net.minecraft.client.settings.BooleanOption; +import net.minecraft.util.text.TranslationTextComponent; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; +import net.minecraftforge.common.ForgeConfigSpec; + +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.function.Predicate; + +@OnlyIn(Dist.CLIENT) +public class ConfigScreenOption { + + private static final String ENABLED_KEY = "borderless.config.enabled"; + private static final String ENABLED_TOOLTIP_KEY = "borderless.config.enabled.tooltip"; + private static final String FULLSCREEN_MODE_KEY = "borderless.config.fullscreen_mode"; + private static final String FOCUS_LOSS_KEY = "borderless.config.focus_loss"; + + public static final BooleanOption enabled = booleanOption(ENABLED_KEY, Config.GENERAL.enabled, ENABLED_TOOLTIP_KEY); + + public static final EnumOption fullscreenMode = + enumOption(FULLSCREEN_MODE_KEY, FullscreenModeConfig.values(), Config.GENERAL.fullscreenMode); + + public static final EnumOption focusLoss = + enumOption(FOCUS_LOSS_KEY, FocusLossConfig.values(), Config.GENERAL.focusLoss); + + private static BooleanOption booleanOption(String key, ForgeConfigSpec.BooleanValue config, String tooltip) { + Predicate getter = gameSettings -> config.get(); + BiConsumer setter = (gameSettings, value) -> config.set(value); + + return new BooleanOption(key, new TranslationTextComponent(tooltip), getter, setter); + } + + private static & Translatable> EnumOption enumOption(String key, E[] enumConstants, ForgeConfigSpec.EnumValue config) { + Function getter = gameSettings -> config.get(); + BiConsumer setter = (gameSettings, value) -> config.set(value); + + return new EnumOption<>(key, enumConstants, getter, setter); + } +} diff --git a/src/main/java/de/nekeras/borderless/client/gui/EnumOption.java b/src/main/java/de/nekeras/borderless/client/gui/EnumOption.java new file mode 100644 index 0000000..ddcb1b4 --- /dev/null +++ b/src/main/java/de/nekeras/borderless/client/gui/EnumOption.java @@ -0,0 +1,60 @@ +package de.nekeras.borderless.client.gui; + +import de.nekeras.borderless.util.Translatable; +import net.minecraft.client.AbstractOption; +import net.minecraft.client.GameSettings; +import net.minecraft.client.gui.widget.Widget; +import net.minecraft.client.gui.widget.button.Button; +import net.minecraft.client.gui.widget.button.OptionButton; +import net.minecraft.util.text.ITextComponent; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import javax.annotation.Nonnull; +import java.util.function.BiConsumer; +import java.util.function.Function; + +@OnlyIn(Dist.CLIENT) +public class EnumOption & Translatable> extends AbstractOption { + + private static final int BUTTON_HEIGHT = 20; + + private final E[] enumConstants; + private final Function getter; + private final BiConsumer setter; + + public EnumOption(@Nonnull String translationKey, @Nonnull E[] enumConstants, + @Nonnull Function getter, @Nonnull BiConsumer setter) { + super(translationKey); + this.enumConstants = enumConstants; + this.getter = getter; + this.setter = setter; + } + + @Nonnull + public E getValue(@Nonnull GameSettings settings) { + return getter.apply(settings); + } + + @Nonnull + public ITextComponent getMessage(@Nonnull GameSettings settings) { + return genericValueLabel(getValue(settings).getTranslation()); + } + + @Nonnull + @Override + public Widget createButton(@Nonnull GameSettings settings, int x, int y, int width) { + return new OptionButton(x, y, width, BUTTON_HEIGHT, this, getMessage(settings), btn -> { + onButtonClick(settings, btn); + }); + } + + private void onButtonClick(@Nonnull GameSettings settings, @Nonnull Button button) { + E value = getValue(settings); + int nextValueIndex = (value.ordinal() + 1) % enumConstants.length; + E nextValue = enumConstants[nextValueIndex]; + + setter.accept(settings, nextValue); + button.setMessage(getMessage(settings)); + } +} diff --git a/src/main/java/de/nekeras/borderless/client/listener/SizeChangedWindowEventListener.java b/src/main/java/de/nekeras/borderless/client/listener/SizeChangedWindowEventListener.java new file mode 100644 index 0000000..01decaa --- /dev/null +++ b/src/main/java/de/nekeras/borderless/client/listener/SizeChangedWindowEventListener.java @@ -0,0 +1,53 @@ +package de.nekeras.borderless.client.listener; + +import de.nekeras.borderless.client.ReflectionUtils; +import net.minecraft.client.renderer.IWindowEventListener; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * A custom {@link IWindowEventListener} that will call all original methods of the supplied + * default event listener. In addition, this method will run a callback supplied in the constructor whenever the + * window's size has changed (i.e. every time {@link #resizeDisplay()} is called by a non-GLFW callback). + */ +@OnlyIn(Dist.CLIENT) +public class SizeChangedWindowEventListener implements IWindowEventListener { + + private final IWindowEventListener defaultWindowEventListener; + private final Runnable onDisplayResize; + + public SizeChangedWindowEventListener(@Nullable IWindowEventListener defaultWindowEventListener, @Nonnull Runnable onDisplayResize) { + this.defaultWindowEventListener = defaultWindowEventListener; + this.onDisplayResize = onDisplayResize; + } + + @Override + public void setWindowActive(boolean focused) { + if (defaultWindowEventListener != null) { + defaultWindowEventListener.setWindowActive(focused); + } + } + + @Override + public void resizeDisplay() { + if (defaultWindowEventListener != null) { + defaultWindowEventListener.resizeDisplay(); + } + + if (ReflectionUtils.isCalledByGlfwCallback()) { + return; + } + + onDisplayResize.run(); + } + + @Override + public void cursorEntered() { + if (defaultWindowEventListener != null) { + defaultWindowEventListener.cursorEntered(); + } + } +} diff --git a/src/main/java/de/nekeras/borderless/client/listener/VideoSettingsListener.java b/src/main/java/de/nekeras/borderless/client/listener/VideoSettingsListener.java new file mode 100644 index 0000000..2502e68 --- /dev/null +++ b/src/main/java/de/nekeras/borderless/client/listener/VideoSettingsListener.java @@ -0,0 +1,56 @@ +package de.nekeras.borderless.client.listener; + +import de.nekeras.borderless.BorderlessWindow; +import de.nekeras.borderless.client.ReflectionUtils; +import de.nekeras.borderless.client.gui.ButtonOption; +import de.nekeras.borderless.client.gui.ConfigScreen; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screen.VideoSettingsScreen; +import net.minecraft.client.gui.widget.list.OptionsRowList; +import net.minecraft.util.text.ITextComponent; +import net.minecraft.util.text.TranslationTextComponent; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; +import net.minecraftforge.client.event.GuiScreenEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import javax.annotation.Nonnull; +import java.util.List; + +@OnlyIn(Dist.CLIENT) +@Mod.EventBusSubscriber(modid = BorderlessWindow.MOD_ID, value = Dist.CLIENT, bus = Mod.EventBusSubscriber.Bus.FORGE) +public class VideoSettingsListener { + + private static final String TITLE_KEY = "borderless.config.video_settings_button"; + private static final ITextComponent tooltip = new TranslationTextComponent("borderless.config.video_settings_button.tooltip"); + private static final Logger log = LogManager.getLogger(); + + @SubscribeEvent + public static void onVideoSettings(@Nonnull GuiScreenEvent.InitGuiEvent.Post event) { + if (event.getGui() instanceof VideoSettingsScreen) { + VideoSettingsScreen screen = (VideoSettingsScreen) event.getGui(); + log.info("Opened VideoSettingsScreen"); + + ReflectionUtils.getOptionsRowList(screen).ifPresent( + optionsRowList -> addToOptionsRowList(screen, optionsRowList)); + } + } + + private static void addToOptionsRowList(@Nonnull VideoSettingsScreen screen, @Nonnull OptionsRowList optionsRowList) { + log.info("Found OptionsRowList"); + Minecraft minecraft = Minecraft.getInstance(); + + ButtonOption fullscreenOption = new ButtonOption(TITLE_KEY, + btn -> minecraft.setScreen(new ConfigScreen(screen))); + fullscreenOption.setTooltip(minecraft.font.split(tooltip, 200)); + + optionsRowList.addBig(fullscreenOption); + List widgets = optionsRowList.children(); + OptionsRowList.Row lastElement = widgets.remove(widgets.size() - 1); + widgets.add(0, lastElement); + log.info("Added Borderless Window Config Screen to OptionsRowList"); + } +} diff --git a/src/main/java/de/nekeras/borderless/config/Config.java b/src/main/java/de/nekeras/borderless/config/Config.java index 4c47206..01883b7 100644 --- a/src/main/java/de/nekeras/borderless/config/Config.java +++ b/src/main/java/de/nekeras/borderless/config/Config.java @@ -1,35 +1,91 @@ package de.nekeras.borderless.config; -import java.util.Arrays; +import de.nekeras.borderless.client.DesktopEnvironment; +import de.nekeras.borderless.client.fullscreen.FullscreenDisplayMode; import net.minecraftforge.common.ForgeConfigSpec; +import javax.annotation.Nonnull; +import java.util.Arrays; + public class Config { - private static final ForgeConfigSpec.Builder BUILDER = new ForgeConfigSpec.Builder(); + private static final String GENERAL_PATH = "general"; + private static final String ENABLED_PATH = "enabled"; + private static final String FULLSCREEN_MODE_PATH = "fullscreenMode"; + private static final String FOCUS_LOSS_PATH = "focusLoss"; + private static final ForgeConfigSpec.Builder builder = new ForgeConfigSpec.Builder(); - public static final General GENERAL = new General(BUILDER); + /** + * The final configuration specification. + */ + public static final General GENERAL = new General(builder); + public static final ForgeConfigSpec CONFIG_SPEC = builder.build(); - public static final ForgeConfigSpec CONFIG_SPEC = BUILDER.build(); + /** + * The display mode as it is currently configured. + * + * @return The fullscreen display mode + */ + @Nonnull + public static FullscreenDisplayMode getFullscreenDisplayMode() { + if (GENERAL.enabled.get()) { + switch (GENERAL.fullscreenMode.get()) { + case BEST: + return DesktopEnvironment.get().getBestFullscreenDisplayMode(); + case BORDERLESS: + return FullscreenDisplayMode.BORDERLESS; + case NATIVE: + switch (GENERAL.focusLoss.get()) { + case MINIMIZE: + return FullscreenDisplayMode.NATIVE; + case DO_NOTHING: + return FullscreenDisplayMode.NATIVE_NON_ICONIFY; + case SWITCH_TO_WINDOWED: + return FullscreenDisplayMode.NATIVE_SWITCH_TO_WINDOWED; + } + } + } + + return FullscreenDisplayMode.NATIVE; + } public static class General { + /** + * Whether the mod should be enabled. + */ + public final ForgeConfigSpec.BooleanValue enabled; + + /** + * The fullscreen mode to use, for more information see {@link FullscreenModeConfig}. + * + * @see FullscreenModeConfig + */ public final ForgeConfigSpec.EnumValue fullscreenMode; + + /** + * The focus loss behaviour to use, for more information see {@link FocusLossConfig}. + * + * @see FocusLossConfig + */ public final ForgeConfigSpec.EnumValue focusLoss; public General(ForgeConfigSpec.Builder builder) { - builder.push("general"); + builder.push(GENERAL_PATH); + + enabled = builder.define(ENABLED_PATH, true); fullscreenMode = builder - .comment(Arrays.stream(FullscreenModeConfig.values()) - .map(mode -> String.format("%s - %s", mode.name(), mode.getComment())) - .toArray(String[]::new)) - .defineEnum("fullscreenMode", FullscreenModeConfig.BEST); + .comment(Arrays.stream(FullscreenModeConfig.values()) + .map(mode -> String.format("%s - %s", mode.name(), mode.getComment())) + .toArray(String[]::new)) + .defineEnum(FULLSCREEN_MODE_PATH, FullscreenModeConfig.BEST); focusLoss = builder - .comment(Arrays.stream(FocusLossConfig.values()) - .map(mode -> String.format("%s - %s", mode.name(), mode.getComment())) - .toArray(String[]::new)) - .defineEnum("focusLoss", FocusLossConfig.MINIMIZE); + .comment(Arrays.stream(FocusLossConfig.values()) + .map(mode -> String.format("%s - %s", mode.name(), mode.getComment())) + .toArray(String[]::new)) + .defineEnum(FOCUS_LOSS_PATH, FocusLossConfig.MINIMIZE); builder.pop(); } diff --git a/src/main/java/de/nekeras/borderless/config/FocusLossConfig.java b/src/main/java/de/nekeras/borderless/config/FocusLossConfig.java index 0535a59..b1a25ae 100644 --- a/src/main/java/de/nekeras/borderless/config/FocusLossConfig.java +++ b/src/main/java/de/nekeras/borderless/config/FocusLossConfig.java @@ -1,46 +1,48 @@ package de.nekeras.borderless.config; -import de.nekeras.borderless.fullscreen.FullscreenMode; +import de.nekeras.borderless.client.fullscreen.FullscreenDisplayMode; +import de.nekeras.borderless.util.Translatable; +import net.minecraft.util.text.TranslationTextComponent; /** * Settings for a focus loss behaviour that is applied on a fullscreen window. These setting will - * only be applied to a {@link FullscreenMode#NATIVE native fullscreen window}. + * only be applied to a {@link FullscreenDisplayMode#NATIVE native fullscreen window}. */ -public enum FocusLossConfig { +public enum FocusLossConfig implements Translatable { /** * Doesn't do anything when focus on a fullscreen window is lost, the window may be always on * top, depending on the operating system. */ DO_NOTHING("Doesn't do anything when focus on a fullscreen window is lost, the window may be " - + "always on top, depending on the operating system."), + + "always on top, depending on the operating system."), /** * Minimizes (iconify) the window when focus on a fullscreen window is lost, this is the default * Minecraft behaviour. */ MINIMIZE( - "Minimizes (iconify) the window when focus on a fullscreen window is lost, this is the " - + "default Minecraft behaviour."), + "Minimizes (iconify) the window when focus on a fullscreen window is lost, this is the " + + "default Minecraft behaviour."), /** * Switches to a windowed mode and leaves the fullscreen when focus on a fullscreen window is * lost. */ SWITCH_TO_WINDOWED( - "Switches to a windowed mode and leaves the fullscreen when focus on a fullscreen window " - + "is lost."), + "Switches to a windowed mode and leaves the fullscreen when focus on a fullscreen window " + + "is lost."), ; - private static final String BASE_TRANSLATION_KEY = "borderless.config.focus_loss.%s"; + private static final String BASE_KEY = "borderless.config.focus_loss.%s"; private final String comment; - private final String translationKey; + private final TranslationTextComponent translation; FocusLossConfig(String comment) { this.comment = comment; - this.translationKey = String.format(BASE_TRANSLATION_KEY, name().toLowerCase()); + this.translation = new TranslationTextComponent(String.format(BASE_KEY, name().toLowerCase())); } /** @@ -52,9 +54,14 @@ public String getComment() { return comment; } + @Override + public TranslationTextComponent getTranslation() { + return translation; + } + @Override public String toString() { - return translationKey; + return translation.getKey(); } } diff --git a/src/main/java/de/nekeras/borderless/config/FullscreenModeConfig.java b/src/main/java/de/nekeras/borderless/config/FullscreenModeConfig.java index 1b0ec5b..1580522 100644 --- a/src/main/java/de/nekeras/borderless/config/FullscreenModeConfig.java +++ b/src/main/java/de/nekeras/borderless/config/FullscreenModeConfig.java @@ -1,11 +1,14 @@ package de.nekeras.borderless.config; +import de.nekeras.borderless.util.Translatable; +import net.minecraft.util.text.TranslationTextComponent; + import javax.annotation.Nonnull; /** * An enum storing all supported fullscreen modes that can be configured in the {@link Config}. */ -public enum FullscreenModeConfig { +public enum FullscreenModeConfig implements Translatable { /** * The best suitable fullscreen mode for the current operating system. @@ -18,26 +21,26 @@ public enum FullscreenModeConfig { * window borders. */ BORDERLESS("A borderless fullscreen which sets the width and height of the window to the " - + "monitor's video mode and removing window borders."), + + "monitor's video mode and removing window borders."), /** * A native fullscreen which changes the monitor's window mode in order to apply the fullscreen. * Focus loss behaviour can be manually configured using {@link FocusLossConfig}. */ NATIVE("A native fullscreen which changes the monitor's window mode in order to apply the " - + "fullscreen. Focus loss behaviour can be manually configured using the 'focusLoss' " - + "option."), + + "fullscreen. Focus loss behaviour can be manually configured using the 'focusLoss' " + + "option."), ; private static final String BASE_KEY = "borderless.config.fullscreen_mode.%s"; private final String comment; - private final String titleKey; + private final TranslationTextComponent translation; FullscreenModeConfig(@Nonnull String comment) { this.comment = comment; - this.titleKey = String.format(BASE_KEY, name().toLowerCase()); + this.translation = new TranslationTextComponent(String.format(BASE_KEY, name().toLowerCase())); } /** @@ -49,9 +52,14 @@ public String getComment() { return comment; } + @Override + public TranslationTextComponent getTranslation() { + return translation; + } + @Override public String toString() { - return titleKey; + return translation.getKey(); } } diff --git a/src/main/java/de/nekeras/borderless/config/gui/ConfigScreen.java b/src/main/java/de/nekeras/borderless/config/gui/ConfigScreen.java deleted file mode 100644 index 8795f8c..0000000 --- a/src/main/java/de/nekeras/borderless/config/gui/ConfigScreen.java +++ /dev/null @@ -1,116 +0,0 @@ -package de.nekeras.borderless.config.gui; - -import com.mojang.blaze3d.matrix.MatrixStack; -import de.nekeras.borderless.config.FocusLossConfig; -import de.nekeras.borderless.config.FullscreenModeConfig; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.screen.Screen; -import net.minecraft.client.gui.widget.Widget; -import net.minecraft.client.gui.widget.button.Button; -import net.minecraft.util.text.TranslationTextComponent; -import net.minecraftforge.api.distmarker.Dist; -import net.minecraftforge.api.distmarker.OnlyIn; - -import javax.annotation.Nonnull; - -@OnlyIn(Dist.CLIENT) -public class ConfigScreen extends Screen { - - private static final String TITLE_KEY = "borderless.config.title"; - private static final String CHANGED_WARNING_KEY = "borderless.config.changed"; - private static final String DESCRIPTION_KEY_BASE = "borderless.%s"; - - private static final int LAYOUT_MAX_WIDTH = 250; - private static final int WHITE = 0xffffff; - private static final int YELLOW = 0xffff00; - private static final int LINE_HEIGHT = 25; - - private final Screen parent; - - private Widget focusLossButton; - - public ConfigScreen(@Nonnull Screen parent) { - super(new TranslationTextComponent(TITLE_KEY)); - this.parent = parent; - } - - @Override - protected void init() { - super.init(); - - Minecraft minecraft = Minecraft.getInstance(); - int x = getHorizontalLayoutStart(width); - - addWidget( - ConfigScreenOption.FULLSCREEN_MODE.createButton(minecraft.options, x, LINE_HEIGHT * 2, LAYOUT_MAX_WIDTH)); - - focusLossButton = addWidget( - ConfigScreenOption.FOCUS_LOSS.createButton(minecraft.options, x, LINE_HEIGHT * 3, LAYOUT_MAX_WIDTH)); - - addWidget(new Button((width - 100) / 2, height - 75, 100, 20, - new TranslationTextComponent("gui.done"), b -> onClose())); - } - - @Override - public void tick() { - super.tick(); - - focusLossButton.visible = - ConfigScreenOption.FULLSCREEN_MODE.getValue() == FullscreenModeConfig.NATIVE; - } - - @Override - public void render(@Nonnull MatrixStack matrixStack, int mouseX, int mouseY, float frameTime) { - Minecraft minecraft = Minecraft.getInstance(); - - this.renderBackground(matrixStack); - - renderTitle(matrixStack, minecraft, width); - renderDescription(minecraft, width); - renderChangedWarning(minecraft, width, height); - - super.render(matrixStack, mouseX, mouseY, frameTime); - } - - @Override - public void onClose() { - super.onClose(); - Minecraft.getInstance().setScreen(parent); - } - - private void renderTitle(@Nonnull MatrixStack matrixStack, @Nonnull Minecraft minecraft, int width) { - drawCenteredString(matrixStack, minecraft.font, title, width / 2, 20, 0xffffff); - } - - private void renderDescription(@Nonnull Minecraft minecraft, int width) { - int x = getHorizontalLayoutStart(width); - int y = LINE_HEIGHT * 4; - - minecraft.font.drawWordWrap(new TranslationTextComponent(getDescriptionKey()), x, y, LAYOUT_MAX_WIDTH, WHITE); - } - - private void renderChangedWarning(@Nonnull Minecraft minecraft, int width, int height) { - int x = getHorizontalLayoutStart(width); - int y = height - 50; - - minecraft.font.drawWordWrap(new TranslationTextComponent(CHANGED_WARNING_KEY), x, y, LAYOUT_MAX_WIDTH, YELLOW); - } - - private String getDescriptionKey() { - FullscreenModeConfig mode = ConfigScreenOption.FULLSCREEN_MODE.getValue(); - String modeKey = String.format(DESCRIPTION_KEY_BASE, mode.name().toLowerCase()); - - if (mode != FullscreenModeConfig.NATIVE) { - return modeKey; - } else { - FocusLossConfig focusLoss = ConfigScreenOption.FOCUS_LOSS.getValue(); - - return String.format("%s.%s", modeKey, focusLoss.name().toLowerCase()); - } - } - - private int getHorizontalLayoutStart(int width) { - return (width - LAYOUT_MAX_WIDTH) / 2; - } - -} diff --git a/src/main/java/de/nekeras/borderless/config/gui/ConfigScreenOption.java b/src/main/java/de/nekeras/borderless/config/gui/ConfigScreenOption.java deleted file mode 100644 index d23bc1c..0000000 --- a/src/main/java/de/nekeras/borderless/config/gui/ConfigScreenOption.java +++ /dev/null @@ -1,26 +0,0 @@ -package de.nekeras.borderless.config.gui; - -import de.nekeras.borderless.FullscreenModeHolder; -import de.nekeras.borderless.config.Config; -import de.nekeras.borderless.config.FocusLossConfig; -import de.nekeras.borderless.config.FullscreenModeConfig; -import de.nekeras.borderless.config.value.ConfigValueHolder; -import de.nekeras.borderless.fullscreen.FullscreenMode; - -public class ConfigScreenOption { - - public static final EnumOption FULLSCREEN_MODE = new EnumOption<>( - "borderless.config.fullscreen_mode", - FullscreenModeConfig.class, - new ConfigValueHolder<>(Config.GENERAL.fullscreenMode, - value -> FullscreenModeHolder.setFullscreenMode(FullscreenMode.fromConfig())) - ); - - public static final EnumOption FOCUS_LOSS = new EnumOption<>( - "borderless.config.focus_loss", - FocusLossConfig.class, - new ConfigValueHolder<>(Config.GENERAL.focusLoss, - value -> FullscreenModeHolder.setFullscreenMode(FullscreenMode.fromConfig())) - ); - -} diff --git a/src/main/java/de/nekeras/borderless/config/gui/EnumOption.java b/src/main/java/de/nekeras/borderless/config/gui/EnumOption.java deleted file mode 100644 index 0e460bf..0000000 --- a/src/main/java/de/nekeras/borderless/config/gui/EnumOption.java +++ /dev/null @@ -1,64 +0,0 @@ -package de.nekeras.borderless.config.gui; - -import de.nekeras.borderless.config.value.ValueHolder; -import net.minecraft.client.AbstractOption; -import net.minecraft.client.GameSettings; -import net.minecraft.client.gui.widget.Widget; -import net.minecraft.client.gui.widget.button.OptionButton; -import net.minecraft.util.text.ITextComponent; -import net.minecraft.util.text.TranslationTextComponent; - -import javax.annotation.Nonnull; - -public class EnumOption> extends AbstractOption { - - private static final int BUTTON_HEIGHT = 20; - - private final Class enumClass; - private final E[] values; - private final ValueHolder valueHolder; - - public EnumOption(@Nonnull String translationKey, @Nonnull Class enumClass, - @Nonnull ValueHolder holder) { - super(translationKey); - this.enumClass = enumClass; - this.values = enumClass.getEnumConstants(); - this.valueHolder = holder; - } - - public E getValue() { - return valueHolder.get(); - } - - public int getValueIndex() { - E value = getValue(); - - for (int i = 0; i < values.length; i++) { - if (values[i].equals(value)) { - return i; - } - } - - throw new IndexOutOfBoundsException(); - } - - public ITextComponent getText() { - return getText(valueHolder.get()); - } - - public ITextComponent getText(E value) { - return genericValueLabel(new TranslationTextComponent(value.toString())); - } - - @Nonnull - @Override - public Widget createButton(@Nonnull GameSettings options, int x, int y, int width) { - return new OptionButton(x, y, width, BUTTON_HEIGHT, this, getText(), (btn) -> { - int index = (getValueIndex() + 1) % enumClass.getEnumConstants().length; - E value = enumClass.getEnumConstants()[index]; - - valueHolder.set(value); - btn.setMessage(getText(value)); - }); - } -} diff --git a/src/main/java/de/nekeras/borderless/config/value/ConfigValueHolder.java b/src/main/java/de/nekeras/borderless/config/value/ConfigValueHolder.java deleted file mode 100644 index feede36..0000000 --- a/src/main/java/de/nekeras/borderless/config/value/ConfigValueHolder.java +++ /dev/null @@ -1,46 +0,0 @@ -package de.nekeras.borderless.config.value; - -import java.util.function.Consumer; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import net.minecraftforge.common.ForgeConfigSpec; - -/** - * A {@link ValueHolder} that will set and retrieve values from - * {@link ForgeConfigSpec.ConfigValue}. - * - * @param The type of value an instance holds - */ -public class ConfigValueHolder implements ValueHolder { - - private final ForgeConfigSpec.ConfigValue configValue; - private final Consumer onValueChange; - - /** - * @param configValue The configuration value store provided by Forge - * @param onValueChange A callback that is executed every time the provided value changes - */ - public ConfigValueHolder(@Nonnull ForgeConfigSpec.ConfigValue configValue, - @Nullable Consumer onValueChange) { - this.configValue = configValue; - this.onValueChange = onValueChange; - } - - @Override - public void set(T t) { - configValue.set(t); - configValue.save(); - - if (onValueChange != null) { - onValueChange.accept(t); - } - } - - @Override - public T get() { - return configValue.get(); - } - -} diff --git a/src/main/java/de/nekeras/borderless/config/value/ValueHolder.java b/src/main/java/de/nekeras/borderless/config/value/ValueHolder.java deleted file mode 100644 index 226c574..0000000 --- a/src/main/java/de/nekeras/borderless/config/value/ValueHolder.java +++ /dev/null @@ -1,24 +0,0 @@ -package de.nekeras.borderless.config.value; - -/** - * Abstraction of a single configuration value that can be set and retrieved using this interface. - * - * @param The type of value - */ -public interface ValueHolder { - - /** - * Sets the value. - * - * @param t The new value - */ - void set(T t); - - /** - * Gets the value. - * - * @return The current value - */ - T get(); - -} diff --git a/src/main/java/de/nekeras/borderless/fullscreen/BorderlessFullscreen.java b/src/main/java/de/nekeras/borderless/fullscreen/BorderlessFullscreen.java deleted file mode 100644 index cb61ef7..0000000 --- a/src/main/java/de/nekeras/borderless/fullscreen/BorderlessFullscreen.java +++ /dev/null @@ -1,40 +0,0 @@ -package de.nekeras.borderless.fullscreen; - -import net.minecraft.client.MainWindow; -import net.minecraft.client.Monitor; -import net.minecraft.client.renderer.VideoMode; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.lwjgl.glfw.GLFW; - -/** - * Switches into a windowed mode and removes the borders of the window. After that, maximizes the - * window on the current monitor as returned by {@link MainWindow#findBestMonitor()}. - */ -public class BorderlessFullscreen implements FullscreenMode { - - private static final Logger log = LogManager.getLogger(); - - @Override - public void apply(MainWindow window) { - Monitor monitor = window.findBestMonitor(); - - if (monitor == null) { - log.error("Window's monitor could not be retrieved"); - return; - } - - VideoMode videoMode = monitor.getCurrentMode(); - - GLFW.glfwSetWindowAttrib(window.getWindow(), GLFW.GLFW_DECORATED, GLFW.GLFW_FALSE); - GLFW.glfwSetWindowMonitor(window.getWindow(), 0, monitor.getX(), - monitor.getY(), videoMode.getWidth(), videoMode.getHeight(), - GLFW.GLFW_DONT_CARE); - } - - @Override - public void reset(MainWindow window) { - GLFW.glfwSetWindowAttrib(window.getWindow(), GLFW.GLFW_DECORATED, GLFW.GLFW_TRUE); - } - -} diff --git a/src/main/java/de/nekeras/borderless/fullscreen/FullscreenMode.java b/src/main/java/de/nekeras/borderless/fullscreen/FullscreenMode.java deleted file mode 100644 index ad70206..0000000 --- a/src/main/java/de/nekeras/borderless/fullscreen/FullscreenMode.java +++ /dev/null @@ -1,131 +0,0 @@ -package de.nekeras.borderless.fullscreen; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import de.nekeras.borderless.DesktopEnvironment; -import de.nekeras.borderless.config.Config; -import de.nekeras.borderless.config.FocusLossConfig; -import de.nekeras.borderless.config.FullscreenModeConfig; -import net.minecraft.client.MainWindow; -import net.minecraft.client.renderer.IWindowEventListener; - -/** - * A fullscreen mode that can be applied for the Minecraft {@link MainWindow}. The {@link - * #apply(MainWindow)} method - * will be called every time the window enters fullscreen, the {@link #reset(MainWindow)} method - * every time the window - * leaves fullscreen. - */ -public interface FullscreenMode { - - /** - * Switches into a windowed mode and removes the borders of the window. After that, maximizes - * the window on the current monitor as returned by {@link MainWindow#getMonitor()}. - */ - FullscreenMode BORDERLESS = new BorderlessFullscreen(); - - /** - * The native fullscreen mode, this fullscreen mode can be used to disable this mod, as this - * mode will not affect the {@link MainWindow}. - */ - FullscreenMode NATIVE = new NativeFullscreen(); - - /** - * The native fullscreen mode, but without automatic iconify on focus loss of the window. - */ - FullscreenMode NATIVE_NON_ICONIFY = new NativeNonIconifyFullscreen(); - - /** - * The same as {@link NativeFullscreen}, but switches to normal windowed mode on focus loss of - * the window. - */ - FullscreenMode NATIVE_WINDOWED = new NativeWindowedFullscreen(); - - /** - * Enters a window into this fullscreen mode. This method should only - * be called by the {@link IWindowEventListener} callback. The fullscreen of a - * minecraft window should only be changed with {@link MainWindow#toggleFullscreen()}. - * - * @param window - * The window to switch into this mode - */ - void apply(MainWindow window); - - /** - * Leaves this fullscreen mode. This method should only - * be called by the {@link IWindowEventListener} callback. The fullscreen of a - * minecraft window should only be changed with {@link MainWindow#toggleFullscreen()}. - * - * @param window - * The window to switch into a normal mode - */ - void reset(MainWindow window); - - /** - * Gets the default fullscreen mode for the specified operating system. - * - * @param environment - * The desktop environment of the operating system - * @return The fullscreen mode - */ - static FullscreenMode getDefault(@Nonnull DesktopEnvironment environment) { - switch (environment) { - case WINDOWS: - return BORDERLESS; - case X11: - return new NativeNonIconifyFullscreen(); - case GENERIC: - default: - return new NativeFullscreen(); - } - } - - /** - * Gets the current {@link FullscreenMode} as configured in the {@link Config} class. - * - * @return The fullscreen mode - */ - static FullscreenMode fromConfig() { - Config.General config = Config.GENERAL; - FullscreenModeConfig mode = config.fullscreenMode.get(); - FocusLossConfig focusLoss = config.focusLoss.get(); - - return fromConfig(mode, focusLoss); - } - - /** - * Gets the {@link FullscreenMode} from a custom configuration. - * - * @param mode - * The desired fullscreen mode - * @param focusLoss - * The behaviour on focus loss of the window, this is only applied if the - * mode is {@link FullscreenModeConfig#NATIVE}. - * @return The fullscreen mode - */ - static FullscreenMode fromConfig(@Nonnull FullscreenModeConfig mode, - @Nullable FocusLossConfig focusLoss) { - - switch (mode) { - case BEST: - return getDefault(DesktopEnvironment.get()); - case BORDERLESS: - return BORDERLESS; - case NATIVE: - if (focusLoss != null) { - switch (focusLoss) { - case DO_NOTHING: - return NATIVE_NON_ICONIFY; - case MINIMIZE: - return NATIVE; - case SWITCH_TO_WINDOWED: - return NATIVE_WINDOWED; - } - } - } - - return NATIVE; - } - -} diff --git a/src/main/java/de/nekeras/borderless/fullscreen/NativeFullscreen.java b/src/main/java/de/nekeras/borderless/fullscreen/NativeFullscreen.java deleted file mode 100644 index 8feb45b..0000000 --- a/src/main/java/de/nekeras/borderless/fullscreen/NativeFullscreen.java +++ /dev/null @@ -1,23 +0,0 @@ -package de.nekeras.borderless.fullscreen; - -import net.minecraft.client.MainWindow; -import org.lwjgl.glfw.GLFW; - -/** - * The native fullscreen mode, this fullscreen mode can be used to disable this mod, as this mode - * will not affect the {@link MainWindow}. - */ -public class NativeFullscreen implements FullscreenMode { - - @Override - public void apply(MainWindow window) { - // Ensure GLFW_AUTO_ICONIFY is enabled to be consistent with standard behaviour - GLFW.glfwSetWindowAttrib(window.getWindow(), GLFW.GLFW_AUTO_ICONIFY, GLFW.GLFW_TRUE); - } - - @Override - public void reset(MainWindow window) { - // Nothing to do for the native fullscreen - } - -} diff --git a/src/main/java/de/nekeras/borderless/fullscreen/NativeNonIconifyFullscreen.java b/src/main/java/de/nekeras/borderless/fullscreen/NativeNonIconifyFullscreen.java deleted file mode 100644 index 8ca8637..0000000 --- a/src/main/java/de/nekeras/borderless/fullscreen/NativeNonIconifyFullscreen.java +++ /dev/null @@ -1,21 +0,0 @@ -package de.nekeras.borderless.fullscreen; - -import net.minecraft.client.MainWindow; -import org.lwjgl.glfw.GLFW; - -/** - * The native fullscreen mode, but without automatic iconify on focus loss of the window. - */ -public class NativeNonIconifyFullscreen implements FullscreenMode { - - @Override - public void apply(MainWindow window) { - GLFW.glfwSetWindowAttrib(window.getWindow(), GLFW.GLFW_AUTO_ICONIFY, GLFW.GLFW_FALSE); - } - - @Override - public void reset(MainWindow window) { - GLFW.glfwSetWindowAttrib(window.getWindow(), GLFW.GLFW_AUTO_ICONIFY, GLFW.GLFW_TRUE); - } - -} diff --git a/src/main/java/de/nekeras/borderless/fullscreen/NativeWindowedFullscreen.java b/src/main/java/de/nekeras/borderless/fullscreen/NativeWindowedFullscreen.java deleted file mode 100644 index b2e5914..0000000 --- a/src/main/java/de/nekeras/borderless/fullscreen/NativeWindowedFullscreen.java +++ /dev/null @@ -1,56 +0,0 @@ -package de.nekeras.borderless.fullscreen; - -import net.minecraft.client.MainWindow; -import net.minecraft.client.Monitor; -import net.minecraft.client.renderer.VideoMode; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.lwjgl.glfw.GLFW; - -/** - * The same as {@link NativeFullscreen}, but switches to normal windowed mode on focus loss of - * the window. - */ -public class NativeWindowedFullscreen implements FullscreenMode { - - private static final Logger log = LogManager.getLogger(); - - @Override - public void apply(MainWindow window) { - GLFW.glfwSetWindowAttrib(window.getWindow(), GLFW.GLFW_AUTO_ICONIFY, GLFW.GLFW_FALSE); - GLFW.glfwSetWindowFocusCallback(window.getWindow(), (win, focused) -> { - if (focused) { - onFocusGained(window); - } else { - onFocusLost(window); - } - }); - } - - @Override - public void reset(MainWindow window) { - GLFW.glfwSetWindowAttrib(window.getWindow(), GLFW.GLFW_AUTO_ICONIFY, GLFW.GLFW_TRUE); - GLFW.glfwSetWindowFocusCallback(window.getWindow(), null); - } - - private void onFocusGained(MainWindow window) { - Monitor monitor = window.findBestMonitor(); - - if (monitor == null) { - log.error("Could not find monitor for window"); - return; - } - - VideoMode videoMode = window.getPreferredFullscreenVideoMode().orElseGet(monitor::getCurrentMode); - - GLFW.glfwSetWindowMonitor(window.getWindow(), monitor.getMonitor(), 0, 0, - videoMode.getWidth(), videoMode.getHeight(), videoMode.getRefreshRate()); - } - - private void onFocusLost(MainWindow window) { - GLFW.glfwSetWindowMonitor(window.getWindow(), 0, - window.getX(), window.getY(), - window.getWidth(), window.getHeight(), GLFW.GLFW_DONT_CARE); - } - -} diff --git a/src/main/java/de/nekeras/borderless/util/AccessibleFieldDelegate.java b/src/main/java/de/nekeras/borderless/util/AccessibleFieldDelegate.java new file mode 100644 index 0000000..5132a6a --- /dev/null +++ b/src/main/java/de/nekeras/borderless/util/AccessibleFieldDelegate.java @@ -0,0 +1,114 @@ +package de.nekeras.borderless.util; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.Optional; +import java.util.function.Function; + +/** + * Makes a field of type F in object thisRef accessible through reflection. + * It's value may be accessed using {@link #getValue(Object)} and {@link #setValue(Object, Object)}. + * + * @param The type of the field to find in the object. + * @see #AccessibleFieldDelegate(Class, Class) + * @see #AccessibleFieldDelegate(Class, Class, Function) + */ +public class AccessibleFieldDelegate { + + private static final Logger log = LogManager.getLogger(); + + private final Field field; + private Function defaultSupplier; + + public AccessibleFieldDelegate(@Nonnull Class inClass, @Nonnull Class fieldClass) + throws NoSuchFieldException { + + Optional field = findFieldOfType(inClass, fieldClass); + + if (field.isPresent()) { + this.field = field.get(); + } else { + throw new NoSuchFieldException(String.format("Failed to find field of type %s in type %s", + fieldClass.getName(), inClass)); + } + } + + public AccessibleFieldDelegate(@Nonnull Class inClass, @Nonnull Class fieldClass, @Nonnull Function defaultSupplier) { + Optional field = findFieldOfType(inClass, fieldClass); + + if (field.isPresent()) { + this.field = field.get(); + } else { + this.field = null; + this.defaultSupplier = defaultSupplier; + } + } + + /** + * Retrieves the field's value. + * + * @return The field value + * @throws IllegalStateException If there was an error while accessing the field via reflection + */ + @Nullable + @SuppressWarnings("unchecked") + public F getValue(@Nonnull C thisRef) { + if (field == null) { + return defaultSupplier.apply(thisRef); + } else { + try { + return (F) field.get(thisRef); + } catch (IllegalAccessException e) { + throw new IllegalStateException(String.format("Failed to access field %s in %s", + field.getType().getName(), thisRef.getClass().getName()), e); + } + } + } + + /** + * Updates the field's value. + * + * @param value The field value + * @throws IllegalStateException If there was an error while accessing the field via reflection + */ + public void setValue(@Nonnull C thisRef, @Nullable F value) { + if (field != null) { + try { + field.set(thisRef, value); + } catch (IllegalAccessException e) { + throw new IllegalStateException(String.format("Failed to access field %s in %s", + field.getType().getName(), thisRef.getClass().getName()), e); + } + } + } + + /** + * Asserts that a field of type F is present in type C and returns the first matching + * field. + * + * @param inClass The class to check for the field + * @param fieldType The type of the field + * @param The class type + * @param The field type + * @return The field if it was found, otherwise an empty optional + */ + @Nonnull + private static Optional findFieldOfType(@Nonnull Class inClass, @Nonnull Class fieldType) { + Optional field = Arrays.stream(inClass.getDeclaredFields()) + .filter(f -> f.getType() == fieldType) + .findFirst(); + + if (field.isPresent()) { + Field resultField = field.get(); + resultField.setAccessible(true); + return Optional.of(resultField); + } else { + return Optional.empty(); + } + } +} diff --git a/src/main/java/de/nekeras/borderless/util/Translatable.java b/src/main/java/de/nekeras/borderless/util/Translatable.java new file mode 100644 index 0000000..aaf3583 --- /dev/null +++ b/src/main/java/de/nekeras/borderless/util/Translatable.java @@ -0,0 +1,16 @@ +package de.nekeras.borderless.util; + +import net.minecraft.util.text.ITextComponent; + +/** + * Indicates a type which may be translated by a {@link ITextComponent}. + */ +public interface Translatable { + + /** + * The localization message for this instance. + * + * @return The message as an {@link ITextComponent}. + */ + ITextComponent getTranslation(); +} diff --git a/src/main/resources/META-INF/mods.toml b/src/main/resources/META-INF/mods.toml index 25f5af0..27513b7 100644 --- a/src/main/resources/META-INF/mods.toml +++ b/src/main/resources/META-INF/mods.toml @@ -4,7 +4,7 @@ license = "MIT License" issueTrackerURL = "https://github.com/Nekeras/borderless/issues" [[mods]] -modId = "borderless" +modId = "borderlesswindow" version = "${version}" displayName = "Borderless Window" displayURL = "https://www.curseforge.com/minecraft/mc-mods/borderless" diff --git a/src/main/resources/assets/borderless/lang/en_us.json b/src/main/resources/assets/borderless/lang/en_us.json index 5dfc737..94aed4c 100644 --- a/src/main/resources/assets/borderless/lang/en_us.json +++ b/src/main/resources/assets/borderless/lang/en_us.json @@ -1,6 +1,12 @@ { + "borderless.config.video_settings_button": "Borderless Window...", + "borderless.config.video_settings_button.tooltip": "Configuration relating Borderless Window to provide advanced fullscreen options", "borderless.config.title": "Borderless Window Config", - "borderless.config.fullscreen_mode": "Fullscreen Mode", + "borderless.config.apply": "Apply", + "borderless.config.enabled": "Enabled", + "borderless.config.enabled.tooltip": "Enable/disable Borderless Window", + "borderless.config.disabled.description": "Borderless Window is disabled, standard Minecraft fullscreen is used.", + "borderless.config.fullscreen_mode": "Fullscreen Display Mode", "borderless.config.fullscreen_mode.best": "Best", "borderless.config.fullscreen_mode.borderless": "Borderless", "borderless.config.fullscreen_mode.native": "Native",