From 45c8a69d7b47e9c3a5c0c2e61348cb76f93dba2d Mon Sep 17 00:00:00 2001 From: Retrograde Date: Wed, 24 Jun 2026 23:13:23 +0200 Subject: [PATCH] FlatLaf look-and-feel with better Dark mode support. (#23) This PR configures Symon to use the FlatLaf Swing look-and-feel, giving both a slightly more modern look but also much better support for Dark/Light UI modes. Additional small tweaks to make the UI more readable in both Dark/Light modes. Detection of system theme also added, selecting the Dark/Light FlatLaf variant accordingly. Gets rid of need for the hardcoded Metal UI for text fields in StatusPanel workaround as well, as FlatLaf renders these small fields correctly. Reviewed-on: https://codeberg.org/sethm/symon/pulls/23 Reviewed-by: Seth Morabito --- pom.xml | 36 +++-- src/main/java/com/loomcom/symon/Main.java | 32 ++--- .../loomcom/symon/ui/BreakpointsWindow.java | 1 - .../com/loomcom/symon/ui/StatusPanel.java | 15 +-- src/main/java/com/loomcom/symon/ui/Ui.java | 124 ++++++++++++++++++ src/main/resources/B_off.png | Bin 191 -> 220 bytes src/main/resources/C_off.png | Bin 196 -> 232 bytes src/main/resources/D_off.png | Bin 192 -> 224 bytes src/main/resources/I_off.png | Bin 183 -> 202 bytes src/main/resources/N_off.png | Bin 198 -> 238 bytes src/main/resources/O_off.png | Bin 192 -> 221 bytes src/main/resources/Z_off.png | Bin 198 -> 238 bytes .../FlatDarkLaf.properties | 3 + .../FlatLaf.properties | 1 + .../FlatLightLaf.properties | 3 + 15 files changed, 170 insertions(+), 45 deletions(-) create mode 100644 src/main/java/com/loomcom/symon/ui/Ui.java create mode 100644 src/main/resources/com.loomcom.symon.themes/FlatDarkLaf.properties create mode 100644 src/main/resources/com.loomcom.symon.themes/FlatLaf.properties create mode 100644 src/main/resources/com.loomcom.symon.themes/FlatLightLaf.properties diff --git a/pom.xml b/pom.xml index 5ab53c3..ac85a67 100644 --- a/pom.xml +++ b/pom.xml @@ -1,5 +1,6 @@ - + 4.0.0 com.loomcom.symon symon @@ -15,12 +16,33 @@ UTF-8 + + + jitpack.io + https://jitpack.io + + ch.qos.logback logback-classic 1.5.16 + + commons-cli + commons-cli + 1.5.0 + + + com.formdev + flatlaf + 3.7.1 + + + com.github.Dansoftowner + jSystemThemeDetector + 3.9.1 + junit junit @@ -33,11 +55,6 @@ 4.11.0 test - - commons-cli - commons-cli - 1.5.0 - @@ -48,7 +65,8 @@ 3.2.0 - + com.loomcom.symon.Main @@ -120,4 +138,4 @@ - + \ No newline at end of file diff --git a/src/main/java/com/loomcom/symon/Main.java b/src/main/java/com/loomcom/symon/Main.java index df45c0d..1f3cc88 100644 --- a/src/main/java/com/loomcom/symon/Main.java +++ b/src/main/java/com/loomcom/symon/Main.java @@ -25,14 +25,15 @@ package com.loomcom.symon; import com.loomcom.symon.machines.*; +import com.loomcom.symon.ui.Ui; + import org.apache.commons.cli.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Locale; -import javax.swing.JOptionPane; + import javax.swing.SwingUtilities; -import javax.swing.UIManager; public class Main { private final static Logger logger = LoggerFactory.getLogger(Main.class.getName()); @@ -58,6 +59,7 @@ public class Main { CommandLineParser parser = new DefaultParser(); try { + CommandLine line = parser.parse(options, args); InstructionTable.CpuBehavior cpuBehavior = null; String romFile = null; @@ -116,27 +118,11 @@ public class Main { haltOnBreak = true; } + Ui.initUi(); + while (true) { if (machineClass == null) { - Object[] possibilities = { "Symon", "Multicomp", "Simple", "BenEater" }; - String s = (String) JOptionPane.showInputDialog( - null, - "Please choose the machine type to be emulated:", - "Machine selection", - JOptionPane.PLAIN_MESSAGE, - null, - possibilities, - "Symon"); - - if (s != null && s.equals("Multicomp")) { - machineClass = MulticompMachine.class; - } else if (s != null && s.equals("Simple")) { - machineClass = SimpleMachine.class; - } else if (s != null && s.equals("BenEater")) { - machineClass = BenEaterMachine.class; - } else { - machineClass = SymonMachine.class; - } + machineClass = Ui.showMachineSelectionDialog(); } if (cpuBehavior == null) { @@ -146,9 +132,8 @@ public class Main { final Simulator simulator = new Simulator(machineClass, cpuBehavior, romFile, haltOnBreak); SwingUtilities.invokeLater(() -> { + // Create the main UI window: try { - UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); - // Create the main UI window simulator.createAndShowUi(); } catch (Exception e) { logger.error("Error during Symon UI initialization: {}", e.getMessage()); @@ -168,4 +153,5 @@ public class Main { logger.error("Could not start Symon. Reason: {}", ex.getMessage()); } } + } diff --git a/src/main/java/com/loomcom/symon/ui/BreakpointsWindow.java b/src/main/java/com/loomcom/symon/ui/BreakpointsWindow.java index 3794220..581b372 100644 --- a/src/main/java/com/loomcom/symon/ui/BreakpointsWindow.java +++ b/src/main/java/com/loomcom/symon/ui/BreakpointsWindow.java @@ -70,7 +70,6 @@ public class BreakpointsWindow extends JFrame { final JTable breakpointsTable = new JTable(breakpoints); breakpointsTable.setShowGrid(true); - breakpointsTable.setGridColor(Color.LIGHT_GRAY); breakpointsTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); breakpointsTable.getSelectionModel().addListSelectionListener(e -> removeButton.setEnabled(e.getFirstIndex() > -1)); diff --git a/src/main/java/com/loomcom/symon/ui/StatusPanel.java b/src/main/java/com/loomcom/symon/ui/StatusPanel.java index cef25fe..b8878b6 100644 --- a/src/main/java/com/loomcom/symon/ui/StatusPanel.java +++ b/src/main/java/com/loomcom/symon/ui/StatusPanel.java @@ -86,7 +86,7 @@ public class StatusPanel extends JPanel { private void createUi() { Border emptyBorder = BorderFactory.createEmptyBorder(EMPTY_BORDER, EMPTY_BORDER, - EMPTY_BORDER, EMPTY_BORDER); + EMPTY_BORDER, EMPTY_BORDER); Border etchedBorder = BorderFactory.createEtchedBorder(EtchedBorder.LOWERED); setBorder(BorderFactory.createCompoundBorder(emptyBorder, etchedBorder)); @@ -125,16 +125,15 @@ public class StatusPanel extends JPanel { // Add tool-tip text carryFlagLabel.setToolTipText("Carry: The last operation caused an overflow " + - "from bit 7 of the result or an underflow from bit 0"); + "from bit 7 of the result or an underflow from bit 0"); zeroFlagLabel.setToolTipText("Zero: The result of the last operation was 0"); irqDisableFlagLabel.setToolTipText("Interrupt Disable: Processor will not respond to IRQ"); decimalModeFlagLabel.setToolTipText("Decimal Mode"); breakFlagLabel.setToolTipText("Break: BRK instruction occurred"); overflowFlagLabel.setToolTipText("Overflow: The result of the last operation was " + - "an invalid 2's complement result"); + "an invalid 2's complement result"); negativeFlagLabel.setToolTipText("Negative: The result of the last operation set bit 7"); - statusFlagsPanel.add(negativeFlagLabel); statusFlagsPanel.add(overflowFlagLabel); statusFlagsPanel.add(breakFlagLabel); @@ -243,7 +242,6 @@ public class StatusPanel extends JPanel { constraints.gridy = 1; add(statusFlagsPanel, constraints); - constraints.insets = new Insets(5, 0, 0, 0); constraints.gridy = 2; add(opcodeLabel, constraints); @@ -392,13 +390,6 @@ public class StatusPanel extends JPanel { textField.setMinimumSize(size); textField.setMaximumSize(size); textField.setPreferredSize(size); - textField.setBackground(Color.WHITE); - // Although we usually defer to the system look-and-feel, for - // these small text fields in particular, we use a Metal - // look-and-feel because native look-and-feel breaks very small - // text fields under GTK+ (they are drawn with an inner margin - // even if the margin is set to 0) - textField.setUI(new MetalTextFieldUI()); return textField; } diff --git a/src/main/java/com/loomcom/symon/ui/Ui.java b/src/main/java/com/loomcom/symon/ui/Ui.java new file mode 100644 index 0000000..57ff169 --- /dev/null +++ b/src/main/java/com/loomcom/symon/ui/Ui.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2008-2025 Seth J. Morabito + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.loomcom.symon.ui; + +import javax.swing.JDialog; +import javax.swing.JFrame; +import javax.swing.JOptionPane; +import javax.swing.LookAndFeel; +import javax.swing.UIManager; +import javax.swing.UnsupportedLookAndFeelException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.formdev.flatlaf.FlatDarkLaf; +import com.formdev.flatlaf.FlatLaf; +import com.formdev.flatlaf.FlatLightLaf; +import com.formdev.flatlaf.util.SystemInfo; +import com.jthemedetecor.OsThemeDetector; +import com.loomcom.symon.machines.BenEaterMachine; +import com.loomcom.symon.machines.MulticompMachine; +import com.loomcom.symon.machines.SimpleMachine; +import com.loomcom.symon.machines.SymonMachine; + +public final class Ui { + private final static Logger logger = LoggerFactory.getLogger(Ui.class.getName()); + + public static void initUi() throws UnsupportedLookAndFeelException { + // Location of properties resources for app-specific UI defaults: + FlatLaf.registerCustomDefaultsSource("com.loomcom.symon.themes"); + + // Enable automatic scaling, based on system DPI settings: + System.setProperty("sun.java2d.uiScale.enabled", "true"); + System.setProperty("sun.java2d.uiScale", "1x"); + System.setProperty("flatlaf.uiScale", "1x"); + + // OS-specific tweaking: + + if (SystemInfo.isLinux) { + logger.debug("Adapting UI for Linux."); + + // Enable custom window decorations: + JFrame.setDefaultLookAndFeelDecorated(true); + JDialog.setDefaultLookAndFeelDecorated(true); + } + + else if (SystemInfo.isMacOS) { + logger.debug("Adapting UI for MacOS."); + + // Move menu bar from JFrame window to top of screen: + System.setProperty("apple.laf.useScreenMenuBar", "true"); + // Set name used in screen menu bar (first menu after Apple menu): + System.setProperty("apple.awt.application.name", "Symon"); + // Use current macOS appearance (light/dark) for window title bars: + System.setProperty("apple.awt.application.appearance", "system"); + } + + else if (SystemInfo.isWindows) { + logger.debug("Adapting UI for Windows."); + } + + UIManager.setLookAndFeel(createLookAndFeel()); + } + + public static Class showMachineSelectionDialog() { + Object[] possibilities = { "Symon", "Multicomp", "Simple", "BenEater" }; + String s = (String) JOptionPane.showInputDialog( + null, + "Please choose the machine type to be emulated:", + "Machine selection", + JOptionPane.PLAIN_MESSAGE, + null, + possibilities, + "Symon"); + + if (s != null && s.equals("Multicomp")) { + return MulticompMachine.class; + } else if (s != null && s.equals("Simple")) { + return SimpleMachine.class; + } else if (s != null && s.equals("BenEater")) { + return BenEaterMachine.class; + } else { + return SymonMachine.class; + } + } + + private static LookAndFeel createLookAndFeel() { + final OsThemeDetector themeDetector = OsThemeDetector.getDetector(); + final boolean systemThemeIsDark = themeDetector.isDark(); + + // Set light/dark mode depending on what can be detected from the system theme: + FlatLaf theme; + if (systemThemeIsDark) { + logger.debug("Using DARK theme."); + theme = new FlatDarkLaf(); + } else { + logger.debug("Using LIGHT theme."); + theme = new FlatLightLaf(); + } + return theme; + } + +} diff --git a/src/main/resources/B_off.png b/src/main/resources/B_off.png index 145264f1aadb6b82e44c07886df44d3a1ae982e2..15a9b9d3903e7dbe50f9fb2eba4eda0a8ddb989a 100644 GIT binary patch delta 167 zcmdnbc!zOEak-aXR_W|Nr*PmJOXc zu{=CHItFd-U7C)o@9(WWUy$`z)=i`rD9WQRxBuAck0*UwY;O83-R~w=()emho=Dy6 zIYEz17HO~ZdwFct%ql}?bI;wohv&-gShr_=V+~I S6BRW$89ZJ6T-G@yGywo|QbPRz delta 137 zcmcb^xSw%?BnLAC1A}9`{_csZHOhnnd_r9H-QEak-aXR_W|Nr*PmJOXc zu{=CHItFd-U7C)o@9(WWUy$`z)=i`rD9WQRxBuAck0*UwY;O83-R~xr({xqyZb8}W zIYEz1!j_jT)^K|+{5B-UjPFo@E1$-m#>-aAr|b!kKf1hP0z01`!}rI#x4#eE#H)C~ fU1%;z!clo*GR! rlzJ@F=x1L_Vhdx=!bTnjCLV?}Gnk%iZncmCn!w=c>gTe~DWM4ferPc? diff --git a/src/main/resources/D_off.png b/src/main/resources/D_off.png index 39db75d1d1276af3d71c71367299f6c70c337804..a41d035bd7b63ba9fdfb72b752a1235cf9daf4f2 100644 GIT binary patch delta 171 zcmX@W_<(VOWIY=L14CKl{Z1gomgMd3!tfsi7wla=87NZV>Eak-aXR_W|Nr*PmJOXc zu{=CHItFd-U7C)o@9(WWUy$`z)=i`rD9WQRxBuAck0*UwY;O83-R~w=()emho=Dy6 zIYEz17HO~ZdwFbCB?l|tS0_<11p|kY6_0`}KGZD{w<)-{x3+&$E(fFAE^%3gQ{Pl{ WWu}>0O;pt2WbkzLb6Mw<&;$TQe?v3? delta 138 zcmaFBcz|(&BnLAC1A}9`{_ctEHOhqod_r9H-QEak-aXR_W|Nr*PmJOXc zu{=CHItFd-U7C)o@9(WWUy$`z)=i`rD9WQRxBuAck0*UwY;O9=x=5`K-qqd}?A~v6|E>$hM)C e&4_`+fZ<>k(OUcWIY=L14CKl{Z1gomgMd3!tfsi7wla=87NZc>Eak-aXR_W|Nr*PmJOXc zu{=CHItFd-U7C)o@9(WWUy$`z)=i`rD9WQRxBuAck0*UwY;O9=vPi8C-u3a*$6FoV zhK{{!i@i9rj=gkun)Q**@AaIZPbQO|hB!3{PP4XA4f6RfJo(8zn;)kv4E&bb*zc?U lEvI^0K-Qw9Sc^exs=>(}ZyKv7Dr#^tc)I$ztaD0e0szbxOi2I$ delta 144 zcmaFIc#Ls^BnLAC1A}9`{_cr;HEM+dd_r9H-QEak-aXR_W|Nr*PmJOXc zu{=CHItFd-U7C)o@9(WWUy$`z)=i`rD9WQRxBuAck0*UwY;O83-R~xr({xqyZb8}W zIYEz1!j_jT)^K~yfAGMU1%;z!b!@rj>J n`6PSt5C`)OCuT_o772z+B1~@?{B44Ox*0rO{an^LB{Ts5V6QCo diff --git a/src/main/resources/Z_off.png b/src/main/resources/Z_off.png index 124dc55e20aaafcee89a9158c984e98224b4171c..0f12a57515c8a42283ed8f89b4d9dacb95eedfab 100644 GIT binary patch delta 185 zcmX@c_>OUcWIY=L14CKl{Z1gomgMd3!tfsi7wla=87NZc>Eak-aXR_W|Nr*PmJOXc zu{=CHItFd-U7C)o@9(WWUy$`z)=i`rD9WQRxBuAck0*UwY;O83-R~w=()emh9*^tV zVlU2Ym++Re3Qc}%{azl+l8)>BVe;^bhlg-j?~x4ELsAwum|E5ZsGjU^4pzR?|Nh=S k_mgHV49a)A`55fy>P`R1sKz}}QG=7g)78&qol`;+0CRmxnE(I) delta 144 zcmaFIc#Ls^BnLAC1A}9`{_cr;HEM+dd_r9H-Q