Compare commits

...

3 Commits

5 changed files with 458 additions and 26 deletions
@@ -416,5 +416,25 @@
</changes>
<url>https://github.com/bancika/diy-layout-creator/releases</url>
</org.diylc.appframework.update.Version>
<org.diylc.appframework.update.Version>
<versionNumber>
<major>5</major>
<minor>8</minor>
<build>0</build>
</versionNumber>
<releaseDate>2025-12-21 00:00:00.000 CET</releaseDate>
<name></name>
<changes>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Improved sensitivity of 'Multimeter Probe' that required pixel-level precision to detect connectivity; improved rendering of the probe in outline mode</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>Fixed underside rendering and mirroring position issues that affected only SPDT 'Mini Toggle Switch' types</description>
</org.diylc.appframework.update.Change>
</changes>
<url>https://github.com/bancika/diy-layout-creator/releases</url>
</org.diylc.appframework.update.Version>
</a>
</java.util.Arrays_-ArrayList>
@@ -28,21 +28,20 @@ import java.awt.geom.Path2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import org.diylc.appframework.miscutils.ConfigurationManager;
import org.diylc.common.IPlugInPort;
import org.diylc.common.ObjectCache;
import org.diylc.common.Orientation45;
import org.diylc.components.AbstractTransparentComponent;
import org.diylc.components.transform.SimpleComponentTransformer;
import org.diylc.core.ComponentState;
import org.diylc.core.IDIYComponent;
import org.diylc.core.IDrawingObserver;
import org.diylc.core.Project;
import org.diylc.core.VisibilityPolicy;
import org.diylc.core.*;
import org.diylc.core.annotations.BomPolicy;
import org.diylc.core.annotations.ComponentDescriptor;
import org.diylc.core.annotations.EditableProperty;
import org.diylc.core.annotations.KeywordPolicy;
import org.diylc.core.measures.Size;
import org.diylc.core.measures.SizeUnit;
import org.diylc.utils.Constants;
@ComponentDescriptor(name = "Multimeter Probe", category = "Connectivity",
author = "Branislav Stojkovic", description = "Multimeter Probe", instanceNamePrefix = "Probe",
@@ -59,6 +58,7 @@ public class MultimeterProbe extends AbstractTransparentComponent<Color> {
public static Size HANDLE_DIAMETER = new Size(0.2d, SizeUnit.in);
public static Size HANDLE_LENGTH = new Size(0.4d, SizeUnit.in);
public static Color HANDLE_COLOR = Color.red;
public static Size SENSITIVITY = new Size(0.05d, SizeUnit.in);
private Point2D.Double point = new Point2D.Double(0, 0);
private Color handleColor = HANDLE_COLOR;
@@ -77,27 +77,49 @@ public class MultimeterProbe extends AbstractTransparentComponent<Color> {
Shape[] shapes = getBody();
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1f));
if (!outlineMode) {
Composite oldComposite = g2d.getComposite();
if (alpha < MAX_ALPHA) {
g2d.setComposite(
AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1f * alpha / MAX_ALPHA));
}
g2d.setColor(PROBE_COLOR);
g2d.fill(shapes[0]);
g2d.setColor(handleColor);
g2d.fill(shapes[1]);
g2d.setComposite(oldComposite);
// draw an invisible conductive circle around the point to increase sensitivity
g2d.setColor(Constants.TRANSPARENT_COLOR);
drawingObserver.startTrackingContinuityArea(true);
double sensitivity = SENSITIVITY.convertToPixels();
g2d.fillOval((int) (point.getX() - sensitivity), (int) (point.getY() - sensitivity),
(int) (sensitivity * 2), (int) (sensitivity * 2));
drawingObserver.stopTrackingContinuityArea();
Composite oldComposite = g2d.getComposite();
if (alpha < MAX_ALPHA) {
g2d.setComposite(
AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1f * alpha / MAX_ALPHA));
}
g2d.setColor(
componentState == ComponentState.SELECTED || componentState == ComponentState.DRAGGING
? SELECTION_COLOR
: handleColor.darker());
g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : PROBE_COLOR);
g2d.fill(shapes[0]);
g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : handleColor);
g2d.fill(shapes[1]);
g2d.setComposite(oldComposite);
drawingObserver.stopTracking();
Theme theme = (Theme) ConfigurationManager.getInstance().readObject(IPlugInPort.THEME_KEY,
Constants.DEFAULT_THEME);
Color handleBorderColor;
if (outlineMode) {
handleBorderColor = theme.getOutlineColor();
} else {
handleBorderColor = componentState == ComponentState.SELECTED || componentState == ComponentState.DRAGGING
? SELECTION_COLOR
: handleColor.darker();
}
g2d.setColor(handleBorderColor);
g2d.draw(shapes[1]);
g2d.setColor(
componentState == ComponentState.SELECTED || componentState == ComponentState.DRAGGING
? SELECTION_COLOR
: PROBE_COLOR.darker());
Color probeBorderColor;
if (outlineMode) {
probeBorderColor = theme.getOutlineColor();
} else {
probeBorderColor = componentState == ComponentState.SELECTED || componentState == ComponentState.DRAGGING
? SELECTION_COLOR
: PROBE_COLOR.darker();
}
g2d.setColor(probeBorderColor);
g2d.draw(shapes[0]);
}
@@ -27,6 +27,7 @@ import java.awt.geom.Point2D;
import org.diylc.common.IComponentTransformer;
import org.diylc.common.OrientationHV;
import org.diylc.components.electromechanical.MiniToggleSwitch;
import org.diylc.components.electromechanical.ToggleSwitchType;
import org.diylc.core.IDIYComponent;
public class MiniToggleSwitchTransformer implements IComponentTransformer {
@@ -107,7 +108,8 @@ public class MiniToggleSwitchTransformer implements IComponentTransformer {
MiniToggleSwitch sw = (MiniToggleSwitch) component;
if (direction == IComponentTransformer.HORIZONTAL) {
double dx = 2 * (center.getX() - sw.getControlPoint(1).getX());
double dx = 2 * (center.getX() -
(sw.getControlPoint(0).getX() + sw.getControlPoint(sw.getControlPointCount() - 1).getX()) / 2);
double dy = 0;
for (int i = 0; i < sw.getControlPointCount(); i++) {
@@ -116,7 +118,8 @@ public class MiniToggleSwitchTransformer implements IComponentTransformer {
}
} else {
double dx = 0;
double dy = 2 * (center.getY() - sw.getControlPoint(1).getY());
double dy = 2 * (center.getY() -
(sw.getControlPoint(0).getY() + sw.getControlPoint(sw.getControlPointCount() - 1).getY()) / 2);
for (int i = 0; i < sw.getControlPointCount(); i++) {
Point2D p = sw.getControlPoint(i);
@@ -33,6 +33,7 @@ import org.diylc.appframework.miscutils.PropertyInjector;
import org.diylc.appframework.miscutils.Utils;
import org.diylc.swingframework.export.DrawingExporter;
import org.diylc.swingframework.fonts.FontOptimizer;
import org.diylc.utils.ThemeUtils;
import com.thoughtworks.xstream.XStream;
import org.diylc.common.IPlugInPort;
import org.diylc.core.IView;
@@ -71,6 +72,9 @@ public class DIYLCStarter {
}
public static void runDIYLC(String[] args) {
// Apply OS dark theme settings before creating any UI components
ThemeUtils.applyOSDarkTheme();
// Initialize splash screen
DIYLCSplash splash = null;
Exception splashException = null;
@@ -137,6 +141,8 @@ public class DIYLCStarter {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
// Apply dark theme colors if dark mode is detected
ThemeUtils.applyDarkThemeColors();
} catch (Exception e) {
LOG.error("Could not set Look&Feel", e);
}
@@ -0,0 +1,381 @@
/*
*
* DIY Layout Creator (DIYLC). Copyright (c) 2009-2025 held jointly by the individual authors.
*
* This file is part of DIYLC.
*
* DIYLC is free software: you can redistribute it and/or modify it under the terms of the GNU
* General Public License as published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* DIYLC is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with DIYLC. If not, see
* <http://www.gnu.org/licenses/>.
*/
package org.diylc.utils;
import java.awt.Color;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import javax.swing.UIManager;
import org.apache.log4j.Logger;
import org.diylc.appframework.miscutils.Utils;
/**
* Utility class for detecting and applying OS dark theme settings.
*
* Note: Modern Java (11+) with system Look and Feel should automatically
* respect OS dark theme settings, but this utility provides explicit detection
* and ensures compatibility with older Java versions.
*
* @author Branislav Stojkovic
*/
public class ThemeUtils {
private static final Logger LOG = Logger.getLogger(ThemeUtils.class);
/**
* Detects and applies OS dark theme settings.
* This must be called before creating any AWT/Swing components.
*/
public static void applyOSDarkTheme() {
try {
if (Utils.isMac()) {
applyMacOSDarkTheme();
} else if (Utils.isWindows()) {
detectWindowsDarkTheme();
} else {
detectLinuxDarkTheme();
}
} catch (Exception e) {
LOG.warn("Error detecting OS dark theme: " + e.getMessage(), e);
}
}
/**
* Applies dark theme colors to UIManager after the Look and Feel is set.
* This should be called after UIManager.setLookAndFeel().
*
* @return true if dark mode was detected and colors were applied, false otherwise
*/
public static boolean applyDarkThemeColors() {
try {
if (Utils.isMac()) {
return applyMacOSDarkThemeColors();
} else if (Utils.isWindows()) {
return applyWindowsDarkThemeColors();
} else {
return applyLinuxDarkThemeColors();
}
} catch (Exception e) {
LOG.warn("Error applying dark theme colors: " + e.getMessage(), e);
return false;
}
}
/**
* Detects and applies macOS dark theme settings.
*/
private static void applyMacOSDarkTheme() {
// macOS: Check if dark mode is enabled
// Modern Java (11+) should automatically detect this, but we'll check explicitly
String appearance = System.getProperty("apple.awt.application.appearance");
if (appearance == null) {
// Try to detect dark mode via system command (for older Java versions)
try {
Process process = Runtime.getRuntime().exec(
new String[]{"defaults", "read", "-g", "AppleInterfaceStyle"});
BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()));
String result = reader.readLine();
if ("Dark".equals(result)) {
// Note: Setting this property after JVM start may not always work,
// but it's worth trying for older Java versions
System.setProperty("apple.awt.application.appearance", "NSAppearanceNameDarkAqua");
LOG.info("Detected macOS dark mode - attempting to apply dark theme");
} else {
LOG.debug("macOS light mode detected");
}
} catch (Exception e) {
// If detection fails, that's okay - modern Java will handle it automatically
LOG.debug("Could not detect macOS theme via defaults command: " + e.getMessage());
}
} else if ("NSAppearanceNameDarkAqua".equals(appearance)) {
LOG.info("macOS dark mode already detected by JVM");
} else {
LOG.debug("macOS appearance: " + appearance);
}
}
/**
* Detects Windows dark theme settings.
*/
private static void detectWindowsDarkTheme() {
// Windows: Try to detect dark theme via registry
try {
Process process = Runtime.getRuntime().exec(
new String[]{"reg", "query",
"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
"/v", "AppsUseLightTheme"});
BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
if (line.contains("AppsUseLightTheme")) {
// If value is 0, dark theme is enabled
if (line.contains("0x0") || line.endsWith("0")) {
// For Windows, the system Look and Feel should automatically respect dark mode
// in Java 11+, but we can log it for debugging
LOG.info("Detected Windows dark mode");
}
break;
}
}
} catch (Exception e) {
LOG.debug("Could not detect Windows theme via registry: " + e.getMessage());
}
}
/**
* Detects Linux dark theme settings.
*/
private static void detectLinuxDarkTheme() {
// Linux: Check GTK theme or environment variables
try {
String gtkTheme = System.getenv("GTK_THEME");
if (gtkTheme != null && (gtkTheme.toLowerCase().contains("dark") ||
gtkTheme.toLowerCase().contains("Dark"))) {
LOG.info("Detected Linux dark theme: " + gtkTheme);
}
// Also check COLORFGBG environment variable (used by some terminals/desktops)
String colorFgBg = System.getenv("COLORFGBG");
if (colorFgBg != null) {
// Format is typically "15;0" where second number < 8 indicates dark background
String[] parts = colorFgBg.split(";");
if (parts.length > 1) {
try {
int bgColor = Integer.parseInt(parts[parts.length - 1]);
if (bgColor < 8) {
LOG.info("Detected Linux dark background via COLORFGBG");
}
} catch (NumberFormatException e) {
// Ignore
}
}
}
} catch (Exception e) {
LOG.debug("Could not detect Linux theme: " + e.getMessage());
}
}
/**
* Applies macOS dark theme colors to UIManager.
* @return true if dark mode is active
*/
private static boolean applyMacOSDarkThemeColors() {
boolean isDarkMode = false;
// Check if dark mode is active
String appearance = System.getProperty("apple.awt.application.appearance");
if (appearance == null) {
try {
Process process = Runtime.getRuntime().exec(
new String[]{"defaults", "read", "-g", "AppleInterfaceStyle"});
BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()));
String result = reader.readLine();
isDarkMode = "Dark".equals(result);
} catch (Exception e) {
LOG.debug("Could not detect macOS theme: " + e.getMessage());
// Try to infer from current UI colors
Color menuBarBg = UIManager.getColor("MenuBar.background");
if (menuBarBg != null && isDarkColor(menuBarBg)) {
isDarkMode = true;
}
}
} else {
isDarkMode = "NSAppearanceNameDarkAqua".equals(appearance);
}
if (isDarkMode) {
LOG.info("Applying macOS dark theme colors");
applyDarkColors();
return true;
}
return false;
}
/**
* Applies Windows dark theme colors to UIManager.
* @return true if dark mode is active
*/
private static boolean applyWindowsDarkThemeColors() {
boolean isDarkMode = false;
try {
Process process = Runtime.getRuntime().exec(
new String[]{"reg", "query",
"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
"/v", "AppsUseLightTheme"});
BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
if (line.contains("AppsUseLightTheme")) {
// If value is 0, dark theme is enabled
if (line.contains("0x0") || line.endsWith("0")) {
isDarkMode = true;
}
break;
}
}
} catch (Exception e) {
LOG.debug("Could not detect Windows theme: " + e.getMessage());
// Try to infer from current UI colors
Color menuBarBg = UIManager.getColor("MenuBar.background");
if (menuBarBg != null && isDarkColor(menuBarBg)) {
isDarkMode = true;
}
}
if (isDarkMode) {
LOG.info("Applying Windows dark theme colors");
applyDarkColors();
return true;
}
return false;
}
/**
* Applies Linux dark theme colors to UIManager.
* @return true if dark mode is active
*/
private static boolean applyLinuxDarkThemeColors() {
boolean isDarkMode = false;
try {
String gtkTheme = System.getenv("GTK_THEME");
if (gtkTheme != null && (gtkTheme.toLowerCase().contains("dark") ||
gtkTheme.toLowerCase().contains("Dark"))) {
isDarkMode = true;
}
// Also check COLORFGBG
if (!isDarkMode) {
String colorFgBg = System.getenv("COLORFGBG");
if (colorFgBg != null) {
String[] parts = colorFgBg.split(";");
if (parts.length > 1) {
try {
int bgColor = Integer.parseInt(parts[parts.length - 1]);
if (bgColor < 8) {
isDarkMode = true;
}
} catch (NumberFormatException e) {
// Ignore
}
}
}
}
// Try to infer from current UI colors
if (!isDarkMode) {
Color menuBarBg = UIManager.getColor("MenuBar.background");
if (menuBarBg != null && isDarkColor(menuBarBg)) {
isDarkMode = true;
}
}
} catch (Exception e) {
LOG.debug("Could not detect Linux theme: " + e.getMessage());
}
if (isDarkMode) {
LOG.info("Applying Linux dark theme colors");
applyDarkColors();
return true;
}
return false;
}
/**
* Applies dark theme colors to common UI components.
* Only overrides colors that would cause visibility issues (e.g., dark text on dark background).
*/
private static void applyDarkColors() {
// Menu bar and menu colors - fix text colors if background is dark
Color menuBarBg = UIManager.getColor("MenuBar.background");
Color menuBarFg = UIManager.getColor("MenuBar.foreground");
if (menuBarBg != null && isDarkColor(menuBarBg)) {
// Background is dark, ensure text is light
if (menuBarFg == null || isDarkColor(menuBarFg)) {
UIManager.put("MenuBar.foreground", Color.WHITE);
}
// Fix menu colors
Color menuBg = UIManager.getColor("Menu.background");
Color menuFg = UIManager.getColor("Menu.foreground");
if (menuBg == null || isDarkColor(menuBg)) {
UIManager.put("Menu.background", new Color(50, 50, 50));
}
if (menuFg == null || (menuBg != null && isDarkColor(menuBg) && isDarkColor(menuFg))) {
UIManager.put("Menu.foreground", Color.WHITE);
}
UIManager.put("Menu.selectionBackground", new Color(60, 60, 60));
UIManager.put("Menu.selectionForeground", Color.WHITE);
// Fix menu item colors
Color menuItemBg = UIManager.getColor("MenuItem.background");
Color menuItemFg = UIManager.getColor("MenuItem.foreground");
if (menuItemBg == null || isDarkColor(menuItemBg)) {
UIManager.put("MenuItem.background", new Color(50, 50, 50));
}
if (menuItemFg == null || (menuItemBg != null && isDarkColor(menuItemBg) && isDarkColor(menuItemFg))) {
UIManager.put("MenuItem.foreground", Color.WHITE);
}
UIManager.put("MenuItem.selectionBackground", new Color(60, 60, 60));
UIManager.put("MenuItem.selectionForeground", Color.WHITE);
}
// Popup menu colors
Color popupBg = UIManager.getColor("PopupMenu.background");
if (popupBg == null || isDarkColor(popupBg)) {
UIManager.put("PopupMenu.background", new Color(50, 50, 50));
UIManager.put("PopupMenu.foreground", Color.WHITE);
UIManager.put("PopupMenu.borderColor", new Color(70, 70, 70));
}
// Checkbox and radio button menu items
Color checkBoxMenuItemBg = UIManager.getColor("CheckBoxMenuItem.background");
if (checkBoxMenuItemBg == null || isDarkColor(checkBoxMenuItemBg)) {
UIManager.put("CheckBoxMenuItem.background", new Color(50, 50, 50));
UIManager.put("CheckBoxMenuItem.foreground", Color.WHITE);
UIManager.put("CheckBoxMenuItem.selectionBackground", new Color(60, 60, 60));
UIManager.put("CheckBoxMenuItem.selectionForeground", Color.WHITE);
}
Color radioMenuItemBg = UIManager.getColor("RadioButtonMenuItem.background");
if (radioMenuItemBg == null || isDarkColor(radioMenuItemBg)) {
UIManager.put("RadioButtonMenuItem.background", new Color(50, 50, 50));
UIManager.put("RadioButtonMenuItem.foreground", Color.WHITE);
UIManager.put("RadioButtonMenuItem.selectionBackground", new Color(60, 60, 60));
UIManager.put("RadioButtonMenuItem.selectionForeground", Color.WHITE);
}
}
/**
* Checks if a color is dark (suitable for dark theme).
*/
private static boolean isDarkColor(Color color) {
if (color == null) {
return false;
}
// Calculate luminance
double luminance = (0.299 * color.getRed() + 0.587 * color.getGreen() + 0.114 * color.getBlue()) / 255;
return luminance < 0.5;
}
}