Merge branch 'master' into controllers

This commit is contained in:
Bane Stojkovic
2026-08-16 23:39:25 +02:00
9 changed files with 382 additions and 17 deletions
+7 -6
View File
@@ -4,7 +4,7 @@ DIY Layout Creator (DIYLC) is a specialized software tool designed for hobbyists
It provides an intuitive, drag-and-drop interface for placing components, arranging connections, and visualizing circuit designs before assembly. With a built-in component library and customization options, it allows users to design layouts with precision and clarity.
🎸 **Perfect for pedal builders and tube amp enthusiasts!** DIYLC is particularly useful for designing, analyzing, and debugging guitar wiring diagrams, helping musicians and builders plan and troubleshoot custom wiring configurations for pickups, switches, and controls. Its versatility makes it an ideal solution for DIY electronics projects, prototyping, and documentation.
🎸 **Perfect for guitar pedal builders, guitar, synth and Hi-Fi enthusiasts!** DIYLC is particularly useful for designing, analyzing, and debugging guitar wiring diagrams, helping musicians and builders plan and troubleshoot custom wiring configurations for pickups, switches, and controls. Its versatility makes it an ideal solution for DIY electronics projects, prototyping, and documentation.
### 🚀 Quick Links
* **[⬇️ Download the latest release](https://github.com/bancika/diy-layout-creator/releases/latest)** for all platforms.
@@ -37,13 +37,13 @@ The **Official DIYLC Guide (v1.0)** is a comprehensive 174-page eBook that goes
### 🖼️ Gallery & Samples
<p align="center">
<img src="https://diy-fever.com/wordpress/wp-content/gallery/diylc/cache/slo.png-nggid041072-ngg0dyn-450x350-00f0w010c010r110f110r010t010.png" width="48%" alt="Point-to-point tube amplifier">
<img src="https://diy-fever.com/nextgen-image/1073/450x350/34b70d759e46422208d61e376b4633dc" width="48%" alt="Guitar wiring">
<img src="https://diy-fever.com/wordpress/wp-content/gallery/diylc/slo.png" width="48%" alt="Point-to-point tube amplifier">
<img src="https://diy-fever.com/wordpress/wp-content/gallery/diylc/travelcaster.png" width="48%" alt="Guitar wiring">
<br>
<sup><i>Left: Point-to-point tube amplifier &nbsp; | &nbsp; Right: Guitar wiring</i></sup>
<br><br>
<img src="https://diy-fever.com/nextgen-image/1070/450x350/ccb592ec5828b6f5bc75e840438918f1" width="48%" alt="PCB tube amplifier">
<img src="https://diy-fever.com/nextgen-image/1069/450x350/358e43165de40e9c4043726c9efde4a7" width="48%" alt="Perfboard guitar effect">
<img src="https://diy-fever.com/wordpress/wp-content/gallery/diylc/iic.png" width="48%" alt="PCB tube amplifier">
<img src="https://diy-fever.com/wordpress/wp-content/gallery/diylc/split.png" width="48%" alt="Perfboard guitar effect">
<br>
<sup><i>Left: PCB tube amplifier &nbsp; | &nbsp; Right: Perfboard guitar effect</i></sup>
</p>
@@ -51,7 +51,8 @@ The **Official DIYLC Guide (v1.0)** is a comprehensive 174-page eBook that goes
---
### ✨ Key Features
* 💻 **Platform Independence:** Runs on any machine capable of running Java JRE/JDK 21 or newer.
* 🆓 **Free for Life**: A true old-school offline app—the app you download is yours forever and the files you created are yours.
* 💻 **Platform Independence:** Runs locally on any machine capable of running Java JRE/JDK 21 or newer.
* 🖱️ **Intuitive UI:** Easy-to-use interface; most operations can be done seamlessly with a mouse.
* 🎛️ **Versatile Designs:** Draw PCB, perfboard, stripboard, or point-to-point layouts, schematics, guitar wiring diagrams or custom faceplates.
* 🔌 **Highly Extensible:** API allows custom plug-ins and new components to be added easily.
@@ -112,5 +112,10 @@ public enum EventType {
/**
* Signals that the Cloud account is logged out.
*/
CLOUD_LOGGED_OUT;
CLOUD_LOGGED_OUT,
/**
* Called when the mouse hovers over sticky control points (nodes) in tooltip mode.
* Parameters: String text (semicolon-separated component and node names, or null), Point point.
*/
NODE_NAME_HOVER_TOOLTIP;
}
@@ -100,6 +100,8 @@ public interface IPlugInPort extends ISelectionProcessor, IMouseProcessor, IKeyP
public static final String SNAP_TO_COMPONENTS = "Components";
public static final String SNAP_TO_DEFAULT = SNAP_TO_GRID;
public static final String SHOW_NODE_NAME_TOOLTIPS_KEY = "showNodeNameTooltips";
public static final String LOCKED_ALPHA = "lockedAlpha";
public static final String LANGUAGE = "language";
@@ -37,6 +37,7 @@ import java.util.function.Supplier;
import java.util.stream.Collectors;
import javax.swing.JOptionPane;
import org.apache.log4j.Logger;
import org.diylc.appframework.miscutils.IConfigListener;
import org.diylc.appframework.miscutils.IConfigurationManager;
import org.diylc.appframework.miscutils.Utils;
import org.diylc.appframework.simplemq.MessageDispatcher;
@@ -161,6 +162,8 @@ public class Presenter implements IPlugInPort {
private DIYTest test = null;
private OperationMode operationMode;
private String lastHoveredNodeText = null;
public Presenter(IView view, IConfigurationManager<?> configManager) {
this(view, configManager, false);
@@ -191,6 +194,14 @@ public class Presenter implements IPlugInPort {
variantManager.importDefaultVariants();
buildingBlockManager.importDefaultBlocks();
}
configManager.addConfigListener(IPlugInPort.SHOW_NODE_NAME_TOOLTIPS_KEY, new IConfigListener() {
@Override
public void valueChanged(String key, Object value) {
lastHoveredNodeText = null;
messageDispatcher.dispatchMessage(EventType.NODE_NAME_HOVER_TOOLTIP, null, null);
}
});
}
public void installPlugin(Supplier<IPlugIn> plugInSupplier) {
@@ -886,6 +897,45 @@ public class Presenter implements IPlugInPort {
messageDispatcher.dispatchMessage(EventType.AVAILABLE_CTRL_POINTS_CHANGED,
new HashMap<IDIYComponent<?>, Set<Integer>>(components));
}
// Node name hover detection for sticky points
String nodeHoverText = null;
if (!dragInProgress && instantiationManager.getComponentTypeSlot() == null) {
List<String> nodeInfos = new ArrayList<String>();
for (int i = currentProject.getComponents().size() - 1; i >= 0; i--) {
IDIYComponent<?> component = currentProject.getComponents().get(i);
for (int pointIndex = 0; pointIndex < component.getControlPointCount(); pointIndex++) {
try {
if (component.isControlPointSticky(pointIndex)) {
Point2D controlPoint = component.getControlPoint(pointIndex);
if (controlPoint != null && previousScaledPoint.distance(controlPoint) < DrawingManager.CONTROL_POINT_SIZE) {
String nodeName = component.getControlPointNodeName(pointIndex);
if (nodeName != null && !nodeName.trim().isEmpty()) {
String info = "<b>" + component.getName() + "</b>: " + nodeName;
if (!nodeInfos.contains(info)) {
nodeInfos.add(info);
}
}
}
}
} catch (Exception e) {
LOG.warn("Error reading control point for component of type: " + component.getClass().getName());
}
}
}
if (!nodeInfos.isEmpty()) {
nodeHoverText = String.join("; ", nodeInfos);
}
}
if (!Objects.equals(nodeHoverText, lastHoveredNodeText)) {
lastHoveredNodeText = nodeHoverText;
if (nodeHoverText != null && configManager.readBoolean(IPlugInPort.SHOW_NODE_NAME_TOOLTIPS_KEY, true)) {
messageDispatcher.dispatchMessage(EventType.NODE_NAME_HOVER_TOOLTIP, nodeHoverText, point);
} else {
messageDispatcher.dispatchMessage(EventType.NODE_NAME_HOVER_TOOLTIP, null, null);
}
}
}
@Override
@@ -1084,6 +1134,8 @@ public class Presenter implements IPlugInPort {
return;
}
this.dragInProgress = true;
this.lastHoveredNodeText = null;
this.messageDispatcher.dispatchMessage(EventType.NODE_NAME_HOVER_TOOLTIP, null, null);
this.dragAction = dragAction;
this.preDragProject = currentProject.clone();
Point2D scaledPoint = scalePoint(point);
@@ -397,6 +397,10 @@
<releaseDate>2026-08-06 00:00:00.000 CET</releaseDate>
<name></name>
<changes>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>Added 'Node Name Tooltips' feature that can be toggled in the Config menu; when enabled a tooltip is shown next to the cursor with names of component nodes under the cursor</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Significantly improved AI Assistant's knowledge base on DIYLC usage by ingesting most of the material from the eBook</description>
@@ -69,6 +69,7 @@ public class CanvasPlugin implements IPlugIn{
private CanvasPanel canvasPanel;
private ComponentPopupMenu popupMenu;
private ResizeInfoTooltip resizeInfoTooltip;
private NodeNameTooltip nodeNameTooltip;
private boolean resizingInProgress = false;
private IPlugInPort plugInPort;
@@ -196,6 +197,7 @@ public class CanvasPlugin implements IPlugIn{
if (canvasPanel == null) {
canvasPanel = new CanvasPanel(plugInPort, configManager);
resizeInfoTooltip = new ResizeInfoTooltip(canvasPanel, configManager);
nodeNameTooltip = new NodeNameTooltip(canvasPanel, configManager);
canvasPanel.addMouseListener(new MouseAdapter() {
private MouseEvent pressedEvent;
@@ -219,6 +221,9 @@ public class CanvasPlugin implements IPlugIn{
canvasPanel.requestFocus();
canvasPanel.setClickInProgress(true);
pressedEvent = e;
if (nodeNameTooltip != null) {
nodeNameTooltip.hideTooltip();
}
}
@Override
@@ -289,6 +294,10 @@ public class CanvasPlugin implements IPlugIn{
resizeInfoTooltip.hideTooltip();
}
}
if (nodeNameTooltip != null && nodeNameTooltip.isVisible()) {
nodeNameTooltip.updatePosition(e.getPoint());
}
}
});
@@ -469,7 +478,7 @@ public class CanvasPlugin implements IPlugIn{
@Override
public EnumSet<EventType> getSubscribedEventTypes() {
return EnumSet.of(EventType.PROJECT_LOADED, EventType.ZOOM_CHANGED, EventType.REPAINT,
EventType.SCROLL_TO);
EventType.SCROLL_TO, EventType.NODE_NAME_HOVER_TOOLTIP);
}
@SuppressWarnings("incomplete-switch")
@@ -567,6 +576,26 @@ public class CanvasPlugin implements IPlugIn{
(int) (targetRect.getCenterY() - visibleRect2.height / 2), visibleRect2.width,
visibleRect2.height));
break;
case NODE_NAME_HOVER_TOOLTIP:
final String nodeText = params.length > 0 ? (String) params[0] : null;
final Point mousePt = params.length > 1 && params[1] instanceof Point ? (Point) params[1] : null;
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
if (nodeText != null) {
Point pt = mousePt != null ? mousePt : getCanvasPanel().getMousePosition();
if (pt != null && nodeNameTooltip != null) {
nodeNameTooltip.update(nodeText, pt);
}
} else {
if (nodeNameTooltip != null) {
nodeNameTooltip.hideTooltip();
}
}
}
});
break;
}
// }
// });
@@ -0,0 +1,198 @@
/*
*
* 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.swing.plugins.canvas;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.Window;
import javax.swing.BorderFactory;
import javax.swing.JLabel;
import javax.swing.JWindow;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import org.diylc.appframework.miscutils.IConfigurationManager;
/**
* A lightweight tooltip window that displays node/terminal information and tracks the mouse cursor.
*/
public class NodeNameTooltip {
private static final int OFFSET_X = 16;
private static final int OFFSET_Y = 16;
private JWindow tooltipWindow;
private JLabel tooltipLabel;
private Component parentComponent;
private String currentText;
public NodeNameTooltip(Component parentComponent, IConfigurationManager<?> configManager) {
this.parentComponent = parentComponent;
initializeTooltip();
}
private void initializeTooltip() {
Window parentWindow = SwingUtilities.getWindowAncestor(parentComponent);
if (parentWindow == null) {
return;
}
tooltipWindow = new JWindow(parentWindow);
tooltipLabel = new JLabel() {
private static final long serialVersionUID = 1L;
@Override
protected void paintComponent(Graphics g) {
Graphics2D g2d = (Graphics2D) g.create();
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
super.paintComponent(g2d);
g2d.dispose();
}
};
// Use system tooltip font and colors
Font tooltipFont = UIManager.getFont("ToolTip.font");
if (tooltipFont != null) {
tooltipLabel.setFont(tooltipFont);
}
Color tooltipBackground = UIManager.getColor("ToolTip.background");
Color tooltipForeground = UIManager.getColor("ToolTip.foreground");
Color tooltipBorderColor = UIManager.getColor("ToolTip.border");
if (tooltipBorderColor == null) {
tooltipBorderColor = Color.GRAY;
}
tooltipLabel.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createLineBorder(tooltipBorderColor, 1),
BorderFactory.createEmptyBorder(4, 6, 4, 6)));
tooltipLabel.setOpaque(true);
tooltipLabel.setBackground(tooltipBackground != null ? tooltipBackground : new Color(255, 255, 220));
tooltipLabel.setForeground(tooltipForeground != null ? tooltipForeground : Color.BLACK);
tooltipWindow.add(tooltipLabel);
tooltipWindow.setFocusableWindowState(false);
tooltipWindow.setAlwaysOnTop(true);
}
/**
* Updates the tooltip with text and mouse position.
*/
public void update(String text, Point mousePosition) {
this.currentText = text;
if (text == null || text.trim().isEmpty() || mousePosition == null) {
hideTooltip();
return;
}
if (tooltipWindow == null) {
initializeTooltip();
if (tooltipWindow == null) {
return;
}
}
tooltipLabel.setText("<html>" + text + "</html>");
tooltipLabel.setPreferredSize(null);
tooltipWindow.pack();
updateTooltipPosition(mousePosition);
tooltipWindow.setVisible(true);
}
/**
* Updates the tooltip position based on the current mouse position.
*/
public void updatePosition(Point mousePosition) {
if (currentText != null && mousePosition != null
&& tooltipWindow != null && tooltipWindow.isVisible()) {
updateTooltipPosition(mousePosition);
}
}
private void updateTooltipPosition(Point mousePosition) {
if (tooltipWindow == null || parentComponent == null) {
return;
}
// Convert mouse position to screen coordinates
Point screenPoint = new Point(mousePosition);
SwingUtilities.convertPointToScreen(screenPoint, parentComponent);
// Position tooltip 16 pixels down and right from cursor
int x = screenPoint.x + OFFSET_X;
int y = screenPoint.y + OFFSET_Y;
// Ensure tooltip stays on screen
Dimension tooltipSize = tooltipWindow.getSize();
if (tooltipSize == null) {
tooltipWindow.pack();
tooltipSize = tooltipWindow.getSize();
}
if (tooltipSize != null) {
java.awt.Toolkit toolkit = java.awt.Toolkit.getDefaultToolkit();
Dimension screenSize = toolkit.getScreenSize();
// Adjust if tooltip would go off screen
if (x + tooltipSize.width > screenSize.width) {
x = screenPoint.x - tooltipSize.width - OFFSET_X;
}
if (y + tooltipSize.height > screenSize.height) {
y = screenPoint.y - tooltipSize.height - OFFSET_Y;
}
if (x < 0) {
x = 0;
}
if (y < 0) {
y = 0;
}
}
tooltipWindow.setLocation(x, y);
}
/**
* Hides the tooltip.
*/
public void hideTooltip() {
if (tooltipWindow != null) {
tooltipWindow.setVisible(false);
}
currentText = null;
}
public boolean isVisible() {
return tooltipWindow != null && tooltipWindow.isVisible();
}
/**
* Disposes the tooltip window.
*/
public void dispose() {
if (tooltipWindow != null) {
tooltipWindow.dispose();
tooltipWindow = null;
}
}
}
@@ -134,6 +134,9 @@ public class ConfigPlugin implements IPlugIn {
swingUI.injectMenuAction(
ActionFactory.getInstance().createConfigAction(plugInPort, "Mouse Wheel Zoom", IPlugInPort.WHEEL_ZOOM_KEY,
false), CONFIG_MENU);
swingUI.injectMenuAction(
ActionFactory.getInstance().createConfigAction(plugInPort, "Node Name Tooltips",
IPlugInPort.SHOW_NODE_NAME_TOOLTIPS_KEY, true), CONFIG_MENU);
swingUI.injectMenuAction(
ActionFactory.getInstance().createConfigAction(plugInPort, "Outline Mode", IPlugInPort.OUTLINE_KEY, false),
CONFIG_MENU);
@@ -153,18 +156,22 @@ public class ConfigPlugin implements IPlugIn {
ActionFactory.getInstance().createConfigAction(plugInPort, "Show AI Assistant",
ConfigPlugin.CHATBOT, true), CONFIG_MENU);
swingUI.injectMenuAction(
ActionFactory.getInstance().createConfigAction(plugInPort, "Show Grid", IPlugInPort.SHOW_GRID_KEY, true),
CONFIG_MENU);
swingUI.injectMenuAction(
ActionFactory.getInstance().createConfigAction(plugInPort, "Show Project Explorer",
ConfigPlugin.PROJECT_EXPLORER, false), CONFIG_MENU);
swingUI.injectMenuAction(
ActionFactory.getInstance().createConfigAction(plugInPort, "Show Rulers", IPlugInPort.SHOW_RULERS_KEY, true),
CONFIG_MENU);
swingUI.injectMenuAction(
ActionFactory.getInstance().createConfigAction(plugInPort, "Show Resize Dimensions",
IPlugInPort.SHOW_RESIZE_DIMENSIONS_TOOLTIP_KEY, true), CONFIG_MENU);
swingUI.injectMenuAction(
ActionFactory.getInstance().createConfigAction(plugInPort, "Show Rulers", IPlugInPort.SHOW_RULERS_KEY, true),
CONFIG_MENU);
swingUI.injectSubmenu(SNAP_MENU, IconLoader.GraphEdgeDirected.getIcon(), CONFIG_MENU);
swingUI.injectMenuAction(
ActionFactory.getInstance().createToggleAction(IPlugInPort.SNAP_TO_NONE, IPlugInPort.SNAP_TO_KEY, SNAP_MENU, IPlugInPort.SNAP_TO_DEFAULT),
@@ -175,11 +182,7 @@ public class ConfigPlugin implements IPlugIn {
swingUI.injectMenuAction(
ActionFactory.getInstance().createToggleAction(IPlugInPort.SNAP_TO_COMPONENTS, IPlugInPort.SNAP_TO_KEY, SNAP_MENU, IPlugInPort.SNAP_TO_DEFAULT),
SNAP_MENU);
swingUI.injectMenuAction(
ActionFactory.getInstance().createConfigAction(plugInPort, "Show Grid", IPlugInPort.SHOW_GRID_KEY, true),
CONFIG_MENU);
swingUI.injectMenuAction(
ActionFactory.getInstance()
.createConfigAction(plugInPort, "Sticky Points", IPlugInPort.STICKY_POINTS_KEY, true), CONFIG_MENU);
@@ -27,6 +27,77 @@ public class PresenterTests extends TestBase {
instantiateOneClick("Connectivity", "Solder Pad", new Point(1000, 1000));
Assert.assertEquals(1, presenter.getCurrentProject().getComponents().size());
}
@Test
public void testNodeNameHover() {
final String[] receivedTooltip = new String[1];
final Point[] receivedPoint = new Point[1];
org.diylc.common.IPlugIn testPlugin = new org.diylc.common.IPlugIn() {
@Override
public void connect(org.diylc.common.IPlugInPort plugInPort) {}
@Override
public java.util.EnumSet<org.diylc.common.EventType> getSubscribedEventTypes() {
return java.util.EnumSet.of(org.diylc.common.EventType.NODE_NAME_HOVER_TOOLTIP);
}
@Override
public void processMessage(org.diylc.common.EventType eventType, Object... params) {
if (eventType == org.diylc.common.EventType.NODE_NAME_HOVER_TOOLTIP) {
receivedTooltip[0] = params.length > 0 ? (String) params[0] : null;
receivedPoint[0] = params.length > 1 ? (Point) params[1] : null;
}
}
};
presenter.installPlugin(() -> testPlugin);
// Add first resistor at (1000, 1000) -> (1200, 1000)
instantiateTwoClick("Passive", "Resistor", new Point(1000, 1000), new Point(1200, 1000));
Assert.assertEquals(1, presenter.getCurrentProject().getComponents().size());
// Enable node names
org.diylc.appframework.miscutils.InMemoryConfigurationManager.getInstance().writeValue(
org.diylc.common.IPlugInPort.SHOW_NODE_NAME_TOOLTIPS_KEY, true);
// Hover over point 1 (1000, 1000)
presenter.mouseMoved(new Point(1000, 1000), false, false, false);
Assert.assertEquals("<b>R1</b>: 1", receivedTooltip[0]);
Assert.assertEquals(new Point(1000, 1000), receivedPoint[0]);
// Hover over point 2 (1200, 1000)
presenter.mouseMoved(new Point(1200, 1000), false, false, false);
Assert.assertEquals("<b>R1</b>: 2", receivedTooltip[0]);
// Add second resistor with pin 1 also at (1000, 1000)
instantiateTwoClick("Passive", "Resistor", new Point(1000, 1000), new Point(1000, 1200));
Assert.assertEquals(2, presenter.getCurrentProject().getComponents().size());
// Hover over point (1000, 1000) - should show both R2 and R1 separated by semicolon
presenter.mouseMoved(new Point(1000, 1000), false, false, false);
Assert.assertEquals("<b>R2</b>: 1; <b>R1</b>: 1", receivedTooltip[0]);
// Hover away
presenter.mouseMoved(new Point(0, 0), false, false, false);
Assert.assertNull(receivedTooltip[0]);
// Hover back over (1000, 1000)
presenter.mouseMoved(new Point(1000, 1000), false, false, false);
Assert.assertNotNull(receivedTooltip[0]);
// Start drag - should hide tooltip
presenter.dragStarted(new Point(1000, 1000), java.awt.dnd.DnDConstants.ACTION_MOVE, false);
Assert.assertNull(receivedTooltip[0]);
// End drag
presenter.dragEnded(new Point(1000, 1000));
// Disable node names config
org.diylc.appframework.miscutils.InMemoryConfigurationManager.getInstance().writeValue(
org.diylc.common.IPlugInPort.SHOW_NODE_NAME_TOOLTIPS_KEY, false);
presenter.mouseMoved(new Point(1000, 1000), false, false, false);
Assert.assertNull(receivedTooltip[0]);
}
// @Test
// public void testLoadVariants() {