From a5a6c2b5208f3a0741ca8677df913f44708e73ec Mon Sep 17 00:00:00 2001 From: James Mark Chan Date: Mon, 7 Sep 2026 19:36:16 -0700 Subject: [PATCH 1/7] ensure rpm version is valid --- .github/actions/ci-version/action.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/actions/ci-version/action.yml b/.github/actions/ci-version/action.yml index 4f0d246..e8f73c3 100644 --- a/.github/actions/ci-version/action.yml +++ b/.github/actions/ci-version/action.yml @@ -45,8 +45,11 @@ runs: RAW=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) BASE=${RAW%-SNAPSHOT} - # Read the dedicated msi.version property — always purely numeric + # Read the dedicated msi.version and rpm.version properties — these are + # maintained separately in the POM with format restrictions (numeric-only + # for MSI, no hyphens for RPM) and must not be derived from project.version. MSI_BASE=$(mvn help:evaluate -Dexpression=msi.version -q -DforceStdout) + RPM_BASE=$(mvn help:evaluate -Dexpression=rpm.version -q -DforceStdout) # Prefer the PR head branch; fall back to push/tag branch name BRANCH="${GITHUB_HEAD_REF:-$GITHUB_REF_NAME}" @@ -63,7 +66,7 @@ runs: echo "install_version=${RAW}" >> "$GITHUB_OUTPUT" echo "base_version=${BASE}" >> "$GITHUB_OUTPUT" echo "msi_version=${MSI_BASE}" >> "$GITHUB_OUTPUT" - echo "rpm_version=${BASE}" >> "$GITHUB_OUTPUT" + echo "rpm_version=${RPM_BASE}" >> "$GITHUB_OUTPUT" echo "rpm_release=1" >> "$GITHUB_OUTPUT" echo "CI label skipped (branch=${BRANCH}, disable=${DISABLE})" exit 0 @@ -96,7 +99,7 @@ runs: echo "install_version=${INSTALL}" >> "$GITHUB_OUTPUT" echo "base_version=${BASE}" >> "$GITHUB_OUTPUT" echo "msi_version=${MSI_BASE}" >> "$GITHUB_OUTPUT" - echo "rpm_version=${BASE}" >> "$GITHUB_OUTPUT" + echo "rpm_version=${RPM_BASE}" >> "$GITHUB_OUTPUT" echo "rpm_release=ci.${SLUG}.${{ github.run_number }}" >> "$GITHUB_OUTPUT" echo "CI version: ${DISPLAY}" echo "Install version: ${INSTALL}" From 278e8a9bf7f225e80e1a637d9a4935b095aff61a Mon Sep 17 00:00:00 2001 From: James Mark Chan Date: Tue, 8 Sep 2026 23:18:17 -0700 Subject: [PATCH 2/7] #224 fix batch mode ubuntu, cleanup --- AGENT.md | 1 + README.md | 4 ++ .../src/main/java/jdiskmark/BatchConfig.java | 9 ++- .../src/main/java/jdiskmark/BatchPanel.java | 59 +++++++++++-------- .../src/main/java/jdiskmark/BatchWorker.java | 4 +- .../src/main/java/jdiskmark/DriveChecker.java | 24 ++++++++ .../src/main/java/jdiskmark/DrivePanel.java | 13 +--- jdm-core/src/main/java/jdiskmark/Gui.java | 3 + pom.xml | 2 +- 9 files changed, 76 insertions(+), 43 deletions(-) diff --git a/AGENT.md b/AGENT.md index d2c426b..1099110 100644 --- a/AGENT.md +++ b/AGENT.md @@ -69,6 +69,7 @@ The `jdm-core/docs/` folder contains authoritative design documents: - **Keep design docs current.** When a code change touches an area covered by `jdm-core/docs/`, update the relevant doc in the same session — do not leave design documentation out of sync with the code. +- Write robust, production-ready code that follows existing project conventions and remains easy to understand and maintain. --- diff --git a/README.md b/README.md index 8998955..1a8abc7 100644 --- a/README.md +++ b/README.md @@ -274,6 +274,10 @@ IOPS: 28892857 JDiskMark is developed with [NetBeans 25](https://netbeans.apache.org/front/main/download/) and [Java 25](https://www.oracle.com/java/technologies/downloads/). +on ubuntu desktop jdk can be installed with: + +`sudo apt update && sudo apt install -y openjdk-25-jdk` + ## Build from Source ### Prerequisites diff --git a/jdm-core/src/main/java/jdiskmark/BatchConfig.java b/jdm-core/src/main/java/jdiskmark/BatchConfig.java index fff6efe..02792d2 100644 --- a/jdm-core/src/main/java/jdiskmark/BatchConfig.java +++ b/jdm-core/src/main/java/jdiskmark/BatchConfig.java @@ -14,8 +14,13 @@ public static BatchConfig of(List drives, List profiles, public void applyProfileToApp(BenchmarkProfile profile, File driveLocation) { App.loadProfile(profile); - App.locationDir = driveLocation; - App.dataDir = new File(driveLocation.getAbsolutePath() + File.separator + App.DATADIRNAME); + File resolved = DriveChecker.resolveLocationForRoot(driveLocation); + if (resolved == null) { + throw new IllegalStateException( + "No writable location found on " + driveLocation.getAbsolutePath()); + } + App.locationDir = resolved; + App.dataDir = new File(resolved.getAbsolutePath() + File.separator + App.DATADIRNAME); } public int totalRuns() { diff --git a/jdm-core/src/main/java/jdiskmark/BatchPanel.java b/jdm-core/src/main/java/jdiskmark/BatchPanel.java index d592d04..2453af0 100644 --- a/jdm-core/src/main/java/jdiskmark/BatchPanel.java +++ b/jdm-core/src/main/java/jdiskmark/BatchPanel.java @@ -29,6 +29,7 @@ import javax.swing.JSplitPane; import javax.swing.JTable; import javax.swing.SpinnerNumberModel; +import javax.swing.SwingConstants; import javax.swing.SwingWorker; import javax.swing.border.EmptyBorder; import javax.swing.table.DefaultTableModel; @@ -49,6 +50,7 @@ public class BatchPanel extends JPanel { private static final Logger LOG = Logger.getLogger(BatchPanel.class.getName()); private static final DecimalFormat DF = new DecimalFormat("###.##"); + private static final DecimalFormat DF2 = new DecimalFormat("###.00"); private static final String CARD_SETUP = "setup"; private static final String CARD_RUNNING = "running"; @@ -79,6 +81,8 @@ public class BatchPanel extends JPanel { private JPanel chartContainer; private JTable summaryTable; private JLabel durationLabel; + private JButton startBatchButton; + private JButton newBatchButton; // State — benchmarks from the currently displayed results (for double-click loading) private List currentResultBenchmarks = new ArrayList<>(); @@ -176,13 +180,13 @@ private JPanel buildSetupCard() { profileColumn.add(profileSelectPanel); profileColumn.add(Box.createVerticalStrut(8)); - JButton startBtn = new JButton("Start Batch"); - startBtn.putClientProperty("FlatLaf.style", org.metricus.jdm.ui.ButtonStyles.DEFAULT_START); - startBtn.addActionListener(e -> startBatch()); + startBatchButton = new JButton("Start Batch"); + startBatchButton.putClientProperty("FlatLaf.style", org.metricus.jdm.ui.ButtonStyles.DEFAULT_START); + startBatchButton.addActionListener(e -> startBatch()); // MigLayout "h 40!" matches BenchmarkControlPanel's start button exactly, including DPI scaling. JPanel startBtnWrapper = new JPanel(new net.miginfocom.swing.MigLayout("insets 0, fillx", "[grow]", "[]")); startBtnWrapper.setAlignmentX(0); - startBtnWrapper.add(startBtn, "growx, h 40!"); + startBtnWrapper.add(startBatchButton, "growx, h 40!"); startBtnWrapper.setMaximumSize(new Dimension(Integer.MAX_VALUE, startBtnWrapper.getPreferredSize().height)); profileColumn.add(startBtnWrapper); @@ -283,13 +287,7 @@ private JPanel buildResultsCard() { JPanel panel = new JPanel(new BorderLayout(8, 8)); panel.setBorder(new EmptyBorder(12, 12, 12, 12)); - JPanel headerPanel = new JPanel(new BorderLayout()); - JLabel titleLabel = new JLabel("Batch Mode — Results"); - titleLabel.setFont(titleLabel.getFont().deriveFont(Font.BOLD, 16f)); - headerPanel.add(titleLabel, BorderLayout.WEST); - durationLabel = new JLabel(""); - headerPanel.add(durationLabel, BorderLayout.EAST); - panel.add(headerPanel, BorderLayout.NORTH); + chartContainer = new JPanel(new BorderLayout()); chartContainer.setPreferredSize(new Dimension(600, 280)); @@ -317,6 +315,10 @@ public void mouseClicked(java.awt.event.MouseEvent e) { cm.getColumn(3).setPreferredWidth(80); // Read MB/s cm.getColumn(4).setPreferredWidth(70); // Latency (ms) cm.getColumn(5).setPreferredWidth(80); // Status + cm.getColumn(2).setCellRenderer(new RightTableCellRenderer()); + cm.getColumn(3).setCellRenderer(new RightTableCellRenderer()); + cm.getColumn(4).setCellRenderer(new RightTableCellRenderer()); + cm.getColumn(5).setCellRenderer(new CenterTableCellRenderer()); JScrollPane tableScroll = new JScrollPane(summaryTable); tableScroll.setPreferredSize(new Dimension(600, 120)); @@ -327,10 +329,13 @@ public void mouseClicked(java.awt.event.MouseEvent e) { panel.add(splitPane, BorderLayout.CENTER); - JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); - JButton newBatchBtn = new JButton("New Batch"); - newBatchBtn.addActionListener(e -> resetToSetup()); - buttonPanel.add(newBatchBtn); + JPanel buttonPanel = new JPanel(new BorderLayout()); + durationLabel = new JLabel(""); + buttonPanel.add(durationLabel, BorderLayout.WEST); + newBatchButton = new JButton("New Batch"); + newBatchButton.putClientProperty("FlatLaf.style", org.metricus.jdm.ui.ButtonStyles.DEFAULT_START); + newBatchButton.addActionListener(e -> resetToSetup()); + buttonPanel.add(newBatchButton, BorderLayout.EAST); panel.add(buttonPanel, BorderLayout.SOUTH); return panel; @@ -398,10 +403,7 @@ public void showBatchFromHistory(UUID batchId) { Duration dur = Duration.between(first, last); durationText = String.format("Duration: %dm %ds", dur.toMinutes(), dur.toSecondsPart()); } - String profileNames = profiles.stream() - .map(p -> p != null ? p.getName() : "—") - .collect(Collectors.joining(", ")); - durationLabel.setText("Profiles: " + profileNames + " | " + durationText); + durationLabel.setText(durationText); cardLayout.show(cardPanel, CARD_RESULTS); } @@ -442,6 +444,11 @@ public void refreshChartTheme() { } } + public void applyStartButtonStyle(String style) { + if (startBatchButton != null) startBatchButton.putClientProperty("FlatLaf.style", style); + if (newBatchButton != null) newBatchButton.putClientProperty("FlatLaf.style", style); + } + // ── Drive Population ──────────────────────────────────────────────────── private void populateDrives() { @@ -666,12 +673,12 @@ private void showResults(BatchResult result) { Duration dur = result.getTotalDuration(); long mins = dur.toMinutes(); long secs = dur.toSecondsPart(); - String profileNames = String.join(", ", result.getProfiles().stream().map(BenchmarkProfile::getName).toList()); - String durationText = String.format("Profiles: %s | Duration: %dm %ds", profileNames, mins, secs); + String durationText = String.format("Duration: %dm %ds", mins, secs); durationLabel.setText(durationText); cardLayout.show(cardPanel, CARD_RESULTS); - App.msg("Batch complete — " + durationText); + String profileNames = String.join(", ", result.getProfiles().stream().map(BenchmarkProfile::getName).toList()); + App.msg("Batch complete — " + profileNames + " | " + durationText); } private void buildChart(List profiles, List successful) { @@ -760,12 +767,12 @@ private void buildSummaryTable(List results) { for (BenchmarkOperation op : rr.benchmark().getOperations()) { switch (op.ioMode) { case WRITE -> { - writeBw = DF.format(op.bwAvg); - if (latency.equals("—")) latency = DF.format(op.accAvg); + writeBw = DF2.format(op.bwAvg); + if (latency.equals("—")) latency = DF2.format(op.accAvg); } case READ -> { - readBw = DF.format(op.bwAvg); - latency = DF.format(op.accAvg); + readBw = DF2.format(op.bwAvg); + latency = DF2.format(op.accAvg); } } } diff --git a/jdm-core/src/main/java/jdiskmark/BatchWorker.java b/jdm-core/src/main/java/jdiskmark/BatchWorker.java index 37be86e..ca27eb9 100644 --- a/jdm-core/src/main/java/jdiskmark/BatchWorker.java +++ b/jdm-core/src/main/java/jdiskmark/BatchWorker.java @@ -117,14 +117,14 @@ private BatchResult.RunResult runSingle(int runIndex, UUID batchId, File drive, } dataDir.mkdirs(); - if (!DriveChecker.validateTargetDirectory(drive, false)) { + if (!DriveChecker.validateTargetDirectory(App.locationDir, false)) { return new BatchResult.RunResult(drive, driveModel, profile, null, isRetry ? BatchResult.DriveStatus.RETRIED_THEN_SKIPPED : BatchResult.DriveStatus.SKIPPED, "Target directory validation failed"); } - if (!DriveChecker.checkDiskSpace(drive)) { + if (!DriveChecker.checkDiskSpace(App.locationDir)) { return new BatchResult.RunResult(drive, driveModel, profile, null, isRetry ? BatchResult.DriveStatus.RETRIED_THEN_SKIPPED : BatchResult.DriveStatus.SKIPPED, diff --git a/jdm-core/src/main/java/jdiskmark/DriveChecker.java b/jdm-core/src/main/java/jdiskmark/DriveChecker.java index 39cd99b..892e5a6 100644 --- a/jdm-core/src/main/java/jdiskmark/DriveChecker.java +++ b/jdm-core/src/main/java/jdiskmark/DriveChecker.java @@ -106,6 +106,30 @@ public static boolean checkDiskSpace(File locationDir) { return false; } + /** + * Resolves a drive root (mount point) to a user-writable location suitable + * for benchmark data. On Linux the root filesystem ({@code /}) is not + * writable by normal users, so when the drive root matches the + * {@code user.home} filesystem root we return the home directory instead. + * + * @param root the drive mount point + * @return a writable directory on that drive, or {@code null} if none found + */ + public static File resolveLocationForRoot(File root) { + File home = new File(System.getProperty("user.home", "")); + if (home.exists()) { + java.nio.file.Path homeRoot = home.toPath().getRoot(); + if (homeRoot != null && homeRoot.equals(root.toPath())) { + File candidate = new File(home, App.DATADIRNAME); + if (candidate.exists() ? candidate.canWrite() : home.canWrite()) { + return home; + } + } + } + if (root.canRead() && root.canWrite()) return root; + return null; + } + private static String formatBytes(long bytes) { if (bytes >= App.GIGABYTE) { return String.format("%.1f GB", bytes / (double) App.GIGABYTE); diff --git a/jdm-core/src/main/java/jdiskmark/DrivePanel.java b/jdm-core/src/main/java/jdiskmark/DrivePanel.java index 9c591ac..89e2946 100644 --- a/jdm-core/src/main/java/jdiskmark/DrivePanel.java +++ b/jdm-core/src/main/java/jdiskmark/DrivePanel.java @@ -423,18 +423,7 @@ private void applySelectedDrive() { } private static File resolveLocationForRoot(File root) { - File home = new File(System.getProperty("user.home", "")); - if (home.exists()) { - java.nio.file.Path homeRoot = home.toPath().getRoot(); - if (homeRoot != null && homeRoot.equals(root.toPath())) { - File candidate = new File(home, App.DATADIRNAME); - if (candidate.exists() ? candidate.canWrite() : home.canWrite()) { - return home; - } - } - } - if (root.canRead() && root.canWrite()) return root; - return null; + return DriveChecker.resolveLocationForRoot(root); } /** diff --git a/jdm-core/src/main/java/jdiskmark/Gui.java b/jdm-core/src/main/java/jdiskmark/Gui.java index 95224ce..af167eb 100644 --- a/jdm-core/src/main/java/jdiskmark/Gui.java +++ b/jdm-core/src/main/java/jdiskmark/Gui.java @@ -416,6 +416,9 @@ public static void applyStartButtonStyle(Theme t) { if (smartPanel != null) { smartPanel.runButton.putClientProperty("FlatLaf.style", style.replace("font: bold ", "font: ")); } + if (batchPanel != null) { + batchPanel.applyStartButtonStyle(style); + } } /** diff --git a/pom.xml b/pom.xml index a2262a2..f899358 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ - 0.9.0-rc1 + 0.9.0-rc2 UTF-8 25 jdiskmark.App From da70ba424c90f850602e9675fc6a08dd0ac9b43c Mon Sep 17 00:00:00 2001 From: James Mark Chan Date: Tue, 8 Sep 2026 23:22:01 -0700 Subject: [PATCH 3/7] #224 adjust focus text for trick or treat --- .../main/java/org/metricus/jdm/ui/theme/TrickOrTreatTheme.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jdm-core/src/main/java/org/metricus/jdm/ui/theme/TrickOrTreatTheme.java b/jdm-core/src/main/java/org/metricus/jdm/ui/theme/TrickOrTreatTheme.java index 7002028..65ad98a 100644 --- a/jdm-core/src/main/java/org/metricus/jdm/ui/theme/TrickOrTreatTheme.java +++ b/jdm-core/src/main/java/org/metricus/jdm/ui/theme/TrickOrTreatTheme.java @@ -94,7 +94,7 @@ public String startButtonStyle() { "#D86413", "#A44606", "#F47B2088", - "#ffffff" + "#1a1a1a" ); } From 5b0c7d46706f017b2784dbe8b42f8ed38bbc0044 Mon Sep 17 00:00:00 2001 From: James Mark Chan Date: Tue, 8 Sep 2026 23:33:37 -0700 Subject: [PATCH 4/7] Fix formatting of JDK installation instructions Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1a8abc7..008ba18 100644 --- a/README.md +++ b/README.md @@ -274,7 +274,7 @@ IOPS: 28892857 JDiskMark is developed with [NetBeans 25](https://netbeans.apache.org/front/main/download/) and [Java 25](https://www.oracle.com/java/technologies/downloads/). -on ubuntu desktop jdk can be installed with: +On Ubuntu Desktop, the JDK can be installed with: `sudo apt update && sudo apt install -y openjdk-25-jdk` From 603b6a623132abc0fb2fc464ee48c6858db3f8f7 Mon Sep 17 00:00:00 2001 From: James Mark Chan Date: Tue, 8 Sep 2026 23:33:50 -0700 Subject: [PATCH 5/7] Remove unused import in BatchPanel.java Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- jdm-core/src/main/java/jdiskmark/BatchPanel.java | 1 - 1 file changed, 1 deletion(-) diff --git a/jdm-core/src/main/java/jdiskmark/BatchPanel.java b/jdm-core/src/main/java/jdiskmark/BatchPanel.java index 2453af0..429b2e3 100644 --- a/jdm-core/src/main/java/jdiskmark/BatchPanel.java +++ b/jdm-core/src/main/java/jdiskmark/BatchPanel.java @@ -29,7 +29,6 @@ import javax.swing.JSplitPane; import javax.swing.JTable; import javax.swing.SpinnerNumberModel; -import javax.swing.SwingConstants; import javax.swing.SwingWorker; import javax.swing.border.EmptyBorder; import javax.swing.table.DefaultTableModel; From 0aa6e6d67f694308e104da0b650049725d3f7e02 Mon Sep 17 00:00:00 2001 From: James Mark Chan Date: Tue, 8 Sep 2026 23:34:11 -0700 Subject: [PATCH 6/7] Clarify drive root resolution in Javadoc comments Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- jdm-core/src/main/java/jdiskmark/DriveChecker.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/jdm-core/src/main/java/jdiskmark/DriveChecker.java b/jdm-core/src/main/java/jdiskmark/DriveChecker.java index 892e5a6..ca26bc3 100644 --- a/jdm-core/src/main/java/jdiskmark/DriveChecker.java +++ b/jdm-core/src/main/java/jdiskmark/DriveChecker.java @@ -109,9 +109,9 @@ public static boolean checkDiskSpace(File locationDir) { /** * Resolves a drive root (mount point) to a user-writable location suitable * for benchmark data. On Linux the root filesystem ({@code /}) is not - * writable by normal users, so when the drive root matches the - * {@code user.home} filesystem root we return the home directory instead. - * + * writable by normal users, so when {@code root} equals the root component + * of {@code user.home} (e.g., {@code /} or {@code C:\\}) we return the home + * directory instead. * @param root the drive mount point * @return a writable directory on that drive, or {@code null} if none found */ From bb12fa4bf1a7f255866a44f44aa41df1e0ab66e8 Mon Sep 17 00:00:00 2001 From: James Mark Chan Date: Wed, 9 Sep 2026 00:03:34 -0700 Subject: [PATCH 7/7] #225 fix cancel focus style, sel one drive, fix location --- jdm-core/src/main/java/jdiskmark/BatchPanel.java | 7 +++++++ jdm-core/src/main/java/jdiskmark/BatchWorker.java | 5 ++--- jdm-core/src/main/java/jdiskmark/Gui.java | 10 ++++++++-- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/jdm-core/src/main/java/jdiskmark/BatchPanel.java b/jdm-core/src/main/java/jdiskmark/BatchPanel.java index 429b2e3..652b93f 100644 --- a/jdm-core/src/main/java/jdiskmark/BatchPanel.java +++ b/jdm-core/src/main/java/jdiskmark/BatchPanel.java @@ -448,6 +448,10 @@ public void applyStartButtonStyle(String style) { if (newBatchButton != null) newBatchButton.putClientProperty("FlatLaf.style", style); } + public void applyCancelButtonStyle(String style) { + if (cancelButton != null) cancelButton.putClientProperty("FlatLaf.style", style); + } + // ── Drive Population ──────────────────────────────────────────────────── private void populateDrives() { @@ -492,6 +496,9 @@ protected void done() { driveLabels.add(entry.model()); driveListPanel.add(cb); } + if (!driveCheckBoxes.isEmpty()) { + driveCheckBoxes.getFirst().setSelected(true); + } driveListPanel.revalidate(); driveListPanel.repaint(); diff --git a/jdm-core/src/main/java/jdiskmark/BatchWorker.java b/jdm-core/src/main/java/jdiskmark/BatchWorker.java index ca27eb9..cd55201 100644 --- a/jdm-core/src/main/java/jdiskmark/BatchWorker.java +++ b/jdm-core/src/main/java/jdiskmark/BatchWorker.java @@ -193,9 +193,8 @@ public void attemptCacheDrop() { : BatchResult.DriveStatus.SKIPPED, err); } finally { - File dataDir = new File(drive.getAbsolutePath() + File.separator + App.DATADIRNAME); - if (dataDir.exists()) { - Util.deleteDirectory(dataDir); + if (App.dataDir != null && App.dataDir.exists()) { + Util.deleteDirectory(App.dataDir); } } } diff --git a/jdm-core/src/main/java/jdiskmark/Gui.java b/jdm-core/src/main/java/jdiskmark/Gui.java index af167eb..e13bd2a 100644 --- a/jdm-core/src/main/java/jdiskmark/Gui.java +++ b/jdm-core/src/main/java/jdiskmark/Gui.java @@ -399,6 +399,7 @@ public static void applyTheme(Theme t) { mainFrame.getGraphPaletteMenu().setAllItemsEnabled(!t.hasLinkedPalette()); } applyStartButtonStyle(t); + applyCancelButtonStyle(t); applyIconToWindow(t); if (batchPanel != null) batchPanel.refreshChartTheme(); } @@ -444,10 +445,14 @@ private static void applyIconToWindow(Theme t) { * a theme-coherent "stop" colour; the default falls back to amber. */ public static void applyCancelButtonStyle(Theme t) { - if (controlPanel == null) return; String style = t.definition().cancelButtonStyle(); if (style == null) style = ButtonStyles.CANCEL; - controlPanel.startButton.putClientProperty("FlatLaf.style", style); + if (controlPanel != null && "Cancel".equals(controlPanel.startButton.getText())) { + controlPanel.startButton.putClientProperty("FlatLaf.style", style); + } + if (batchPanel != null) { + batchPanel.applyCancelButtonStyle(style); + } } @@ -545,6 +550,7 @@ public static void init() { // panel exists. Without this, the button always opens GitHub-green // because BenchmarkControlPanel seeds DEFAULT_START in its constructor. applyStartButtonStyle(theme); + applyCancelButtonStyle(theme); // On macOS, replace the default system-provided About dialog (which shows // the Java runtime info) with our own branded dialog.