From b5769de1752592dd86be28b950d5f8550f3d8e19 Mon Sep 17 00:00:00 2001 From: Paint_Ninja Date: Mon, 30 Mar 2026 20:36:45 +0100 Subject: [PATCH 1/6] Add Windows 11 Mica backdrop effect --- build.gradle | 18 ++ installer-java22/build.gradle | 13 + .../installer/Win11MicaEffect.java | 246 ++++++++++++++++++ settings.gradle | 2 + .../installer/InstallerPanel.java | 4 +- .../installer/SimpleInstaller.java | 3 +- .../minecraftforge/installer/SwingUtil.java | 19 ++ .../minecraftforge/installer/Win11Dialog.java | 192 ++++++++++++++ .../installer/Win11MicaEffect.java | 23 ++ 9 files changed, 518 insertions(+), 2 deletions(-) create mode 100644 installer-java22/build.gradle create mode 100644 installer-java22/src/main/java/net/minecraftforge/installer/Win11MicaEffect.java create mode 100644 src/main/java/net/minecraftforge/installer/Win11Dialog.java create mode 100644 src/main/java/net/minecraftforge/installer/Win11MicaEffect.java diff --git a/build.gradle b/build.gradle index f5c5e0e..57fa8ea 100644 --- a/build.gradle +++ b/build.gradle @@ -12,6 +12,7 @@ plugins { id 'net.minecraftforge.licenser' version '1.2.0' id 'net.minecraftforge.gradleutils' version '3.4.4' id 'net.minecraftforge.changelog' version '3.2.1' + id 'net.minecraftforge.multi-release' version '0.1.4' } gradleutils.displayName = 'Installer' @@ -67,9 +68,26 @@ tasks.named('jar', Jar) { 'Implementation-Version': project.version ] as LinkedHashMap, 'net/minecraftforge/installer/') } + + archiveClassifier = 'java8' +} + +multiRelease.register { + add(JavaLanguageVersion.of(22), project(':installer-java22')) +} + +tasks.named('multiReleaseJar', Jar) { + archiveClassifier = '' } tasks.named('shadowJar', ShadowJar) { + dependsOn tasks.named('multiReleaseJar') + + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + from({ + zipTree(tasks.named('multiReleaseJar').get().archiveFile.get().asFile) + }) + archiveClassifier = 'fatjar' minimize() } diff --git a/installer-java22/build.gradle b/installer-java22/build.gradle new file mode 100644 index 0000000..65407f1 --- /dev/null +++ b/installer-java22/build.gradle @@ -0,0 +1,13 @@ +plugins { + id 'java' + id 'eclipse' + id 'net.minecraftforge.licenser' version '1.2.0' + id 'net.minecraftforge.gradleutils' version '3.4.4' +} + +java.toolchain.languageVersion = JavaLanguageVersion.of(22) + +license { + header = rootProject.file('LICENSE-header.txt') + newLine = false +} diff --git a/installer-java22/src/main/java/net/minecraftforge/installer/Win11MicaEffect.java b/installer-java22/src/main/java/net/minecraftforge/installer/Win11MicaEffect.java new file mode 100644 index 0000000..117c6f5 --- /dev/null +++ b/installer-java22/src/main/java/net/minecraftforge/installer/Win11MicaEffect.java @@ -0,0 +1,246 @@ +/* + * Copyright (c) Forge Development LLC + * SPDX-License-Identifier: LGPL-2.1-only + */ +package net.minecraftforge.installer; + +import java.awt.Color; +import java.awt.Component; +import java.awt.Container; +import java.awt.Dialog; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.lang.foreign.Arena; +import java.lang.foreign.FunctionDescriptor; +import java.lang.foreign.Linker; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.SymbolLookup; +import java.lang.foreign.ValueLayout; +import java.lang.invoke.MethodHandle; +import java.util.Locale; + +import javax.swing.JComponent; +import javax.swing.JDialog; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JRadioButton; + +final class Win11MicaEffect { + private static final Color TRANSPARENT = new Color(220, 220, 220, 0); + private static final Color WINDOW_BACKGROUND = new Color(220, 220, 220, 1); + + private Win11MicaEffect() {} + + /// Swing paints an opaque background by default for all components, which hides the Mica backdrop effect. + /// This method recursively undoes that so that DWM is responsible for painting the window background. + static void prepare(Component component) { + if (isUnsupportedPlatform()) + return; + + if (component instanceof JPanel || component instanceof JOptionPane || component instanceof JRadioButton) { + JComponent jComponent = (JComponent) component; + jComponent.setOpaque(false); + jComponent.setBackground(TRANSPARENT); + } + + if (component instanceof Container container) { + for (Component child : container.getComponents()) { + prepare(child); + } + } + } + + static void install(JDialog dialog) { + if (isUnsupportedPlatform()) + return; + + dialog.getRootPane().setOpaque(false); + dialog.getLayeredPane().setOpaque(false); + if (dialog.getContentPane() instanceof JComponent contentPane) { + contentPane.setOpaque(false); + contentPane.setBackground(TRANSPARENT); + } + prepare(dialog.getRootPane()); + + dialog.setBackground(WINDOW_BACKGROUND); + + // In order for the Mica effect to apply correctly on a Swing window, we need to ensure a repaint after applying + // for timing reasons. If the window is already showing, we can repaint immediately, otherwise wait for the + // window to be opened first. + Runnable apply = () -> { + try { + applyTo(dialog); + dialog.invalidate(); + dialog.validate(); + dialog.repaint(); + dialog.getRootPane().repaint(); + } catch (Throwable _) {} + }; + + if (dialog.isShowing()) { + apply.run(); + return; + } + + dialog.addWindowListener(new WindowAdapter() { + @Override + public void windowOpened(WindowEvent e) { + dialog.removeWindowListener(this); + apply.run(); + } + }); + } + + private static void applyTo(Dialog dialog) throws Throwable { + if (!dialog.isDisplayable()) + return; + + var linker = Linker.nativeLinker(); + try (Arena arena = Arena.ofConfined()) { + // Lookup the necessary Windows APIs + SymbolLookup user32 = SymbolLookup.libraryLookup("user32", arena); + SymbolLookup dwmapi = SymbolLookup.libraryLookup("dwmapi", arena); + + MemorySegment hwnd = findDialogWindow(dialog, arena, user32); + if (MemorySegment.NULL.equals(hwnd)) + return; + + MethodHandle setWindowPos = linker.downcallHandle( + user32.find("SetWindowPos").orElseThrow(), + FunctionDescriptor.of( + ValueLayout.JAVA_INT, + ValueLayout.ADDRESS, + ValueLayout.ADDRESS, + ValueLayout.JAVA_INT, + ValueLayout.JAVA_INT, + ValueLayout.JAVA_INT, + ValueLayout.JAVA_INT, + ValueLayout.JAVA_INT + ) + ); + MethodHandle dwmSetWindowAttribute = linker.downcallHandle( + dwmapi.find("DwmSetWindowAttribute").orElseThrow(), + FunctionDescriptor.of( + ValueLayout.JAVA_INT, + ValueLayout.ADDRESS, + ValueLayout.JAVA_INT, + ValueLayout.ADDRESS, + ValueLayout.JAVA_INT + ) + ); + MethodHandle dwmExtendFrameIntoClientArea = linker.downcallHandle( + dwmapi.find("DwmExtendFrameIntoClientArea").orElseThrow(), + FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS) + ); + + promoteToTaskbarWindow(hwnd, user32, setWindowPos); + + // Win11 defaults to a rounded corner preference for decorated windows, but for backwards-compatibility it + // defaults to sharp corners for undecorated windows. This tells Win11 that we want rounded corners for our + // undecorated dialog window + MemorySegment cornerPreference = arena.allocate(ValueLayout.JAVA_INT); + cornerPreference.set(ValueLayout.JAVA_INT, 0, 2); // DWMWCP_ROUND + int DWMWA_WINDOW_CORNER_PREFERENCE = 33; + int _ = (int) dwmSetWindowAttribute.invokeExact(hwnd, DWMWA_WINDOW_CORNER_PREFERENCE, cornerPreference, Integer.BYTES); + + // Tell DWM that we'd like to opt-into the main window backdrop type rather than the backwards-compatible + // default of no backdrop effect. This is the main bit that actually enables the Mica backdrop effect - the + // rest of the code is mostly workarounds for Swing limitations + MemorySegment backdropType = arena.allocate(ValueLayout.JAVA_INT); + backdropType.set(ValueLayout.JAVA_INT, 0, 2); // DWMSBT_MAINWINDOW + int DWMWA_SYSTEMBACKDROP_TYPE = 38; + int hresult = (int) dwmSetWindowAttribute.invokeExact(hwnd, DWMWA_SYSTEMBACKDROP_TYPE, backdropType, Integer.BYTES); + if (hresult != 0) + return; + + // Tell DWM to paint the entire window background with the Mica brush rather than the default of only the + // title bar. + MemorySegment margins = arena.allocate(4L * Integer.BYTES, Integer.BYTES); + margins.set(ValueLayout.JAVA_INT, 0L, -1); + margins.set(ValueLayout.JAVA_INT, Integer.BYTES, -1); + margins.set(ValueLayout.JAVA_INT, 2L * Integer.BYTES, -1); + margins.set(ValueLayout.JAVA_INT, 3L * Integer.BYTES, -1); + int _ = (int) dwmExtendFrameIntoClientArea.invokeExact(hwnd, margins); + + // Comply with the remarks in Windows' API docs to ensure that the style changes apply + refreshWindowFrame(hwnd, setWindowPos); + } + } + + /// Attempt to find the window handle (HWND) of the dialog window by querying for a window with its title first, + /// falling back to the foreground window if no window with this dialog's window title is found. + private static MemorySegment findDialogWindow(Dialog dialog, Arena arena, SymbolLookup user32) throws Throwable { + var linker = Linker.nativeLinker(); + MethodHandle findWindowW = linker.downcallHandle( + user32.find("FindWindowW").orElseThrow(), + FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS) + ); + + String title = dialog.getTitle(); + if (title != null && !title.isBlank()) { + MemorySegment windowTitle = arena.allocateFrom(ValueLayout.JAVA_CHAR, (title + '\0').toCharArray()); + MemorySegment hwnd = (MemorySegment) findWindowW.invokeExact(MemorySegment.NULL, windowTitle); + if (!MemorySegment.NULL.equals(hwnd)) + return hwnd; + } + + MethodHandle getForegroundWindow = linker.downcallHandle( + user32.find("GetForegroundWindow").orElseThrow(), + FunctionDescriptor.of(ValueLayout.ADDRESS) + ); + + return (MemorySegment) getForegroundWindow.invokeExact(); + } + + /// Ensures that the dialog box is visible in the taskbar by removing the tool window style and adding the app + /// window style. This is a workaround for Swing hiding the dialog box from the taskbar when undecorated and + /// non-opaque due to it expecting that the window is invisible, however in practice it is visible as DWM will draw + /// the window background for us which is what we want for the Mica effect to be visible. + /// @see [#prepare(Component) + private static void promoteToTaskbarWindow(MemorySegment hwnd, SymbolLookup user32, MethodHandle setWindowPos) throws Throwable { + var linker = Linker.nativeLinker(); + MethodHandle getWindowLongW = linker.downcallHandle( + user32.find("GetWindowLongW").orElseThrow(), + FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.JAVA_INT) + ); + MethodHandle setWindowLongW = linker.downcallHandle( + user32.find("SetWindowLongW").orElseThrow(), + FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.JAVA_INT, ValueLayout.JAVA_INT) + ); + + int WS_EX_TOOLWINDOW = 0x00000080; + int WS_EX_APPWINDOW = 0x00040000; + int GWL_EXSTYLE = -20; + int exStyle = (int) getWindowLongW.invokeExact(hwnd, GWL_EXSTYLE); + int newExStyle = (exStyle | WS_EX_APPWINDOW) & ~WS_EX_TOOLWINDOW; + if (newExStyle == exStyle) + return; + + int _ = (int) setWindowLongW.invokeExact(hwnd, GWL_EXSTYLE, newExStyle); + refreshWindowFrame(hwnd, setWindowPos); + } + + /// The [docs](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowpos#remarks) mention + /// that we should call SetWindowPos with the SWP_FRAMECHANGED flag after calling SetWindowLong to ensure that + /// style updates apply on Windows Vista onwards. This is done after the DWM calls just in case those count too. + /// @see [#promoteToTaskbarWindow(MemorySegment, SymbolLookup, MethodHandle) + private static void refreshWindowFrame(MemorySegment hwnd, MethodHandle setWindowPos) throws Throwable { + // SWP_NOSIZE | SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED; + int flags = 0x0001 | 0x0002 | 0x0004 | 0x0010 | 0x0020; + int _ = (int) setWindowPos.invokeExact(hwnd, MemorySegment.NULL, 0, 0, 0, 0, flags); + } + + /// @return true if the current platform is Windows 11 + static boolean isSupported() { + return !isUnsupportedPlatform(); + } + + private static boolean isUnsupportedPlatform() { + final class LazyInit { + private LazyInit() {} + private static final boolean IS_UNSUPPORTED + = !System.getProperty("os.name", "").toLowerCase(Locale.ENGLISH).startsWith("windows 11"); + } + return LazyInit.IS_UNSUPPORTED; + } +} diff --git a/settings.gradle b/settings.gradle index d291251..8d80827 100644 --- a/settings.gradle +++ b/settings.gradle @@ -28,3 +28,5 @@ dependencyResolutionManagement { } rootProject.name = 'Installer' + +include 'installer-java22' diff --git a/src/main/java/net/minecraftforge/installer/InstallerPanel.java b/src/main/java/net/minecraftforge/installer/InstallerPanel.java index 358d86b..c1171ff 100644 --- a/src/main/java/net/minecraftforge/installer/InstallerPanel.java +++ b/src/main/java/net/minecraftforge/installer/InstallerPanel.java @@ -255,7 +255,9 @@ private void updateFilePath() { public void run(ProgressCallback monitor) { JOptionPane optionPane = new JOptionPane(this, JOptionPane.PLAIN_MESSAGE, JOptionPane.OK_CANCEL_OPTION); - dialog = optionPane.createDialog("Forge Installer"); + dialog = Win11MicaEffect.isSupported() + ? Win11Dialog.create(optionPane, this, "Forge Installer") + : optionPane.createDialog("Forge Installer"); dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); dialog.setVisible(true); int result = (Integer) (optionPane.getValue() != null ? optionPane.getValue() : -1); diff --git a/src/main/java/net/minecraftforge/installer/SimpleInstaller.java b/src/main/java/net/minecraftforge/installer/SimpleInstaller.java index 7ad95e7..e6a38f1 100644 --- a/src/main/java/net/minecraftforge/installer/SimpleInstaller.java +++ b/src/main/java/net/minecraftforge/installer/SimpleInstaller.java @@ -99,7 +99,7 @@ public static void main(String[] args) throws IOException, URISyntaxException { "sessionserver.mojang.com", "authserver.mojang.com", }) { - monitor.message("Host: " + host + " [" + DownloadUtils.getIpString(host) + "]"); + monitor.message("Host: " + host + " [" + DownloadUtils.getIpString(host) + ']'); } for (String host : new String[] { @@ -178,6 +178,7 @@ private static void launchGui(ProgressCallback monitor, File installer, String b private static void launchGui(ProgressCallback monitor, File installer, String badCerts, OptionParser parser) { try { UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + if (Win11MicaEffect.isSupported()) SwingUtil.applyGlobalFont("Segoe UI"); } catch (HeadlessException headless) { // if ran in a headless CLI environment with no args, show some help text and exit gracefully if (parser == null) { diff --git a/src/main/java/net/minecraftforge/installer/SwingUtil.java b/src/main/java/net/minecraftforge/installer/SwingUtil.java index ba48910..d0ac0ec 100644 --- a/src/main/java/net/minecraftforge/installer/SwingUtil.java +++ b/src/main/java/net/minecraftforge/installer/SwingUtil.java @@ -5,9 +5,11 @@ package net.minecraftforge.installer; import javax.swing.*; +import javax.swing.plaf.FontUIResource; import java.awt.*; import java.io.File; import java.io.IOException; +import java.util.Enumeration; public class SwingUtil { public static JButton createLogButton() { @@ -28,4 +30,21 @@ public static JButton createLogButton() { }); return button; } + + /** + * Applies a new global font to all Swing UI components instantiated after this method call + * @param fontFamily The name of the desired font to apply + */ + static void applyGlobalFont(String fontFamily) { + UIDefaults defaults = UIManager.getDefaults(); + Enumeration keys = defaults.keys(); + while (keys.hasMoreElements()) { + Object key = keys.nextElement(); + Object value = defaults.get(key); + if (value instanceof FontUIResource) { + FontUIResource font = (FontUIResource) value; + defaults.put(key, new FontUIResource(fontFamily, font.getStyle(), font.getSize())); + } + } + } } diff --git a/src/main/java/net/minecraftforge/installer/Win11Dialog.java b/src/main/java/net/minecraftforge/installer/Win11Dialog.java new file mode 100644 index 0000000..3d34fa6 --- /dev/null +++ b/src/main/java/net/minecraftforge/installer/Win11Dialog.java @@ -0,0 +1,192 @@ +/* + * Copyright (c) Forge Development LLC + * SPDX-License-Identifier: LGPL-2.1-only + */ +package net.minecraftforge.installer; + +import java.awt.BasicStroke; +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Dialog; +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Insets; +import java.awt.Point; +import java.awt.RenderingHints; +import java.awt.Window; +import java.awt.geom.Line2D; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; + +import javax.swing.BorderFactory; +import javax.swing.ButtonModel; +import javax.swing.JButton; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.SwingConstants; + +final class Win11Dialog { + private static final Dimension CAPTION_BUTTON_SIZE = new Dimension(46, 30); + + // Note: These are here because Java 8 doesn't support declaring static fields inside anonymous classes + private static final Color WINDOW_HIT_TEST_COLOR = new Color(255, 255, 255, 1); + private static final Color TITLE_BAR_OVERLAY = new Color(255, 255, 255, 28); + private static final Color CLOSE_BUTTON_HOVER = new Color(196, 43, 28); + private static final Color CLOSE_BUTTON_PRESSED = new Color(143, 32, 20); + private static final Color CLOSE_BUTTON_GLYPH = new Color(32, 32, 32); + + private Win11Dialog() {} + + /** + * Swing does not support creating decorated windows that are non-opaque, the latter of which is required for the + * Mica backdrop to work. So let's make our own window decoration that looks close to the native title bar. + * @param optionPane The option pane to display inside the dialog + * @param title The window title to display on the dialog + * @return A new JDialog instance with custom window decorations and support for the Mica backdrop on Windows 11. + * @see Win11MicaEffect + */ + static JDialog create(JOptionPane optionPane, JPanel installerPanel, String title) { + JDialog installerDialog = new JDialog(null, title, Dialog.ModalityType.APPLICATION_MODAL); + installerDialog.setUndecorated(true); + installerDialog.setType(Window.Type.NORMAL); + installerDialog.setContentPane(createInstallerChrome(installerDialog, optionPane)); + installerDialog.addWindowListener(new WindowAdapter() { + @Override + public void windowClosing(WindowEvent e) { + optionPane.setValue(JOptionPane.CLOSED_OPTION); + } + }); + optionPane.addPropertyChangeListener(event -> { + String propertyName = event.getPropertyName(); + if (!installerDialog.isVisible() || event.getSource() != optionPane) { + return; + } + if (!JOptionPane.VALUE_PROPERTY.equals(propertyName) && !JOptionPane.INPUT_VALUE_PROPERTY.equals(propertyName)) { + return; + } + installerDialog.setVisible(false); + }); + installerDialog.pack(); + installerDialog.setLocationRelativeTo(null); + optionPane.selectInitialValue(); + Win11MicaEffect.prepare(optionPane); + Win11MicaEffect.prepare(installerPanel); + Win11MicaEffect.install(installerDialog); + return installerDialog; + } + + private static JPanel createInstallerChrome(JDialog installerDialog, JOptionPane optionPane) { + JPanel chrome = new JPanel(new BorderLayout()) { + @Override + protected void paintComponent(Graphics graphics) { + Graphics2D g2 = (Graphics2D) graphics.create(); + try { + g2.setColor(WINDOW_HIT_TEST_COLOR); + g2.fillRect(0, 0, getWidth(), getHeight()); + } finally { + g2.dispose(); + } + super.paintComponent(graphics); + } + }; + chrome.setOpaque(false); + chrome.setBorder(BorderFactory.createEmptyBorder(1, 1, 1, 1)); + chrome.add(createTitleBar(installerDialog, optionPane), BorderLayout.NORTH); + chrome.add(optionPane, BorderLayout.CENTER); + return chrome; + } + + private static JPanel createTitleBar(JDialog installerDialog, JOptionPane optionPane) { + JPanel titleBar = new JPanel(new BorderLayout()) { + @Override + protected void paintComponent(Graphics graphics) { + Graphics2D g2 = (Graphics2D) graphics.create(); + try { + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g2.setColor(TITLE_BAR_OVERLAY); + g2.fillRect(0, 0, getWidth(), getHeight()); + } finally { + g2.dispose(); + } + super.paintComponent(graphics); + } + }; + titleBar.setOpaque(false); + titleBar.setBorder(BorderFactory.createEmptyBorder(0, 12, 0, 0)); + + JLabel titleLabel = new JLabel(installerDialog.getTitle()); + titleLabel.setHorizontalAlignment(SwingConstants.LEFT); + titleLabel.setBorder(BorderFactory.createEmptyBorder(8, 0, 8, 0)); + titleBar.add(titleLabel, BorderLayout.WEST); + + JButton closeButton = new JButton() { + @Override + protected void paintComponent(Graphics graphics) { + Graphics2D g2 = (Graphics2D) graphics.create(); + try { + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_NORMALIZE); + + ButtonModel model = getModel(); + if (model.isPressed()) { + g2.setColor(CLOSE_BUTTON_PRESSED); + g2.fillRect(0, 0, getWidth(), getHeight()); + } else if (model.isRollover()) { + g2.setColor(CLOSE_BUTTON_HOVER); + g2.fillRect(0, 0, getWidth(), getHeight()); + } + + g2.setColor(model.isPressed() || model.isRollover() ? Color.WHITE : CLOSE_BUTTON_GLYPH); + g2.setStroke(new BasicStroke(1.2f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); + int cx = getWidth() / 2; + int cy = getHeight() / 2; + g2.draw(new Line2D.Float(cx - 4.5f, cy - 4.5f, cx + 4.5f, cy + 4.5f)); + g2.draw(new Line2D.Float(cx + 4.5f, cy - 4.5f, cx - 4.5f, cy + 4.5f)); + } finally { + g2.dispose(); + } + } + }; + closeButton.setFocusable(false); + closeButton.setOpaque(false); + closeButton.setContentAreaFilled(false); + closeButton.setBorderPainted(false); + closeButton.setRolloverEnabled(true); + closeButton.setMargin(new Insets(8, 12, 8, 12)); + closeButton.setPreferredSize(CAPTION_BUTTON_SIZE); + closeButton.setMinimumSize(CAPTION_BUTTON_SIZE); + closeButton.setMaximumSize(CAPTION_BUTTON_SIZE); + closeButton.setToolTipText("Close"); + closeButton.addActionListener(ignored -> optionPane.setValue(JOptionPane.CLOSED_OPTION)); + titleBar.add(closeButton, BorderLayout.EAST); + + Point[] dragOffset = {null}; + MouseAdapter dragListener = new MouseAdapter() { + @Override + public void mousePressed(MouseEvent e) { + dragOffset[0] = e.getPoint(); + } + + @Override + public void mouseDragged(MouseEvent e) { + if (dragOffset[0] == null) { + return; + } + Point screen = e.getLocationOnScreen(); + installerDialog.setLocation(screen.x - dragOffset[0].x, screen.y - dragOffset[0].y); + } + }; + titleBar.addMouseListener(dragListener); + titleBar.addMouseMotionListener(dragListener); + titleLabel.addMouseListener(dragListener); + titleLabel.addMouseMotionListener(dragListener); + + return titleBar; + } +} + diff --git a/src/main/java/net/minecraftforge/installer/Win11MicaEffect.java b/src/main/java/net/minecraftforge/installer/Win11MicaEffect.java new file mode 100644 index 0000000..25a1f20 --- /dev/null +++ b/src/main/java/net/minecraftforge/installer/Win11MicaEffect.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) Forge Development LLC + * SPDX-License-Identifier: LGPL-2.1-only + */ +package net.minecraftforge.installer; + +import javax.swing.JDialog; +import java.awt.Component; + +/** + * No-op implementation for Java 8. See the installer-java22 subproject for the implementation. + */ +final class Win11MicaEffect { + private Win11MicaEffect() {} + + static void prepare(Component component) {} + + static void install(JDialog dialog) {} + + static boolean isSupported() { + return false; + } +} From 2e0da69d16d59480f93ec8e1bb755830cbc42c23 Mon Sep 17 00:00:00 2001 From: Paint_Ninja Date: Mon, 30 Mar 2026 21:29:16 +0100 Subject: [PATCH 2/6] Fix native access warning --- build.gradle | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 57fa8ea..3f66652 100644 --- a/build.gradle +++ b/build.gradle @@ -58,7 +58,10 @@ Files.list(Paths.get(projectDir.absolutePath)) tasks.named('jar', Jar) { manifest { - attributes('Main-Class': 'net.minecraftforge.installer.SimpleInstaller') + attributes([ + 'Main-Class': 'net.minecraftforge.installer.SimpleInstaller', + 'Enable-Native-Access': 'ALL-UNNAMED' + ]) attributes([ 'Specification-Title': 'Installer', 'Specification-Vendor': 'Forge Development LLC', From d5e03871fad5cdde3effa58f2744b583e2a5b0a5 Mon Sep 17 00:00:00 2001 From: Paint_Ninja Date: Mon, 30 Mar 2026 21:42:49 +0100 Subject: [PATCH 3/6] Fix titlebar background blending --- src/main/java/net/minecraftforge/installer/Win11Dialog.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/net/minecraftforge/installer/Win11Dialog.java b/src/main/java/net/minecraftforge/installer/Win11Dialog.java index 3d34fa6..7ecb58f 100644 --- a/src/main/java/net/minecraftforge/installer/Win11Dialog.java +++ b/src/main/java/net/minecraftforge/installer/Win11Dialog.java @@ -35,7 +35,7 @@ final class Win11Dialog { // Note: These are here because Java 8 doesn't support declaring static fields inside anonymous classes private static final Color WINDOW_HIT_TEST_COLOR = new Color(255, 255, 255, 1); - private static final Color TITLE_BAR_OVERLAY = new Color(255, 255, 255, 28); + private static final Color TITLE_BAR_OVERLAY = new Color(220, 220, 220, 0); private static final Color CLOSE_BUTTON_HOVER = new Color(196, 43, 28); private static final Color CLOSE_BUTTON_PRESSED = new Color(143, 32, 20); private static final Color CLOSE_BUTTON_GLYPH = new Color(32, 32, 32); From 7bb477a4a54583f99f97c492b76371dd5380e298 Mon Sep 17 00:00:00 2001 From: Paint_Ninja Date: Mon, 30 Mar 2026 21:50:21 +0100 Subject: [PATCH 4/6] Add sysprop for changing backdrop type --- .../java/net/minecraftforge/installer/Win11MicaEffect.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/installer-java22/src/main/java/net/minecraftforge/installer/Win11MicaEffect.java b/installer-java22/src/main/java/net/minecraftforge/installer/Win11MicaEffect.java index 117c6f5..f8ad901 100644 --- a/installer-java22/src/main/java/net/minecraftforge/installer/Win11MicaEffect.java +++ b/installer-java22/src/main/java/net/minecraftforge/installer/Win11MicaEffect.java @@ -147,7 +147,9 @@ private static void applyTo(Dialog dialog) throws Throwable { // default of no backdrop effect. This is the main bit that actually enables the Mica backdrop effect - the // rest of the code is mostly workarounds for Swing limitations MemorySegment backdropType = arena.allocate(ValueLayout.JAVA_INT); - backdropType.set(ValueLayout.JAVA_INT, 0, 2); // DWMSBT_MAINWINDOW + // 2 = Mica (DWMSBT_MAINWINDOW), 3 = Acrylic (DWMSBT_TRANSIENTWINDOW), 4 = Mica Alt (DWMSBT_TABBEDWINDOW) + int backdropTypePreference = Integer.getInteger("forgeinstaller.backdroptype", 2); + backdropType.set(ValueLayout.JAVA_INT, 0, backdropTypePreference); int DWMWA_SYSTEMBACKDROP_TYPE = 38; int hresult = (int) dwmSetWindowAttribute.invokeExact(hwnd, DWMWA_SYSTEMBACKDROP_TYPE, backdropType, Integer.BYTES); if (hresult != 0) From b480edb985fd9e52f9763237e145f75d4ac35bb6 Mon Sep 17 00:00:00 2001 From: Paint_Ninja Date: Mon, 30 Mar 2026 23:32:55 +0100 Subject: [PATCH 5/6] Add sysprop to disable the new UI if people run into problems --- .../java/net/minecraftforge/installer/Win11MicaEffect.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/installer-java22/src/main/java/net/minecraftforge/installer/Win11MicaEffect.java b/installer-java22/src/main/java/net/minecraftforge/installer/Win11MicaEffect.java index f8ad901..aa0924e 100644 --- a/installer-java22/src/main/java/net/minecraftforge/installer/Win11MicaEffect.java +++ b/installer-java22/src/main/java/net/minecraftforge/installer/Win11MicaEffect.java @@ -241,7 +241,8 @@ private static boolean isUnsupportedPlatform() { final class LazyInit { private LazyInit() {} private static final boolean IS_UNSUPPORTED - = !System.getProperty("os.name", "").toLowerCase(Locale.ENGLISH).startsWith("windows 11"); + = !System.getProperty("os.name", "").toLowerCase(Locale.ENGLISH).startsWith("windows 11") + || !Boolean.getBoolean("forgeinstaller.usewin11mica"); } return LazyInit.IS_UNSUPPORTED; } From e82dece2ba4b1e52e433c575333b54bdc53eb220 Mon Sep 17 00:00:00 2001 From: Paint_Ninja Date: Tue, 31 Mar 2026 13:54:18 +0100 Subject: [PATCH 6/6] Fix sysprop, add more robust fallback --- .../installer/Win11MicaEffect.java | 50 ++++++++++++++----- .../installer/DownloadUtils.java | 4 +- .../installer/InstallerPanel.java | 12 +++-- .../minecraftforge/installer/Win11Dialog.java | 2 +- .../installer/Win11MicaEffect.java | 2 +- 5 files changed, 50 insertions(+), 20 deletions(-) diff --git a/installer-java22/src/main/java/net/minecraftforge/installer/Win11MicaEffect.java b/installer-java22/src/main/java/net/minecraftforge/installer/Win11MicaEffect.java index aa0924e..f99e593 100644 --- a/installer-java22/src/main/java/net/minecraftforge/installer/Win11MicaEffect.java +++ b/installer-java22/src/main/java/net/minecraftforge/installer/Win11MicaEffect.java @@ -18,6 +18,7 @@ import java.lang.foreign.ValueLayout; import java.lang.invoke.MethodHandle; import java.util.Locale; +import java.util.concurrent.Callable; import javax.swing.JComponent; import javax.swing.JDialog; @@ -50,7 +51,7 @@ static void prepare(Component component) { } } - static void install(JDialog dialog) { + static void install(JDialog dialog) throws Exception { if (isUnsupportedPlatform()) return; @@ -67,18 +68,22 @@ static void install(JDialog dialog) { // In order for the Mica effect to apply correctly on a Swing window, we need to ensure a repaint after applying // for timing reasons. If the window is already showing, we can repaint immediately, otherwise wait for the // window to be opened first. - Runnable apply = () -> { + Callable apply = () -> { try { applyTo(dialog); - dialog.invalidate(); - dialog.validate(); - dialog.repaint(); - dialog.getRootPane().repaint(); - } catch (Throwable _) {} + } catch (Throwable t) { + if (t instanceof Exception e) throw e; + else throw new RuntimeException(t); + } + dialog.invalidate(); + dialog.validate(); + dialog.repaint(); + dialog.getRootPane().repaint(); + return null; }; if (dialog.isShowing()) { - apply.run(); + apply.call(); return; } @@ -86,7 +91,11 @@ static void install(JDialog dialog) { @Override public void windowOpened(WindowEvent e) { dialog.removeWindowListener(this); - apply.run(); + try { + apply.call(); + } catch (Exception ex) { + throw new RuntimeException(ex); + } } }); } @@ -153,7 +162,7 @@ private static void applyTo(Dialog dialog) throws Throwable { int DWMWA_SYSTEMBACKDROP_TYPE = 38; int hresult = (int) dwmSetWindowAttribute.invokeExact(hwnd, DWMWA_SYSTEMBACKDROP_TYPE, backdropType, Integer.BYTES); if (hresult != 0) - return; + throw new RuntimeException("DwmSetWindowAttribute failed with HRESULT 0x" + Integer.toHexString(hresult)); // Tell DWM to paint the entire window background with the Mica brush rather than the default of only the // title bar. @@ -240,9 +249,24 @@ static boolean isSupported() { private static boolean isUnsupportedPlatform() { final class LazyInit { private LazyInit() {} - private static final boolean IS_UNSUPPORTED - = !System.getProperty("os.name", "").toLowerCase(Locale.ENGLISH).startsWith("windows 11") - || !Boolean.getBoolean("forgeinstaller.usewin11mica"); + private static final boolean IS_UNSUPPORTED; + static { + var useMica = true; + boolean forceDisableMica = false; + var isWin11 = System.getProperty("os.name", "").toLowerCase(Locale.ENGLISH).startsWith("windows 11"); + if (isWin11) { + // Disable Mica if explicitly requested + forceDisableMica = !Boolean.parseBoolean(System.getProperty("forgeinstaller.usewin11mica", "true")); + if (forceDisableMica) + useMica = false; + } else { + // Mica is only supported on Windows 11 + useMica = false; + } + +// System.out.println("OS: " + System.getProperty("os.name", "") + ", isWin11: " + isWin11 + ", forceDisableMica: " + forceDisableMica); + IS_UNSUPPORTED = !useMica; + } } return LazyInit.IS_UNSUPPORTED; } diff --git a/src/main/java/net/minecraftforge/installer/DownloadUtils.java b/src/main/java/net/minecraftforge/installer/DownloadUtils.java index bc336c2..1245a3a 100644 --- a/src/main/java/net/minecraftforge/installer/DownloadUtils.java +++ b/src/main/java/net/minecraftforge/installer/DownloadUtils.java @@ -48,7 +48,7 @@ public static boolean downloadLibrary(ProgressCallback monitor, Mirror mirror, L download.setPath(artifact.getPath()); } - monitor.message(String.format("Considering library %s", artifact.getDescriptor())); + monitor.message("Considering library " + artifact.getDescriptor()); if (target.exists()) { if (download.getSha1() != null) { @@ -279,7 +279,7 @@ public static List getIps(String host) { } public static String getIpString(String host) { - return getIps(host).stream().collect(Collectors.joining(", ")); + return String.join(", ", getIps(host)); } public static boolean checkCertificate(String host) { diff --git a/src/main/java/net/minecraftforge/installer/InstallerPanel.java b/src/main/java/net/minecraftforge/installer/InstallerPanel.java index c1171ff..b6340bc 100644 --- a/src/main/java/net/minecraftforge/installer/InstallerPanel.java +++ b/src/main/java/net/minecraftforge/installer/InstallerPanel.java @@ -255,9 +255,15 @@ private void updateFilePath() { public void run(ProgressCallback monitor) { JOptionPane optionPane = new JOptionPane(this, JOptionPane.PLAIN_MESSAGE, JOptionPane.OK_CANCEL_OPTION); - dialog = Win11MicaEffect.isSupported() - ? Win11Dialog.create(optionPane, this, "Forge Installer") - : optionPane.createDialog("Forge Installer"); + try { + dialog = Win11MicaEffect.isSupported() + ? Win11Dialog.create(optionPane, this, "Forge Installer") + : optionPane.createDialog("Forge Installer"); + } catch (Exception e) { + System.out.println("Failed to setup Win11 dialog, falling back to Swing default:"); + e.printStackTrace(); + dialog = optionPane.createDialog("Forge Installer"); + } dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); dialog.setVisible(true); int result = (Integer) (optionPane.getValue() != null ? optionPane.getValue() : -1); diff --git a/src/main/java/net/minecraftforge/installer/Win11Dialog.java b/src/main/java/net/minecraftforge/installer/Win11Dialog.java index 7ecb58f..228d795 100644 --- a/src/main/java/net/minecraftforge/installer/Win11Dialog.java +++ b/src/main/java/net/minecraftforge/installer/Win11Dialog.java @@ -50,7 +50,7 @@ private Win11Dialog() {} * @return A new JDialog instance with custom window decorations and support for the Mica backdrop on Windows 11. * @see Win11MicaEffect */ - static JDialog create(JOptionPane optionPane, JPanel installerPanel, String title) { + static JDialog create(JOptionPane optionPane, JPanel installerPanel, String title) throws Exception { JDialog installerDialog = new JDialog(null, title, Dialog.ModalityType.APPLICATION_MODAL); installerDialog.setUndecorated(true); installerDialog.setType(Window.Type.NORMAL); diff --git a/src/main/java/net/minecraftforge/installer/Win11MicaEffect.java b/src/main/java/net/minecraftforge/installer/Win11MicaEffect.java index 25a1f20..587734c 100644 --- a/src/main/java/net/minecraftforge/installer/Win11MicaEffect.java +++ b/src/main/java/net/minecraftforge/installer/Win11MicaEffect.java @@ -15,7 +15,7 @@ private Win11MicaEffect() {} static void prepare(Component component) {} - static void install(JDialog dialog) {} + static void install(JDialog dialog) throws Exception {} static boolean isSupported() { return false;