Compare commits

..
7 Commits
Author SHA1 Message Date
RuiFPB f152ed9df5 Fix scaling issues on Linux 2026-09-25 00:38:28 +01:00
RuiFPB 87217afa6e Add Mike42 Machine Type 2026-09-25 00:37:52 +01:00
Retrograde b9997e3edc Use HTTPS instead of HTTP in Schema referencs in the Maven POM file. (#25)
Use HTTPS instead of HTTP in Schema referencs in the Maven POM file.

This fixes cvc-elt.1.a: Cannot find the declaration of element 'project'. errors when building fx from VS.code.

Reviewed-on: https://codeberg.org/sethm/symon/pulls/25
Reviewed-by: Seth Morabito <sethm@noreply.codeberg.org>
2026-06-25 00:08:22 +02:00
Retrograde 45c8a69d7b 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 <sethm@noreply.codeberg.org>
2026-06-24 23:13:23 +02:00
Retrograde 2e2163fefb Added handling of -h/--help CLI option, to print out available options. (#22)
Small CLI improvement: adding a -h / --help switch, to be able to get a printout of the available options.

Reviewed-on: https://codeberg.org/sethm/symon/pulls/22
Reviewed-by: Seth Morabito <sethm@noreply.codeberg.org>
2026-06-24 20:45:51 +02:00
Seth Morabito 995b372f55 Update README with Codeberg links 2026-03-25 13:16:52 -07:00
Seth Morabito 47c87aac56 Update README for high-dpi 2025-04-13 11:38:33 -07:00
19 changed files with 467 additions and 84 deletions

No files matched your search

+33 -15
View File
@@ -1,13 +1,13 @@
SYMON - A 6502 System Simulator
kSYMON - A 6502 System Simulator
===============================
**Version:** 1.5.0
**Version:** 1.5.1k
**Last Updated:** 30 March, 2025
**Last Updated:** 24 September, 2026
See the file COPYING for license.
![Symon Simulator in Action](https://github.com/sethm/symon/raw/master/screenshots/full.jpg)
![Symon Simulator in Action](https://codeberg.org/sethm/symon/raw/master/screenshots/full.jpg)
## 1.0 About
@@ -71,9 +71,16 @@ memory.
- `$6000`--`$600F`: 6522 VIA
- `$8000`--`$FFFF`: 16KB ROM
#### 3.1.5 Mike42 Memory Map
- `$0000`--`$7FFF`: 32KB RAM
- `$8000`--`$800F`: 6522 VIA
- `$8400`--`$8403`: MOS 6551 ACIA (Serial Console)
- `$C000`--`$FFFF`: 16KB ROM
### 3.2 Serial Console and CPU Status
![Serial Console](https://github.com/sethm/symon/raw/master/screenshots/console.png)
![Serial Console](https://codeberg.org/sethm/symon/raw/master/screenshots/console.png)
The main window of the simulator acts as the primary Input/Output
system through a virtual serial terminal. It also provides CPU status.
@@ -100,13 +107,13 @@ see the official datasheet:
- [MOS 6551 ACIA](http://archive.6502.org/datasheets/mos_6551_acia.pdf)
![Font Selection](https://github.com/sethm/symon/raw/master/screenshots/font_selection.png)
![Font Selection](https://codeberg.org/sethm/symon/raw/master/screenshots/font_selection.png)
The console supports font sizes from 10 to 20 points.
### 3.3 ROM Loading
![ROM Loading](https://github.com/sethm/symon/raw/master/screenshots/load_rom.png)
![ROM Loading](https://codeberg.org/sethm/symon/raw/master/screenshots/load_rom.png)
Symon can load any appropriately sized ROM image. The Symon
architecture expects as 16KB (16384 byte) ROM image, while the
@@ -117,32 +124,32 @@ address.
### 3.4 Memory Window
![Memory Window](https://github.com/sethm/symon/raw/master/screenshots/memory_window.png)
![Memory Window](https://codeberg.org/sethm/symon/raw/master/screenshots/memory_window.png)
Memory contents can be viewed (and edited) one page at a time through the Memory Window.
### 3.5 Trace Log
![Trace Log](https://github.com/sethm/symon/raw/master/screenshots/trace_log.png)
![Trace Log](https://codeberg.org/sethm/symon/raw/master/screenshots/trace_log.png)
The last 20,000 execution steps are disassembled and logged to the Trace Log
Window.
### 3.6 Simulator Speeds
![Speeds](https://github.com/sethm/symon/raw/master/screenshots/simulator_menu.png)
![Speeds](https://codeberg.org/sethm/symon/raw/master/screenshots/simulator_menu.png)
Simulated speeds may be set from 1MHz to 8MHz.
### 3.7 Breakpoints
![Breakpoints](https://github.com/sethm/symon/raw/master/screenshots/breakpoints.png)
![Breakpoints](https://codeberg.org/sethm/symon/raw/master/screenshots/breakpoints.png)
Breakpoints can be set and removed through the Breakpoints window.
### 3.8 Experimental 6545 CRTC Video
![Composite Video](https://github.com/sethm/symon/raw/master/screenshots/video_window.png)
![Composite Video](https://codeberg.org/sethm/symon/raw/master/screenshots/video_window.png)
This feature is highly experimental. It's possible to open a video window
from the "View" menu. This window simulates the output of a MOS 6545 CRT
@@ -229,7 +236,18 @@ to specify machine type and CPU type. The options are:
- `-r`,`-rom <file>`: Use the specified file as the ROM image.
- `-b`,`-brk`: Halt the simulator on a BRK instruction (default is to continue)
### 4.2 ROM images
### 4.2 High-DPI UI scaling
In certain environments with high-DPI (e.g. 4K) displays, the UI may
not automatically scale and appear quite tiny. This can be fixed
with the command line argument `-Dsun.java2d.uiScale=<factor>`,
for example:
```
java -Dsun.java2d.uiScale=2.0 -jar symon-1.5.0.jar
```
### 4.3 ROM images
The simulator requires a ROM image loaded into memory to work
properly. Without a ROM in memory, the simulator will not be able to
@@ -247,7 +265,7 @@ architecture named 'ehbasic.rom', containing Lee Davison's Enhanced
*Note*: Presently, EhBASIC only works with the Symon machine
architecture, not with MULTICOMP.
### 4.3 Loading A Program
### 4.4 Loading A Program
In addition to ROM images, programs in the form of raw binary object files can
be loaded directly into memory from "Load Program..." in the File menu.
@@ -263,7 +281,7 @@ for testing.
- 'hello.prg' will continuously print "Hello, 6502 World!" to the console.
### 4.4 Running
### 4.5 Running
After loading a program or ROM image, clicking "Run" will start the simulator
running.
+34 -11
View File
@@ -1,10 +1,11 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/maven-v4.0.0.xsd">
<project xmlns="https://maven.apache.org/POM/4.0.0"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://maven.apache.org/POM/4.0.0 https://maven.apache.org/maven-v4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.loomcom.symon</groupId>
<artifactId>symon</artifactId>
<packaging>jar</packaging>
<version>1.5.0</version>
<version>1.5.1k</version>
<name>symon</name>
<url>https://git.loomcom.com/seth/symon</url>
<properties>
@@ -15,11 +16,32 @@
UTF-8
</project.reporting.outputEncoding>
</properties>
<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.5.16</version>
<version>1.5.34</version>
</dependency>
<dependency>
<groupId>commons-cli</groupId>
<artifactId>commons-cli</artifactId>
<version>1.5.0</version>
</dependency>
<dependency>
<groupId>com.formdev</groupId>
<artifactId>flatlaf</artifactId>
<version>3.7.1</version>
</dependency>
<dependency>
<groupId>com.github.Dansoftowner</groupId>
<artifactId>jSystemThemeDetector</artifactId>
<version>3.9.1</version>
</dependency>
<dependency>
<groupId>junit</groupId>
@@ -34,9 +56,9 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>commons-cli</groupId>
<artifactId>commons-cli</artifactId>
<version>1.5.0</version>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna</artifactId>
<version>5.14.0</version>
</dependency>
</dependencies>
@@ -48,7 +70,8 @@
<version>3.2.0</version>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.loomcom.symon.Main</mainClass>
</transformer>
</transformers>
@@ -79,8 +102,8 @@
<version>3.7.0</version>
<configuration>
<compilerArgument>-Xlint:unchecked</compilerArgument>
<source>11</source>
<target>11</target>
<source>16</source>
<target>16</target>
</configuration>
</plugin>
@@ -120,4 +143,4 @@
</plugin>
</plugins>
</build>
</project>
</project>
+17 -26
View File
@@ -25,19 +25,20 @@
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());
/**
/**
* Main entry point to the simulator. Creates a simulator and shows the main
* window.
*
@@ -49,6 +50,7 @@ public class Main {
Options options = new Options();
options.addOption(new Option("h", "help", false, "Print this message."));
options.addOption(new Option("m", "machine", true, "Specify machine type."));
options.addOption(new Option("c", "cpu", true, "Specify CPU type."));
options.addOption(new Option("r", "rom", true, "Specify ROM file."));
@@ -57,11 +59,18 @@ public class Main {
CommandLineParser parser = new DefaultParser();
try {
CommandLine line = parser.parse(options, args);
InstructionTable.CpuBehavior cpuBehavior = null;
String romFile = null;
boolean haltOnBreak = false;
if (line.hasOption("help")) {
HelpFormatter formatter = new HelpFormatter();
formatter.printHelp("symon", options, false);
return;
}
if (line.hasOption("machine")) {
String machine = line.getOptionValue("machine").toLowerCase(Locale.ENGLISH);
switch (machine) {
@@ -109,28 +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) {
@@ -140,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());
@@ -150,7 +141,6 @@ public class Main {
}
});
Simulator.MainCommand cmd = simulator.waitForCommand();
if (cmd.equals(Simulator.MainCommand.SELECTMACHINE)) {
@@ -163,4 +153,5 @@ public class Main {
logger.error("Could not start Symon. Reason: {}", ex.getMessage());
}
}
}
@@ -23,6 +23,7 @@
package com.loomcom.symon;
import com.formdev.flatlaf.util.UIScale;
import com.loomcom.symon.devices.Memory;
import com.loomcom.symon.exceptions.*;
import com.loomcom.symon.machines.Machine;
@@ -53,7 +54,7 @@ public class Simulator {
// UI constants
private static final int DEFAULT_FONT_SIZE = 12;
private static final Font DEFAULT_FONT = new Font(Font.MONOSPACED, Font.PLAIN, DEFAULT_FONT_SIZE);
private static final Font DEFAULT_FONT = new Font(Font.MONOSPACED, Font.PLAIN, UIScale.scale(DEFAULT_FONT_SIZE));
private static final int CONSOLE_BORDER_WIDTH = 10;
// Clock periods, in NS, for each speed. 0MHz, 1MHz, 2MHz, 3MHz, 4MHz, 5MHz, 6MHz, 7MHz, 8MHz.
@@ -0,0 +1,134 @@
package com.loomcom.symon.machines;
import com.loomcom.symon.Bus;
import com.loomcom.symon.Cpu;
import com.loomcom.symon.InstructionTable;
import com.loomcom.symon.devices.*;
import com.loomcom.symon.exceptions.MemoryRangeException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
public class MichaelBillingtonMachine implements Machine{
private static final Logger logger = LoggerFactory.getLogger(MichaelBillingtonMachine.class.getName());
// Constants used by the simulated system
private static final int BUS_BOTTOM = 0x0000;
private static final int BUS_TOP = 0xffff;
// 32k of ram, from $0000 to $7fff
private static final int MEMORY_BASE = 0x0000;
private static final int MEMORY_SIZE = 0x8000;
// VIA at $8000-$800f
private static final int PIA_BASE = 0x8000;
// ACIA at $8400-$8403
private static final int ACIA_BASE = 0x8400;
// 16k ROM from $c000 to $ffff
private static final int ROM_BASE = 0xc000;
private static final int ROM_SIZE = 0x4000;
private final Bus bus;
private final Cpu cpu;
private final Acia acia;
private final Pia pia;
private final Memory ram;
private Memory rom;
public MichaelBillingtonMachine(String romFile) throws Exception {
this.bus = new Bus(BUS_BOTTOM, BUS_TOP);
this.cpu = new Cpu();
this.ram = new Memory(MEMORY_BASE, MEMORY_BASE + MEMORY_SIZE - 1);
this.pia = new Via6522(PIA_BASE);
this.acia = new Acia6551(ACIA_BASE);
bus.addCpu(cpu);
bus.addDevice(ram);
bus.addDevice(pia);
bus.addDevice(acia);
if (romFile != null) {
File romImage = new File(romFile);
if (romImage.canRead()) {
logger.info("Loading ROM image from file {}", romImage);
this.rom = Memory.makeROM(ROM_BASE, ROM_BASE + ROM_SIZE - 1, romImage);
} else {
logger.info("Default ROM file {} not found, loading empty R/W memory image.", romImage);
this.rom = Memory.makeRAM(ROM_BASE, ROM_BASE + ROM_SIZE - 1);
}
} else {
logger.info("No ROM file specified, loading empty R/W memory image.");
this.rom = Memory.makeRAM(ROM_BASE, ROM_BASE + ROM_SIZE - 1);
}
bus.addDevice(rom);
}
@Override
public Bus getBus() {
return bus;
}
@Override
public Cpu getCpu() {
return cpu;
}
@Override
public Memory getRam() {
return ram;
}
@Override
public Acia getAcia() {
return acia;
}
@Override
public Pia getPia() {
return pia;
}
@Override
public Crtc getCrtc() {
return null;
}
@Override
public Memory getRom() {
return rom;
}
@Override
public void setRom(Memory rom) throws MemoryRangeException {
if (this.rom != null) {
bus.removeDevice(this.rom);
}
this.rom = rom;
bus.addDevice(this.rom);
}
@Override
public int getRomBase() {
return ROM_BASE;
}
@Override
public int getRomSize() {
return ROM_SIZE;
}
@Override
public int getMemorySize() {
return MEMORY_SIZE;
}
@Override
public String getName() {
return "mike42";
}
}
@@ -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));
@@ -23,6 +23,7 @@
package com.loomcom.symon.ui;
import com.formdev.flatlaf.util.UIScale;
import com.loomcom.symon.Cpu;
import com.loomcom.symon.CpuState;
import com.loomcom.symon.machines.Machine;
@@ -34,6 +35,7 @@ import javax.swing.plaf.metal.MetalTextFieldUI;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Objects;
/**
* UI component that displays the current state of the simulated CPU.
@@ -74,7 +76,7 @@ public class StatusPanel extends JPanel {
private static final int EMPTY_BORDER = 10;
private static final Border LABEL_BORDER = BorderFactory.createEmptyBorder(0, 5, 0, 0);
private static final Font LABEL_FONT = new Font(Font.SANS_SERIF, Font.BOLD, 12);
private static final Font LABEL_FONT = new Font(Font.SANS_SERIF, Font.BOLD, UIScale.scale(12));
private static final Dimension LARGE_TEXT_FIELD_SIZE = new Dimension(134, 22);
private static final Dimension SMALL_TEXT_FIELD_SIZE = new Dimension(65, 22);
@@ -86,7 +88,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));
@@ -99,20 +101,20 @@ public class StatusPanel extends JPanel {
JPanel statusFlagsPanel = new JPanel();
statusFlagsPanel.setAlignmentX(LEFT_ALIGNMENT);
carryOn = new ImageIcon(this.getClass().getResource("/C_on.png"));
carryOff = new ImageIcon(this.getClass().getResource("/C_off.png"));
zeroOn = new ImageIcon(this.getClass().getResource("/Z_on.png"));
zeroOff = new ImageIcon(this.getClass().getResource("/Z_off.png"));
irqOn = new ImageIcon(this.getClass().getResource("/I_on.png"));
irqOff = new ImageIcon(this.getClass().getResource("/I_off.png"));
decimalOn = new ImageIcon(this.getClass().getResource("/D_on.png"));
decimalOff = new ImageIcon(this.getClass().getResource("/D_off.png"));
breakOn = new ImageIcon(this.getClass().getResource("/B_on.png"));
breakOff = new ImageIcon(this.getClass().getResource("/B_off.png"));
overflowOn = new ImageIcon(this.getClass().getResource("/O_on.png"));
overflowOff = new ImageIcon(this.getClass().getResource("/O_off.png"));
negativeOn = new ImageIcon(this.getClass().getResource("/N_on.png"));
negativeOff = new ImageIcon(this.getClass().getResource("/N_off.png"));
carryOn = loadScaledIcon("/C_on.png");
carryOff = loadScaledIcon("/C_off.png");
zeroOn = loadScaledIcon("/Z_on.png");
zeroOff = loadScaledIcon("/Z_off.png");
irqOn = loadScaledIcon("/I_on.png");
irqOff = loadScaledIcon("/I_off.png");
decimalOn = loadScaledIcon("/D_on.png");
decimalOff = loadScaledIcon("/D_off.png");
breakOn = loadScaledIcon("/B_on.png");
breakOff = loadScaledIcon("/B_off.png");
overflowOn = loadScaledIcon("/O_on.png");
overflowOff = loadScaledIcon("/O_off.png");
negativeOn = loadScaledIcon("/N_on.png");
negativeOff = loadScaledIcon("/N_off.png");
// Initialize all to off
carryFlagLabel = new JLabel(carryOff, JLabel.CENTER);
@@ -125,16 +127,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 +244,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);
@@ -389,16 +389,11 @@ public class StatusPanel extends JPanel {
JTextField textField = new JTextField("");
textField.setAlignmentX(LEFT_ALIGNMENT);
textField.setEditable(editable);
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());
Dimension scaleSize = UIScale.scale(size);
textField.setMinimumSize(scaleSize);
textField.setMaximumSize(scaleSize);
textField.setPreferredSize(scaleSize);
return textField;
}
@@ -407,4 +402,14 @@ public class StatusPanel extends JPanel {
String val = source.getText().replaceAll("[^0-9a-fA-F]", "");
return Integer.parseInt(val, 16);
}
private ImageIcon loadScaledIcon(String resourcePath) {
ImageIcon icon = new ImageIcon(Objects.requireNonNull(this.getClass().getResource(resourcePath)));
int scaledWidth = UIScale.scale(icon.getIconWidth());
int scaledHeight = UIScale.scale(icon.getIconHeight());
Image scaleImage = icon.getImage().getScaledInstance(scaledWidth, scaledHeight, Image.SCALE_SMOOTH);
return new ImageIcon(scaleImage);
}
}
+151
View File
@@ -0,0 +1,151 @@
/*
* Copyright (c) 2008-2025 Seth J. Morabito <web@loomcom.com>
*
* 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 com.loomcom.symon.machines.*;
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 java.awt.*;
import java.beans.PropertyChangeListener;
import java.lang.reflect.InvocationTargetException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
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");
logger.debug("AWT Toolkit class: {}", Toolkit.getDefaultToolkit().getClass().getName());
// 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");
applyDetectedUiScale();
// 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", "Mike42" };
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 if (s != null && s.equals("Mike42")) {
return MichaelBillingtonMachine.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;
}
private static void applyDetectedUiScale() {
Double rawScale = X11ResourceReader.readXftDpiScale(); // ex: 1.5, 2.0, 2.5...
if (rawScale != null && rawScale > 1.0) {
// flatlaf.uiScale aceita frações — mantém a precisão original
System.setProperty("flatlaf.uiScale", rawScale + "x");
// sun.java2d.uiScale só aceita inteiros — arredonda para cima
int javaScale = (int) Math.ceil(rawScale);
//System.setProperty("sun.java2d.uiScale", String.valueOf(javaScale));
logger.debug("Detected scale {}x, applying flatlaf.uiScale={}x, sun.java2d.uiScale={}x",
rawScale, rawScale, javaScale);
} else {
logger.debug("No X11 HiDPI scale detected, using default.");
}
}
}
@@ -0,0 +1,54 @@
package com.loomcom.symon.ui;
import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.Pointer;
import com.sun.jna.ptr.IntByReference;
import com.sun.jna.ptr.PointerByReference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class X11ResourceReader {
private final static Logger logger = LoggerFactory.getLogger(X11ResourceReader.class.getName());
public interface X11 extends Library {
X11 INSTANCE = Native.load("X11", X11.class);
Pointer XOpenDisplay(String display);
Pointer XResourceManagerString(Pointer display);
void XCloseDisplay(Pointer display);
}
public static Double readXftDpiScale() {
Pointer display = null;
try {
display = X11.INSTANCE.XOpenDisplay(null);
if (display == null) {
return null; // não conseguimos ligar ao X server (ex: Wayland puro sem XWayland)
}
Pointer resourceString = X11.INSTANCE.XResourceManagerString(display);
if (resourceString == null) {
return null;
}
String resources = resourceString.getString(0);
for (String line : resources.split("\n")) {
if (line.startsWith("Xft.dpi:")) {
int dpi = Integer.parseInt(line.split(":")[1].trim());
return Math.round((dpi / 96.0) * 4) / 4.0;
}
}
return null;
} catch (UnsatisfiedLinkError | Exception e) {
logger.debug("Could not read X11 resource manager (libX11 unavailable?)", e);
return null;
} finally {
if (display != null) {
X11.INSTANCE.XCloseDisplay(display);
}
}
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 191 B

After

Width:  |  Height:  |  Size: 220 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 196 B

After

Width:  |  Height:  |  Size: 232 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 192 B

After

Width:  |  Height:  |  Size: 224 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 183 B

After

Width:  |  Height:  |  Size: 202 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 198 B

After

Width:  |  Height:  |  Size: 238 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 192 B

After

Width:  |  Height:  |  Size: 221 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 198 B

After

Width:  |  Height:  |  Size: 238 B

@@ -0,0 +1,3 @@
# Loaded for FlatDarkLaf theme only.
Colors.accent.default = #4B6EAF
@@ -0,0 +1 @@
# Loaded for all FlatLaf themes.
@@ -0,0 +1,3 @@
# Loaded for FlatLightLaf theme only.
Colors.accent.default = #2675BF