Compare commits

...

21 Commits

Author SHA1 Message Date
Bane Stojkovic d3e80d7472 release 2026-01-10 20:43:59 +01:00
Bane Stojkovic 06e836b1ed cleanup 2026-01-10 19:40:30 +01:00
Bane Stojkovic a7bb1b02bf Group naming added 2026-01-08 22:16:30 +01:00
Bane Stojkovic a8e8e72458 introduce component ids. Refactor how groups are modeled in the Project 2026-01-07 23:08:04 +01:00
Bane Stojkovic 7da4444c18 show groups in explorer 2026-01-07 21:07:46 +01:00
Bane Stojkovic 2383f9ae77 'Potentiometer Panel' can now render numerical lug markers for easier identification 2026-01-06 19:46:14 +01:00
Bane Stojkovic 14ec18dfef Update update.xml 2026-01-06 19:06:10 +01:00
Bane Stojkovic 2f492c8fa8 Ability to resize 'Transformer Core' schematic symbol after it's already placed 2026-01-06 19:06:04 +01:00
Bane Stojkovic 9db43cd2f4 Make the label of schematic symbols (e.g. 'Resistor Symbol') clickable as well 2026-01-06 18:54:08 +01:00
Bane Stojkovic 344c238f73 Fixed the offset of terminals on a DP4T Tele type of 'Lever Switch' to be aligned with the physical switch 2026-01-05 21:30:13 +01:00
Bane Stojkovic 3376aa995a 'Open 1/8" Jack' and 'Open 1/4" Jack' can be rotated 2026-01-05 21:10:10 +01:00
Bane Stojkovic ec75f1b6eb 'Cliff 1/8" Jack' can be rotated 2026-01-05 21:06:22 +01:00
Bane Stojkovic 5f2b20ea41 New lines at the end of the 'Label' text get erroneously rendered as <br> 2026-01-05 20:53:00 +01:00
Bane Stojkovic 9d130c0ff3 Minor cleamup 2026-01-05 20:17:17 +01:00
Martin Morrell 119c41dc86 Add Freeway 3x3-05 (Ultra) Switch (#989) 2026-01-05 20:10:37 +01:00
Bane Stojkovic c5613a6fea 'Auto-Wrap Label' retains line breaks from the input text 2026-01-02 22:16:17 +01:00
Bane Stojkovic 479bd50540 Label adjustment 2026-01-02 21:42:09 +01:00
Bane Stojkovic 502a4b8d39 Tactile switch improvements 2026-01-02 21:06:27 +01:00
Bane Stojkovic 1f85c07453 Configurable 'Tactile Micro Switch' dimensions and added a few most commonly used variants 2026-01-02 10:17:23 +01:00
Bane Stojkovic 53ec1e780e fix: Clicking on 'Tactile Micro Switch' body doesn't register 2026-01-01 22:29:39 +01:00
Bane Stojkovic 2de3def32e Resize tooltip (#986)
* resize tooltip

* Fix blank board area rendering

* Fixes

* translations

* Add length

* Update update.xml
2025-12-31 15:07:22 +01:00
60 changed files with 2666 additions and 356 deletions
+1 -1
View File
@@ -7,7 +7,7 @@
<parent>
<groupId>org.diylc</groupId>
<artifactId>diylc-parent</artifactId>
<version>5.8.0</version>
<version>5.9.0</version>
</parent>
<artifactId>diylc-core</artifactId>
@@ -29,6 +29,7 @@ import java.io.Serializable;
import java.util.List;
import java.util.Set;
import org.diylc.core.ComponentGroup;
import org.diylc.core.IDIYComponent;
/**
@@ -44,19 +45,21 @@ public class ComponentTransferable implements Transferable, Serializable {
public static final DataFlavor listFlavor = new DataFlavor(ComponentTransferable.class, "application/diylc");
private List<IDIYComponent<?>> components;
private Set<Set<IDIYComponent<?>>> groups;
private Set<ComponentGroup> groups;
private String name;
public ComponentTransferable() {
super();
}
public ComponentTransferable(List<IDIYComponent<?>> components) {
public ComponentTransferable(List<IDIYComponent<?>> components, String name) {
super();
this.components = components;
this.name = name;
this.groups = null;
}
public ComponentTransferable(List<IDIYComponent<?>> components, Set<Set<IDIYComponent<?>>> groups) {
public ComponentTransferable(List<IDIYComponent<?>> components, Set<ComponentGroup> groups) {
super();
this.components = components;
this.groups = groups;
@@ -84,7 +87,11 @@ public class ComponentTransferable implements Transferable, Serializable {
return components;
}
public Set<Set<IDIYComponent<?>>> getGroups() {
public Set<ComponentGroup> getGroups() {
return groups;
}
public String getName() {
return name;
}
}
@@ -17,15 +17,11 @@
*/
package org.diylc.clipboard;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.*;
import java.util.stream.Collectors;
import org.apache.log4j.Logger;
import org.diylc.core.ComponentGroup;
import org.diylc.core.IDIYComponent;
public class ComponentTransferableFactory {
@@ -40,13 +36,19 @@ public class ComponentTransferableFactory {
return instance;
}
public ComponentTransferable build(Collection<IDIYComponent<?>> selectedComponents, Set<Set<IDIYComponent<?>>> groups) {
Set<IDIYComponent<?>> originalComponentSet = new HashSet<IDIYComponent<?>>(selectedComponents);
public ComponentTransferable build(Collection<IDIYComponent<?>> selectedComponents, Set<ComponentGroup> groups) {
List<IDIYComponent<?>> originalComponents = new ArrayList<IDIYComponent<?>>(selectedComponents);
Map<UUID, UUID> idMap = new HashMap<>();
List<IDIYComponent<?>> clonedComponents = originalComponents.stream(
).map(x -> {
try {
return x.clone();
IDIYComponent<?> clone = x.clone();
clone.setId(UUID.randomUUID());
idMap.put(x.getId(), clone.getId());
return clone;
} catch (CloneNotSupportedException e) {
LOG.error(e);
return null;
@@ -54,22 +56,11 @@ public class ComponentTransferableFactory {
})
.filter(Objects::nonNull)
.collect(Collectors.toList());
Set<Set<IDIYComponent<?>>> clonedGroups = new HashSet<Set<IDIYComponent<?>>>();
for (Set<IDIYComponent<?>> group : groups) {
if (group.isEmpty())
continue;
if (originalComponentSet.contains(group.iterator().next())) {
Set<IDIYComponent<?>> clonedGroup = new HashSet<IDIYComponent<?>>();
clonedGroups.add(clonedGroup);
for (int i = 0; i < originalComponents.size(); i++) {
if (group.contains(originalComponents.get(i))) {
clonedGroup.add(clonedComponents.get(i));
}
}
}
}
Set<ComponentGroup> clonedGroups = groups.stream().map(group ->
new ComponentGroup(group.getComponentIds().stream()
.map(idMap::get).collect(Collectors.toSet()), group.getName()))
.collect(Collectors.toSet());
return new ComponentTransferable(clonedComponents, clonedGroups);
}
@@ -21,7 +21,6 @@
*/
package org.diylc.common;
import java.awt.Rectangle;
import java.awt.geom.Point2D;
import org.diylc.core.Project;
@@ -0,0 +1,52 @@
/*
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.common;
import java.util.Set;
import org.diylc.core.ComponentGroup;
/**
* Interface for component grouping operations.
*
* @author Branislav Stojkovic
*/
public interface IGroupProcessor {
/**
* Returns the set of component groups in the current project. Each group is a set of components
* that are grouped together.
*
* @return set of component groups
*/
Set<ComponentGroup> getComponentGroups();
/**
* Groups all selected components.
*/
void groupSelectedComponents();
/**
* Ungroups all selected components.
*/
void ungroupSelectedComponents();
}
@@ -41,6 +41,7 @@ import org.diylc.core.IDIYComponent;
import org.diylc.core.Project;
import org.diylc.core.Template;
import org.diylc.core.Theme;
import org.diylc.core.measures.ResizeDimensions;
import org.diylc.core.measures.Size;
import org.diylc.filter.IComponentFilter;
import org.diylc.test.ITestProcessor;
@@ -49,9 +50,9 @@ import org.diylc.test.ITestProcessor;
* Interface for communication between plug-ins and the application. Plug-ins may acquire
* information or make changes through this interface. Application events are dispatched to plug-ins
* through {@link MessageDispatcher}
*
*
* @author Branislav Stojkovic
*
*
* @see IPlugIn
* @see MessageDispatcher
* @see IMessageListener
@@ -86,18 +87,19 @@ public interface IPlugInPort extends ISelectionProcessor, IMouseProcessor, IKeyP
public static final String RULER_IN_SUBDIVISION_KEY = "rulerInSubdivision";
public static final String CACHING_ENABLED_KEY = "cachingEnabled";
public static final String FONT_SCALE_FACTOR_KEY = "fontScaleFactor";
public static final String SHOW_RESIZE_DIMENSIONS_TOOLTIP_KEY = "showResizeDimensionsTooltip";
public static final String RULER_IN_SUBDIVISION_2 = "base of 2";
public static final String RULER_IN_SUBDIVISION_10 = "base of 10";
public static final String RULER_IN_SUBDIVISION_DEFAULT = RULER_IN_SUBDIVISION_10;
public static final String SNAP_TO_NONE = "None";
public static final String SNAP_TO_GRID = "Grid";
public static final String SNAP_TO_COMPONENTS = "Components";
public static final String SNAP_TO_DEFAULT = SNAP_TO_GRID;
public static final String LOCKED_ALPHA = "lockedAlpha";
public static final String LANGUAGE = "language";
public static final String LANGUAGE_DEFAULT = "English";
@@ -110,25 +112,25 @@ public interface IPlugInPort extends ISelectionProcessor, IMouseProcessor, IKeyP
public static final int NOBUTTON = 0;
public static final int BUTTON1 = 1;
public static final int BUTTON2 = 2;
public static final int BUTTON3 = 3;
public static final int BUTTON3 = 3;
/**
* Returns size of the canvas that takes project dimensions into account as well as zoom level.
* Each dimension is calculated as the product of the actual size and the number of pixels per
* unit. If <code>useZoom</code> is set to true, the result is scaled by zoom factor.
*
*
* @param useZoom
* @param includeExtraSpace
*
*
* @return canvas dimensions
*/
Dimension getCanvasDimensions(boolean useZoom, boolean includeExtraSpace);
/**
* Returns an instance of {@link Cursor} that should be used at the specified location.
*
*
* Note: point coordinates are scaled for zoom factor.
*
*
* @param point
* @param ctrlDown
* @param shiftDown
@@ -139,14 +141,14 @@ public interface IPlugInPort extends ISelectionProcessor, IMouseProcessor, IKeyP
/**
* Returns an instance of currently loaded project.
*
*
* @return
*/
Project getCurrentProject();
/**
* Loads specified {@link Project}.
*
*
* @param project
* @param freshStart
* @param filename file name when loading from a file
@@ -160,22 +162,22 @@ public interface IPlugInPort extends ISelectionProcessor, IMouseProcessor, IKeyP
/**
* Loads a project from the specified file.
*
*
* @param fileName
*/
void loadProjectFromFile(String fileName);
/**
* Saves the current project into the specified file.
*
*
* @param fileName
* @param isBackup
*/
void saveProjectToFile(String fileName, boolean isBackup);
/**
* Exports the current project into one or more files - one per layer and potentially drill layer.
*
*
* @param fileNameBase
* @param g2d
*/
@@ -193,7 +195,7 @@ public interface IPlugInPort extends ISelectionProcessor, IMouseProcessor, IKeyP
/**
* Shows a user dialog if there are changes to confirm that it's safe to proceed.
*
*
* @return true, if file actions (new, open, close) can be taken
*/
boolean allowFileAction();
@@ -202,7 +204,7 @@ public interface IPlugInPort extends ISelectionProcessor, IMouseProcessor, IKeyP
* Returns all available {@link ComponentType}s classified by category. Result is a {@link Map}
* between category name to a {@link List} of all {@link ComponentType}s that share that category
* name.
*
*
* @return
*/
Map<String, List<ComponentType>> getComponentTypes();
@@ -210,7 +212,7 @@ public interface IPlugInPort extends ISelectionProcessor, IMouseProcessor, IKeyP
/**
* Draws project on the provided {@link Graphics2D}. If the provided filter is not null, it will
* be used to filter the components that are shown.
*
*
* @param g2d
* @param drawOptions specific drawing options
* @param filter
@@ -224,35 +226,35 @@ public interface IPlugInPort extends ISelectionProcessor, IMouseProcessor, IKeyP
/**
* Returns current zoom level where <code>zoomLevel = 1.0d</code> means 100%.
*
*
* @return current zoom level
*/
double getZoomLevel();
/**
* Changes current zoom level where <code>zoomLevel = 1.0d</code> means 100%
*
*
* @param zoomLevel new zoom leve
*/
void setZoomLevel(double zoomLevel);
/**
* Returns current version number.
*
*
* @return
*/
VersionNumber getCurrentVersionNumber();
/**
* Returns few most recent versions.
*
* Returns few most recent versions.
*
* @return
*/
List<Version> getRecentUpdates();
/**
* Adds a list of components to the project.
*
*
* @param componentTransferable
* @param autoGroup
* @param assignNewNames
@@ -271,7 +273,7 @@ public interface IPlugInPort extends ISelectionProcessor, IMouseProcessor, IKeyP
/**
* Sets default value for the specified property name for currently selected component types.
*
*
* @param propertyName display name for property
* @param value new default value, must not be null
*/
@@ -279,7 +281,7 @@ public interface IPlugInPort extends ISelectionProcessor, IMouseProcessor, IKeyP
/**
* Sets default value for the specified property name for projects.
*
*
* @param clazz class to set defaults to
* @param propertyName display name for property
* @param value new default value, must not be null
@@ -293,6 +295,15 @@ public interface IPlugInPort extends ISelectionProcessor, IMouseProcessor, IKeyP
*/
List<PropertyWrapper> getMutualSelectionProperties();
/**
* Available only during resizing, returns the dimensions of the component being resized.
* Otherwise returns null.
*
* @return ResizeDimensions with width, height (in mm or in depending on configuration),
* and length (if component implements IHaveLength), or null if not resizing
*/
ResizeDimensions getResizeDimensions();
void editSelection();
/**
@@ -31,7 +31,7 @@ import org.diylc.core.ExpansionMode;
import org.diylc.core.IDIYComponent;
import org.diylc.core.measures.Size;
public interface ISelectionProcessor {
public interface ISelectionProcessor extends IGroupProcessor {
/**
* Returns the current {@link ComponentTransferable}.
@@ -89,16 +89,6 @@ public interface ISelectionProcessor {
*/
void rotateSelection(int direction);
/**
* Groups all selected components.
*/
void groupSelectedComponents();
/**
* Ungroups all selected components.
*/
void ungroupSelectedComponents();
/**
* Mirrors selected components in the given axis.
*
@@ -30,6 +30,7 @@ import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.UUID;
import java.util.stream.Collectors;
import org.diylc.core.IDIYComponent;
@@ -52,6 +53,7 @@ public abstract class AbstractComponent<T> implements IDIYComponent<T> {
private static final long serialVersionUID = 1L;
protected UUID id = UUID.randomUUID();
protected String name = "";
public static Color SELECTION_COLOR = Color.red;
@@ -78,6 +80,19 @@ public abstract class AbstractComponent<T> implements IDIYComponent<T> {
this.name = name;
}
@Override
public UUID getId() {
if (id == null) {
id = UUID.randomUUID();
}
return id;
}
@Override
public void setId(UUID id) {
this.id = id;
}
@Override
public boolean canControlPointOverlap(int index) {
return false;
@@ -186,6 +201,10 @@ public abstract class AbstractComponent<T> implements IDIYComponent<T> {
field.setAccessible(true);
Object value = field.get(this);
if (field.getName().equalsIgnoreCase("id")) {
value = getId();
}
// Deep copy point arrays.
// TODO: something nicer
if (value != null && value.getClass().isArray()
@@ -37,16 +37,14 @@ import com.thoughtworks.xstream.annotations.XStreamOmitField;
import org.diylc.common.IPlugInPort;
import org.diylc.common.LineStyle;
import org.diylc.common.ObjectCache;
import org.diylc.core.ComponentState;
import org.diylc.core.IDrawingObserver;
import org.diylc.core.Project;
import org.diylc.core.VisibilityPolicy;
import org.diylc.core.*;
import org.diylc.core.annotations.EditableProperty;
import org.diylc.core.measures.Size;
import org.diylc.core.measures.SizeUnit;
import org.diylc.utils.Constants;
public abstract class AbstractCurvedComponent<T> extends AbstractTransparentComponent<T> {
public abstract class AbstractCurvedComponent<T> extends AbstractTransparentComponent<T> implements
IHaveLength {
private static final long serialVersionUID = 1L;
@@ -337,8 +335,8 @@ public abstract class AbstractCurvedComponent<T> extends AbstractTransparentComp
// }
}
@EditableProperty
public Size getLength() {
@EditableProperty(name = "Length")
public Size calculateLength() {
double d = 0;
try {
Point2D[] p = getControlPoints();
@@ -0,0 +1,40 @@
package org.diylc.core;
import java.io.Serializable;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
public class ComponentGroup implements Serializable {
private Set<UUID> componentIds;
private String name;
public ComponentGroup(Set<UUID> componentIds) {
this.componentIds = componentIds;
}
public ComponentGroup(Set<UUID> componentIds, String name) {
this.componentIds = componentIds;
this.name = name;
}
public static ComponentGroup from(Collection<IDIYComponent<?>> components, String name) {
return new ComponentGroup(components.stream()
.map(IDIYComponent::getId).collect(Collectors.toSet()), name);
}
public Set<UUID> getComponentIds() {
return componentIds;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
@@ -25,6 +25,7 @@ import java.awt.Graphics2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.io.Serializable;
import java.util.UUID;
import org.diylc.core.annotations.ComponentDescriptor;
import org.diylc.core.annotations.EditableProperty;
@@ -55,6 +56,10 @@ public interface IDIYComponent<T> extends Serializable, Cloneable {
public static final int WIRING = 5;
public static final int TEXT = 6;
UUID getId();
void setId(UUID id);
/**
* @return component instance name.
*/
@@ -0,0 +1,29 @@
/*
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.core;
import org.diylc.core.measures.Size;
public interface IHaveLength {
Size calculateLength();
}
@@ -23,13 +23,9 @@ package org.diylc.core;
import java.awt.Font;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.*;
import java.util.stream.Collectors;
import org.diylc.appframework.update.VersionNumber;
import org.diylc.core.annotations.EditableProperty;
@@ -67,6 +63,7 @@ public class Project implements Serializable, Cloneable {
private Integer dotSpacing;
private List<IDIYComponent<?>> components;
private Set<Set<IDIYComponent<?>>> groups;
private Set<ComponentGroup> groupsEx;
private Set<Integer> lockedLayers;
private Set<IDIYComponent<?>> lockedComponents;
private Set<Integer> hiddenLayers;
@@ -75,6 +72,7 @@ public class Project implements Serializable, Cloneable {
public Project() {
components = new ArrayList<IDIYComponent<?>>();
groups = new HashSet<Set<IDIYComponent<?>>>();
groupsEx = new HashSet<>();
lockedLayers = new HashSet<Integer>();
hiddenLayers = new HashSet<Integer>();
lockedComponents = new HashSet<IDIYComponent<?>>();
@@ -153,22 +151,28 @@ public class Project implements Serializable, Cloneable {
/**
* List of components sorted by z-order ascending.
*
* s
* @return
*/
public List<IDIYComponent<?>> getComponents() {
return components;
}
/**
* Set of grouped components.
*
* @return
*/
public Set<Set<IDIYComponent<?>>> getGroups() {
return groups;
public Set<ComponentGroup> getGroupsEx() {
if (groupsEx == null) {
groupsEx = new HashSet<>();
if (groups != null && !groups.isEmpty()) {
for (Set<IDIYComponent<?>> group : groups) {
ComponentGroup newGroup = ComponentGroup.from(group, null);
groupsEx.add(newGroup);
}
groups.clear();
}
}
return groupsEx;
}
public Set<IDIYComponent<?>> getLockedComponents() {
if (lockedComponents == null)
lockedComponents = new HashSet<IDIYComponent<?>>();
@@ -292,6 +296,11 @@ public class Project implements Serializable, Cloneable {
return false;
} else if (!groups.equals(other.groups))
return false;
if (groupsEx == null) {
if (other.groupsEx != null)
return false;
} else if (!groupsEx.equals(other.groupsEx))
return false;
if (lockedLayers == null) {
if (other.lockedLayers != null)
return false;
@@ -356,12 +365,8 @@ public class Project implements Serializable, Cloneable {
}
}
for (Set<IDIYComponent<?>> group : this.groups) {
Set<IDIYComponent<?>> cloneGroup = new HashSet<IDIYComponent<?>>();
for (IDIYComponent<?> component : group) {
cloneGroup.add(cloneMap.get(component));
}
project.groups.add(cloneGroup);
for (ComponentGroup group : this.getGroupsEx()) {
project.getGroupsEx().add(new ComponentGroup(group.getComponentIds(), group.getName()));
}
for (IDIYComponent<?> component : this.getLockedComponents()) {
@@ -0,0 +1,33 @@
/*
*
* 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.core.measures;
/**
* Record containing resize dimensions for a component being resized.
*
* @param width the width of the component (in mm or in, depending on configuration)
* @param height the height of the component (in mm or in, depending on configuration)
* @param length the length of the component (if it implements IHaveLength), null otherwise
*/
public record ResizeDimensions(Size width, Size height, Size length) {
}
@@ -24,13 +24,7 @@ package org.diylc.presenter;
import java.awt.geom.Point2D;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.*;
import java.util.stream.Collectors;
import org.apache.log4j.Logger;
import org.diylc.appframework.miscutils.ConfigurationManager;
@@ -68,9 +62,9 @@ public class InstantiationManager {
public static final ComponentType clipboardType = new ComponentType("Clipboard contents",
"Components from the clipboard", CreationMethod.SINGLE_CLICK, "Multi", "", "", null, null, 0, false, null,
false, null, KeywordPolicy.NEVER_SHOW, null, false, null, 0);
public static final ComponentType blockType = new ComponentType("Building block",
"Components from the building block", CreationMethod.SINGLE_CLICK, "Multi", "", "", null, null, 0, false, null,
false, null, KeywordPolicy.NEVER_SHOW, null, false, null, 0);
// public static final ComponentType blockType = new ComponentType("Building block",
// "Components from the building block", CreationMethod.SINGLE_CLICK, "Multi", "", "", null, null, 0, false, null,
// false, null, KeywordPolicy.NEVER_SHOW, null, false, null, 0);
public InstantiationManager() {}
@@ -213,7 +207,10 @@ public class InstantiationManager {
// Update the component type slot so the app knows that something's
// being instantiated.
this.componentTypeSlot = autoGroup ? blockType : clipboardType;
String slotName = Optional.ofNullable(componentTransferable.getName()).orElse("Building Block");
this.componentTypeSlot = autoGroup ? new ComponentType(slotName,
"Components from the building block", CreationMethod.SINGLE_CLICK, "Multi", "", "", null, null, 0, false, null,
false, null, KeywordPolicy.NEVER_SHOW, null, false, null, 0) : clipboardType;
if (snapToGrid) {
scaledPoint = new Point2D.Double(scaledPoint.getX(), scaledPoint.getY());
@@ -224,7 +221,7 @@ public class InstantiationManager {
// copy group information if available
if (componentTransferable.getGroups() != null) {
currentProject.getGroups().addAll(componentTransferable.getGroups());
currentProject.getGroupsEx().addAll(componentTransferable.getGroups());
}
}
@@ -32,17 +32,7 @@ import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.*;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import javax.swing.JOptionPane;
@@ -59,16 +49,10 @@ import com.thoughtworks.xstream.security.AnyTypePermission;
import org.diylc.clipboard.ComponentTransferable;
import org.diylc.common.*;
import org.diylc.core.ExpansionMode;
import org.diylc.core.IDIYComponent;
import org.diylc.core.IDatasheetSupport;
import org.diylc.core.IView;
import org.diylc.core.Project;
import org.diylc.core.Template;
import org.diylc.core.Theme;
import org.diylc.core.VisibilityPolicy;
import org.diylc.core.*;
import org.diylc.core.annotations.IAutoCreator;
import org.diylc.core.gerber.GerberExporter;
import org.diylc.core.measures.ResizeDimensions;
import org.diylc.core.measures.Size;
import org.diylc.core.measures.SizeUnit;
import org.diylc.filter.IComponentFilter;
@@ -559,7 +543,7 @@ public class Presenter implements IPlugInPort {
// group components if there's more than one, e.g. building blocks, but not clipboard
// contents
if (componentSlot.size() > 1 && !componentTypeSlot.getName().toLowerCase().contains("clipboard")) {
this.currentProject.getGroups().add(new HashSet<IDIYComponent<?>>(componentSlot));
this.currentProject.getGroupsEx().add(ComponentGroup.from(componentSlot, componentTypeSlot.getName()));
}
notifyProjectModifiedIfNeeded(oldProject, "Add " + componentTypeSlot.getName(), true, true);
@@ -928,29 +912,36 @@ public class Presenter implements IPlugInPort {
messageDispatcher.dispatchMessage(EventType.REPAINT);
}
@Override
public Rectangle2D getSelectionBounds(boolean applyZoom) {
if (selectedComponents == null || selectedComponents.isEmpty())
return getComponentBounds(selectedComponents, applyZoom);
}
private Rectangle2D getComponentBounds(Collection<IDIYComponent<?>> components, boolean applyZoom) {
if (components == null || components.isEmpty())
return null;
int minX = Integer.MAX_VALUE;
int maxX = Integer.MIN_VALUE;
int minY = Integer.MAX_VALUE;
int maxY = Integer.MIN_VALUE;
for (IDIYComponent<?> c : selectedComponents) {
for (IDIYComponent<?> c : components) {
ComponentArea compArea = drawingManager.getComponentArea(c);
if (compArea != null && compArea.getOutlineArea() != null) {
Rectangle rect = compArea.getOutlineArea().getBounds();
if (rect.x < minX)
minX = rect.x;
if (rect.x + rect.width > maxX)
maxX = rect.x + rect.width;
if (rect.y < minY)
minY = rect.y;
if (rect.y + rect.height > maxY)
maxY = rect.y + rect.height;
} else if (currentProject.getComponents().contains(c))
if (compArea == null || compArea.getOutlineArea() == null) {
LOG.debug("Area is null for " + c.getName() + " of type " + c.getClass().getName());
return null;
}
Rectangle rect = compArea.getOutlineArea().getBounds();
if (rect.x < minX)
minX = rect.x;
if (rect.x + rect.width > maxX)
maxX = rect.x + rect.width;
if (rect.y < minY)
minY = rect.y;
if (rect.y + rect.height > maxY)
maxY = rect.y + rect.height;
}
if (configManager.readBoolean(EXTRA_SPACE_KEY, true)) {
@@ -1019,8 +1010,9 @@ public class Presenter implements IPlugInPort {
}
updateSelection(matching);
messageDispatcher.dispatchMessage(EventType.REPAINT, true);
if (matching.size() > 0)
if (!matching.isEmpty()) {
messageDispatcher.dispatchMessage(EventType.SCROLL_TO, getSelectionBounds(true));
}
}
@Override
@@ -1237,11 +1229,14 @@ public class Presenter implements IPlugInPort {
Map<String, Object> params = new HashMap<String, Object>();
params.put("point", point);
test.addStep(DIYTest.DRAG_OVER, params);
}
}
if (point == null || operationMode == OperationMode.HIGHLIGHT_CONNECTED_AREAS) {
return false;
}
boolean resizeInProgress = false;
Point2D scaledPoint = scalePoint(point);
if (controlPointMap != null && !controlPointMap.isEmpty()) {
// We're dragging control point(s).
@@ -1252,6 +1247,13 @@ public class Presenter implements IPlugInPort {
if (actualD == null)
return true;
// if there's at least one component with only one control point being dragged it means that we are resizing
if (controlPointMap.size() == 1 && controlPointMap.entrySet().stream()
.anyMatch(entry -> entry.getValue().size() == 1
&& entry.getKey().getControlPointCount() > 1)) {
resizeInProgress = true;
}
previousDragPoint.setLocation(previousDragPoint.getX() + actualD.getX(), previousDragPoint.getY() + actualD.getY());
} else if (selectedComponents.isEmpty() && instantiationManager.getComponentTypeSlot() == null
&& previousDragPoint != null) {
@@ -1269,10 +1271,50 @@ public class Presenter implements IPlugInPort {
this.previousScaledPoint = scalePoint(point);
instantiationManager.updateSingleClick(previousScaledPoint, isSnapToGrid(), currentProject.getGridSpacing());
}
messageDispatcher.dispatchMessage(EventType.REPAINT);
messageDispatcher.dispatchMessage(EventType.REPAINT, false, resizeInProgress);
return true;
}
public ResizeDimensions getResizeDimensions() {
if (controlPointMap == null || controlPointMap.isEmpty()) {
return null;
}
// Find the component being resized (one with a single control point being dragged)
IDIYComponent<?> resizingComponent = null;
for (Map.Entry<IDIYComponent<?>, Set<Integer>> entry : controlPointMap.entrySet()) {
if (entry.getValue().size() == 1 && entry.getKey().getControlPointCount() > 1) {
resizingComponent = entry.getKey();
break;
}
}
if (resizingComponent == null) {
return null;
}
Rectangle2D componentBounds = getComponentBounds(controlPointMap.keySet(), false);
if (componentBounds == null) {
return null;
}
// Convert pixel dimensions to Size (mm or in based on configuration)
boolean metric = configManager.readBoolean(METRIC_KEY, true);
SizeUnit unit = metric ? SizeUnit.mm : SizeUnit.in;
Size width = Size.fromPixels(componentBounds.getWidth(), unit);
Size height = Size.fromPixels(componentBounds.getHeight(), unit);
// Get length if component implements IHaveLength
Size length = null;
if (resizingComponent instanceof IHaveLength iHaveLength) {
length = iHaveLength.calculateLength();
}
return new ResizeDimensions(width, height, length);
}
private Point2D moveComponents(Map<IDIYComponent<?>, Set<Integer>> controlPointMap, int dx, int dy, boolean snapToGrid, boolean snapToObjects) {
// After we make the transfer and snap to grid, calculate actual dx
// and dy. We'll use them to translate the previous drag point.
@@ -1675,11 +1717,32 @@ public class Presenter implements IPlugInPort {
@Override
public void groupSelectedComponents() {
LOG.info("groupSelectedComponents()");
// Prompt user for group name
String groupName = view.showInputDialog("Enter group name:", "Group Components");
// If user cancelled, don't create the group
if (groupName == null) {
return;
}
Project oldProject = currentProject.clone();
// First remove the selected components from other groups.
// First break any existing groups that contain any of the selected components.
// This ensures that if components A and B are in a group, and the user selects
// A, B, and C to group together, the old group of A and B is deleted and replaced
// with a new group containing A, B, and C.
ungroupComponents(selectedComponents);
// Then group them together.
currentProject.getGroups().add(new HashSet<IDIYComponent<?>>(selectedComponents));
// Then create a new group containing all selected components with the provided name.
// Filter out any null IDs to prevent creating groups with null component IDs.
Set<UUID> componentIds = selectedComponents.stream()
.map(IDIYComponent::getId)
.filter(id -> id != null)
.collect(Collectors.toSet());
// Only create the group if there are valid component IDs
if (!componentIds.isEmpty()) {
currentProject.getGroupsEx().add(new ComponentGroup(componentIds, groupName));
}
// Notify the listeners.
notifyProjectModifiedIfNeeded(oldProject, "Group", false, true);
}
@@ -2134,14 +2197,11 @@ public class Presenter implements IPlugInPort {
* @param components
*/
private void ungroupComponents(Collection<IDIYComponent<?>> components) {
Iterator<Set<IDIYComponent<?>>> groupIterator = currentProject.getGroups().iterator();
while (groupIterator.hasNext()) {
Set<IDIYComponent<?>> group = groupIterator.next();
group.removeAll(components);
if (group.isEmpty()) {
groupIterator.remove();
}
}
Set<UUID> componentIds = components.stream().map(IDIYComponent::getId).collect(Collectors.toSet());
// Remove the group if it contains any of the specified components
currentProject.getGroupsEx().removeIf(
group -> group.getComponentIds() != null && group.getComponentIds().stream()
.anyMatch(id -> id != null && componentIds.contains(id)));
}
/**
@@ -2153,15 +2213,16 @@ public class Presenter implements IPlugInPort {
* the minimum, set contains that single component.
*/
private Set<IDIYComponent<?>> findAllGroupedComponents(IDIYComponent<?> component) {
Set<IDIYComponent<?>> components = new HashSet<IDIYComponent<?>>();
components.add(component);
for (Set<IDIYComponent<?>> group : currentProject.getGroups()) {
if (group.contains(component)) {
components.addAll(group);
break;
for (ComponentGroup group : currentProject.getGroupsEx()) {
if (group.getComponentIds().stream().anyMatch(id -> Objects.equals(id, component.getId()))) {
return currentProject.getComponents().stream()
.filter(comp -> group.getComponentIds().contains(comp.getId()))
.collect(Collectors.toSet());
}
}
return components;
return new HashSet<>(Set.of(component));
}
@Override
@@ -2171,6 +2232,10 @@ public class Presenter implements IPlugInPort {
}
Rectangle2D rect = getSelectionBounds(false);
if (rect == null) {
return null;
}
double width = rect.getWidth();
double height = rect.getHeight();
@@ -2513,7 +2578,7 @@ public class Presenter implements IPlugInPort {
}
try {
BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(
"import-defaults/variants.xml"));
"pending-variants.xml"));
XStream xStream = new XStream(new DomDriver());
xStream.addPermission(AnyTypePermission.ANY);
ProjectFileManager.xStreamSerializer.toXML(defaultVariantMap, out);
@@ -2691,7 +2756,7 @@ public class Presenter implements IPlugInPort {
@Override
public void loadBlock(String blockName) throws InvalidBlockException {
List<IDIYComponent<?>> components = buildingBlockManager.loadBlock(blockName, currentProject.getComponents());
pasteComponents(new ComponentTransferable(components), true, true);
pasteComponents(new ComponentTransferable(components, blockName), true, true);
}
@Override
@@ -2856,4 +2921,9 @@ public class Presenter implements IPlugInPort {
public DrawingManager getDrawingManager() {
return drawingManager;
}
@Override
public Set<ComponentGroup> getComponentGroups() {
return currentProject.getGroupsEx();
}
}
@@ -112,6 +112,7 @@ public class ProjectFileManager {
xStream.autodetectAnnotations(true);
xStream.alias("point", java.awt.geom.Point2D.class);
xStream.alias("point", java.awt.geom.Point2D.Double.class);
xStream.alias("group", org.diylc.core.ComponentGroup.class);
xStream.alias("font", java.awt.Font.class);
xStream.alias("project", Project.class);
xStream.aliasPackage("diylc", "org.diylc.components");
@@ -4128,4 +4128,204 @@
</org.diylc.core.Template>
</list>
</entry>
</map>
<entry>
<string>org.diylc.components.electromechanical.TactileMicroSwitch</string>
<list>
<org.diylc.core.Template>
<name>6x6 (e.g. Omron B3F-1000)</name>
<values>
<entry>
<string>Spacing X</string>
<org.diylc.core.measures.Size value="6.5" unit="mm"/>
</entry>
<entry>
<string>Spacing Y</string>
<org.diylc.core.measures.Size value="4.5" unit="mm"/>
</entry>
<entry>
<string>Label</string>
<awt-color hex="ffffff"/>
</entry>
<entry>
<string>Orientation</string>
<org.diylc.common.Orientation>DEFAULT</org.diylc.common.Orientation>
</entry>
<entry>
<string>Case</string>
<org.diylc.core.measures.Size value="6.0" unit="mm"/>
</entry>
<entry>
<string>Name</string>
<string>SW2</string>
</entry>
<entry>
<string>Font Size Override</string>
<null/>
</entry>
<entry>
<string>Value</string>
<string></string>
</entry>
<entry>
<string>Alpha</string>
<byte>127</byte>
</entry>
<entry>
<string>Body</string>
<awt-color hex="c0c0c0"/>
</entry>
<entry>
<string>Border</string>
<awt-color hex="595959"/>
</entry>
<entry>
<string>Display</string>
<org.diylc.common.Display>BOTH</org.diylc.common.Display>
</entry>
<entry>
<string>Pin Pairing Mode</string>
<diylc.electromechanical.TactileMicroSwitch_-PinPairingMode>Opposite</diylc.electromechanical.TactileMicroSwitch_-PinPairingMode>
</entry>
</values>
<points>
<point x="0.0" y="0.0"/>
<point x="0.0" y="0.175"/>
<point x="0.255" y="0.0"/>
<point x="0.255" y="0.175"/>
</points>
<defaultFlag>false</defaultFlag>
<createdOn>2026-01-02T10:01:17.805688</createdOn>
</org.diylc.core.Template>
<org.diylc.core.Template>
<name>12x12 (e.g. Omron B3F-4000)</name>
<values>
<entry>
<string>Spacing X</string>
<org.diylc.core.measures.Size value="12.6" unit="mm"/>
</entry>
<entry>
<string>Spacing Y</string>
<org.diylc.core.measures.Size value="5.0" unit="mm"/>
</entry>
<entry>
<string>Label</string>
<awt-color hex="ffffff"/>
</entry>
<entry>
<string>Orientation</string>
<org.diylc.common.Orientation>DEFAULT</org.diylc.common.Orientation>
</entry>
<entry>
<string>Case</string>
<org.diylc.core.measures.Size value="12.0" unit="mm"/>
</entry>
<entry>
<string>Name</string>
<string>SW1</string>
</entry>
<entry>
<string>Font Size Override</string>
<null/>
</entry>
<entry>
<string>Value</string>
<string></string>
</entry>
<entry>
<string>Alpha</string>
<byte>127</byte>
</entry>
<entry>
<string>Body</string>
<awt-color hex="c0c0c0"/>
</entry>
<entry>
<string>Border</string>
<awt-color hex="595959"/>
</entry>
<entry>
<string>Display</string>
<org.diylc.common.Display>BOTH</org.diylc.common.Display>
</entry>
<entry>
<string>Pin Pairing Mode</string>
<diylc.electromechanical.TactileMicroSwitch_-PinPairingMode>Opposite</diylc.electromechanical.TactileMicroSwitch_-PinPairingMode>
</entry>
</values>
<points>
<point x="0.0" y="0.0"/>
<point x="0.0" y="0.195"/>
<point x="0.495" y="0.0"/>
<point x="0.495" y="0.195"/>
</points>
<defaultFlag>false</defaultFlag>
<createdOn>2026-01-02T10:00:31.950676</createdOn>
</org.diylc.core.Template>
<org.diylc.core.Template>
<name>5x5 Low Profile SMD (e.g. Alps SKQG)</name>
<values>
<entry>
<string>Spacing X</string>
<org.diylc.core.measures.Size value="5.2" unit="mm"/>
</entry>
<entry>
<string>Spacing Y</string>
<org.diylc.core.measures.Size value="3.7" unit="mm"/>
</entry>
<entry>
<string>Label</string>
<awt-color hex="ffffff"/>
</entry>
<entry>
<string>Orientation</string>
<org.diylc.common.Orientation>DEFAULT</org.diylc.common.Orientation>
</entry>
<entry>
<string>Case</string>
<org.diylc.core.measures.Size value="5.0" unit="mm"/>
</entry>
<entry>
<string>Name</string>
<string>SW2</string>
</entry>
<entry>
<string>Font Size Override</string>
<null/>
</entry>
<entry>
<string>Value</string>
<string></string>
</entry>
<entry>
<string>Alpha</string>
<byte>127</byte>
</entry>
<entry>
<string>Border</string>
<awt-color hex="595959"/>
</entry>
<entry>
<string>Body</string>
<awt-color hex="c0c0c0"/>
</entry>
<entry>
<string>Display</string>
<org.diylc.common.Display>BOTH</org.diylc.common.Display>
</entry>
<entry>
<string>Pin Pairing Mode</string>
<diylc.electromechanical.TactileMicroSwitch_-PinPairingMode>Diagonal</diylc.electromechanical.TactileMicroSwitch_-PinPairingMode>
</entry>
</values>
<points>
<point x="0.0" y="0.0"/>
<point x="0.0" y="0.145"/>
<point x="0.2" y="0.0"/>
<point x="0.2" y="0.145"/>
</points>
<defaultFlag>false</defaultFlag>
<createdOn>2026-01-02T10:04:33.795741</createdOn>
</org.diylc.core.Template>
</list>
</entry>
</map>
@@ -484,5 +484,73 @@
</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>9</minor>
<build>0</build>
</versionNumber>
<releaseDate>2026-01-10 00:00:00.000 CET</releaseDate>
<name></name>
<changes>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>Added ability to name groups of components</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>Show groups of components as single items in the Project Explorer list</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>Added balloon tooltip next to cursor showing component width, height and length (where available) while resizing; can be disabled from Config menu</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>Added 'Freeway 3x3-05 (Ultra) Switch' (thank you mm-rnd)</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>Configurable 'Tactile Micro Switch' dimensions and added a few most commonly used variants</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>Exposed the most common pin pairing modes and added switching logic for 'Tactile Micro Switch' to allow using it for analysis</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>'Potentiometer Panel' can now render numerical lug markers for easier identification</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>'Auto-Wrap Label' retains line breaks from the input text</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>'Cliff 1/8" Jack', 'Open 1/8" Jack' and 'Open 1/4" Jack' can be rotated</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Fixed the offset of terminals on a DP4T Tele type of 'Lever Switch' to be aligned with the physical switch (attention - this will break existing diagrams and will require adjusting leads to match the new terminal positions)</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Make the label of schematic symbols (e.g. 'Resistor Symbol') clickable as well</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Ability to resize 'Transformer Core' schematic symbol after it's already placed</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>Clicking on 'Tactile Micro Switch' body doesn't register</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>New lines at the end of the 'Label' text get erroneously rendered as br HTML tags</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>
+1 -1
View File
@@ -7,7 +7,7 @@
<parent>
<groupId>org.diylc</groupId>
<artifactId>diylc-parent</artifactId>
<version>5.8.0</version>
<version>5.9.0</version>
</parent>
<artifactId>diylc-library</artifactId>
@@ -31,7 +31,6 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.stream.Collectors;
import org.diylc.common.HorizontalAlignment;
import org.diylc.common.VerticalAlignment;
@@ -72,13 +71,18 @@ public class StringUtils {
* @return a non-empty list of strings
*/
public static List<String> wrap(String str, FontMetrics fm, int maxWidth) {
String[] split = str.split("\\<br\\>");
List<String> lines = Arrays.stream(split).flatMap(s -> splitIntoLines(s).stream()).collect(Collectors.toList());
if (lines.size() == 0)
return lines;
// First split by actual newlines to honor them, then split by <br> tags
List<String> lines = splitIntoLines(str);
List<String> allLines = new ArrayList<String>();
for (String line : lines) {
String[] split = line.split("\\<br\\>");
allLines.addAll(Arrays.asList(split));
}
if (allLines.size() == 0)
return allLines;
ArrayList<String> strings = new ArrayList<String>();
for (Iterator<String> iter = lines.iterator(); iter.hasNext();)
for (Iterator<String> iter = allLines.iterator(); iter.hasNext();)
wrapLineInto((String) iter.next(), strings, fm, maxWidth);
return strings;
}
@@ -126,8 +130,8 @@ public class StringUtils {
line = line.substring(pos).trim();
len = line.length();
}
if (len > 0)
list.add(line);
// Always add the line, even if empty, to preserve empty lines from newlines
list.add(line);
}
/**
@@ -209,12 +213,24 @@ public class StringUtils {
if (text == null)
return;
String[] parts = text.split("\\<br\\>");
// Use -1 limit to preserve trailing empty strings so we can filter them out
String[] parts = text.split("\\<br\\>", -1);
if (parts.length > 1) {
// Remove trailing empty strings to avoid rendering extra blank lines
int lastNonEmpty = parts.length - 1;
while (lastNonEmpty >= 0 && parts[lastNonEmpty].trim().isEmpty()) {
lastNonEmpty--;
}
// If all parts are empty, don't render anything
if (lastNonEmpty < 0) {
return;
}
FontMetrics fontMetrics = g2d.getFontMetrics();
Rectangle stringBounds = fontMetrics.getStringBounds(parts[0], g2d).getBounds();
for (int i = 0; i < parts.length; i++)
drawCenteredText(g2d, parts[i], x, (int)(y - stringBounds.height * (parts.length - 1) / 2d + i * stringBounds.height), horizontalAlignment, verticalAlignment);
int actualLength = lastNonEmpty + 1;
for (int i = 0; i < actualLength; i++)
drawCenteredText(g2d, parts[i], x, (int)(y - stringBounds.height * (actualLength - 1) / 2d + i * stringBounds.height), horizontalAlignment, verticalAlignment);
return;
}
@@ -43,11 +43,7 @@ import org.diylc.common.IPlugInPort;
import org.diylc.common.LineStyle;
import org.diylc.common.ObjectCache;
import org.diylc.common.VerticalAlignment;
import org.diylc.core.ComponentState;
import org.diylc.core.IDrawingObserver;
import org.diylc.core.Project;
import org.diylc.core.Theme;
import org.diylc.core.VisibilityPolicy;
import org.diylc.core.*;
import org.diylc.core.annotations.EditableProperty;
import org.diylc.core.annotations.PositiveNonZeroMeasureValidator;
import org.diylc.core.measures.Size;
@@ -62,7 +58,8 @@ import org.diylc.utils.Constants;
*
* @author Branislav Stojkovic
*/
public abstract class AbstractLeadedComponent<T> extends AbstractLabeledComponent<T> {
public abstract class AbstractLeadedComponent<T> extends AbstractLabeledComponent<T> implements
IHaveLength {
private static final long serialVersionUID = 1L;
@@ -366,8 +363,8 @@ public abstract class AbstractLeadedComponent<T> extends AbstractLabeledComponen
offset = -offset;
}
drawingObserver.startTracking();
if (getMoveLabel()) {
drawingObserver.startTracking();
g2d.setTransform(oldTransform);
g2d.translate(getPoints()[2].getX(), getPoints()[2].getY());
if (getLabelOriantation() != LabelOriantation.Horizontal) {
@@ -377,19 +374,18 @@ public abstract class AbstractLeadedComponent<T> extends AbstractLabeledComponen
g2d.setTransform(oldTransform);
} else {
if (isStanding() || getLabelOriantation() == LabelOriantation.Horizontal) {
drawingObserver.startTracking();
g2d.setTransform(oldTransform);
double x = (getPoints()[0].getX() + getPoints()[1].getX() - length) / 2.0;
double y = (getPoints()[0].getY() + getPoints()[1].getY() - width) / 2.0;
g2d.drawString(label, (int) (x + (length - textRect.getWidth()) / 2 + offset),
(int) (y + calculateLabelYOffset(shapeRect, textRect, fontMetrics)));
} else {
drawingObserver.stopTracking();
g2d.drawString(label, (int) (length - textRect.getWidth()) / 2 + offset,
calculateLabelYOffset(shapeRect, textRect, fontMetrics));
g2d.setTransform(oldTransform);
}
}
drawingObserver.stopTracking();
}
// if (getLabelOriantation() == LabelOriantation.Horizontal) {
//
@@ -640,6 +636,32 @@ public abstract class AbstractLeadedComponent<T> extends AbstractLabeledComponen
this.length = length;
}
@Override
public Size calculateLength() {
try {
Point2D[] points = getPoints();
if (points.length < 2) {
return null;
}
// Calculate distance between ending points
double distanceInPixels = points[0].distance(points[1]);
// Convert to Size with appropriate unit (mm or in based on configuration)
SizeUnit unit;
if (ConfigurationManager.getInstance().readBoolean(IPlugInPort.METRIC_KEY, true)) {
unit = SizeUnit.mm;
} else {
unit = SizeUnit.in;
}
return Size.fromPixels(distanceInPixels, unit);
} catch (Exception e) {
return null;
}
}
@EditableProperty(name = "Width", defaultable = true, validatorClass = PositiveNonZeroMeasureValidator.class, sortOrder = 102)
public Size getWidth() {
return width;
@@ -69,18 +69,18 @@ public class BlankBoard extends AbstractBoard {
}
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1));
if (componentState != ComponentState.DRAGGING) {
Composite oldComposite = g2d.getComposite();
if (alpha < MAX_ALPHA) {
g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1f * alpha / MAX_ALPHA));
}
g2d.setColor(boardColor);
if (getType() == Type.SQUARE)
g2d.fillRect((int)firstPoint.getX(), (int)firstPoint.getY(), (int)(finalSecondPoint.getX() - firstPoint.getX()), (int)(finalSecondPoint.getY() - firstPoint.getY()));
else
g2d.fillOval((int)firstPoint.getX(), (int)firstPoint.getY(), (int)(finalSecondPoint.getX() - firstPoint.getX()), (int)(finalSecondPoint.getY() - firstPoint.getY()));
g2d.setComposite(oldComposite);
// if (componentState != ComponentState.DRAGGING) {
Composite oldComposite = g2d.getComposite();
if (alpha < MAX_ALPHA) {
g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1f * alpha / MAX_ALPHA));
}
g2d.setColor(boardColor);
if (getType() == Type.SQUARE)
g2d.fillRect((int)firstPoint.getX(), (int)firstPoint.getY(), (int)(finalSecondPoint.getX() - firstPoint.getX()), (int)(finalSecondPoint.getY() - firstPoint.getY()));
else
g2d.fillOval((int)firstPoint.getX(), (int)firstPoint.getY(), (int)(finalSecondPoint.getX() - firstPoint.getX()), (int)(finalSecondPoint.getY() - firstPoint.getY()));
g2d.setComposite(oldComposite);
// }
// Do not track any changes that follow because the whole board has been
// tracked so far.
drawingObserver.stopTracking();
@@ -42,6 +42,7 @@ import org.diylc.common.Orientation;
import org.diylc.common.VerticalAlignment;
import org.diylc.components.AbstractMultiPartComponent;
import org.diylc.components.electromechanical.OpenJack1_4.OpenJackType;
import org.diylc.components.transform.OpenJackTransformer;
import org.diylc.core.ComponentState;
import org.diylc.core.IDIYComponent;
import org.diylc.core.IDrawingObserver;
@@ -56,7 +57,7 @@ import org.diylc.utils.Constants;
@ComponentDescriptor(name = "Open 1/4\" Jack", category = "Electro-Mechanical", author = "Branislav Stojkovic",
description = "Switchcraft-style open panel mount 1/4\" phono jack, stereo and mono",
zOrder = IDIYComponent.COMPONENT, instanceNamePrefix = "J", enableCache = true)
zOrder = IDIYComponent.COMPONENT, instanceNamePrefix = "J", transformer = OpenJackTransformer.class, enableCache = true)
public class OpenJack1_4 extends AbstractMultiPartComponent<OpenJackType> {
private static final float FONT_SCALE = 0.8f;
@@ -42,6 +42,7 @@ import org.diylc.common.Orientation;
import org.diylc.common.VerticalAlignment;
import org.diylc.components.AbstractMultiPartComponent;
import org.diylc.components.electromechanical.OpenJack1_8.OpenJackType;
import org.diylc.components.transform.OpenJackTransformer;
import org.diylc.core.ComponentState;
import org.diylc.core.IDIYComponent;
import org.diylc.core.IDrawingObserver;
@@ -56,7 +57,7 @@ import org.diylc.utils.Constants;
@ComponentDescriptor(name = "Open 1/8\" Jack", category = "Electro-Mechanical", author = "Branislav Stojkovic",
description = "Switchcraft-style open panel mount 1/8\" phono jack, stereo and mono",
zOrder = IDIYComponent.COMPONENT, instanceNamePrefix = "J", enableCache = true)
zOrder = IDIYComponent.COMPONENT, instanceNamePrefix = "J", transformer = OpenJackTransformer.class, enableCache = true)
public class OpenJack1_8 extends AbstractMultiPartComponent<OpenJackType> {
private static final float FONT_SCALE = 0.6f;
@@ -35,13 +35,10 @@ import org.diylc.common.IPlugInPort;
import org.diylc.common.ObjectCache;
import org.diylc.common.Orientation;
import org.diylc.components.AbstractLabeledComponent;
import org.diylc.core.ComponentState;
import org.diylc.core.IDIYComponent;
import org.diylc.core.IDrawingObserver;
import org.diylc.core.Project;
import org.diylc.core.Theme;
import org.diylc.core.VisibilityPolicy;
import org.diylc.components.guitar.LeverSwitchPositionPropertyValueSource;
import org.diylc.core.*;
import org.diylc.core.annotations.ComponentDescriptor;
import org.diylc.core.annotations.DynamicEditableProperty;
import org.diylc.core.annotations.EditableProperty;
import org.diylc.core.annotations.KeywordPolicy;
import org.diylc.core.gerber.IGerberComponentSimple;
@@ -53,22 +50,21 @@ import org.diylc.utils.Constants;
category = "Electro-Mechanical", instanceNamePrefix = "SW",
description = "4-pin tactile momentary switch", zOrder = IDIYComponent.COMPONENT,
keywordPolicy = KeywordPolicy.SHOW_VALUE, enableCache = true)
public class TactileMicroSwitch extends AbstractLabeledComponent<String> implements IGerberComponentSimple {
public class TactileMicroSwitch extends AbstractLabeledComponent<String> implements IGerberComponentSimple,
ISwitch {
private static final long serialVersionUID = 1L;
public static Color BODY_COLOR = Color.lightGray;
public static Color BORDER_COLOR = Color.gray.darker();
public static Color PIN_COLOR = METAL_COLOR;
public static Color PIN_BORDER_COLOR = PIN_COLOR.darker();
public static Color INDENT_COLOR = Color.gray.darker();
public static Color LABEL_COLOR = Color.white;
public static Size PIN_SIZE = new Size(0.04d, SizeUnit.in);
public static int PIN_COUNT = 4;
public static Size CASE_SIZE = new Size(6d, SizeUnit.mm);
public static Size INDENT_SIZE = new Size(3d, SizeUnit.mm);
private static Size PIN_SPACING = new Size(4.5d, SizeUnit.mm);
private static Size ROW_SPACING = new Size(6.5d, SizeUnit.mm);
private static final Color BODY_COLOR = Color.lightGray;
private static final Color BORDER_COLOR = Color.gray.darker();
private static final Color PIN_COLOR = METAL_COLOR;
private static final Color PIN_BORDER_COLOR = PIN_COLOR.darker();
private static final Color INDENT_COLOR = Color.gray.darker();
private static final Color LABEL_COLOR = Color.white;
private static final Size PIN_SIZE = new Size(0.04d, SizeUnit.in);
private static final Size CASE_SIZE = new Size(6d, SizeUnit.mm);
private static final Size PIN_SPACING = new Size(4.5d, SizeUnit.mm);
private static final Size ROW_SPACING = new Size(6.5d, SizeUnit.mm);
private String value = "";
private Orientation orientation = Orientation.DEFAULT;
@@ -80,6 +76,12 @@ public class TactileMicroSwitch extends AbstractLabeledComponent<String> impleme
private Color labelColor = LABEL_COLOR;
transient private Area[] body;
private Size caseSize;
private Size pinSpacing;
private Size rowSpacing;
private PinPairingMode pinPairingMode;
private int selectedPosition;
public TactileMicroSwitch() {
super();
updateControlPoints();
@@ -146,10 +148,11 @@ public class TactileMicroSwitch extends AbstractLabeledComponent<String> impleme
private void updateControlPoints() {
Point2D firstPoint = controlPoints[0];
int PIN_COUNT = 4;
controlPoints = new Point2D[PIN_COUNT];
controlPoints[0] = firstPoint;
double pinSpacing = PIN_SPACING.convertToPixels();
double rowSpacing = ROW_SPACING.convertToPixels();
double pinSpacing = getPinSpacing().convertToPixels();
double rowSpacing = getRowSpacing().convertToPixels();
// Update control points.
double dx1;
double dy1;
@@ -198,8 +201,8 @@ public class TactileMicroSwitch extends AbstractLabeledComponent<String> impleme
double y = controlPoints[0].getY();
double width;
double height;
double caseSize = CASE_SIZE.convertToPixels();
double indentSize = INDENT_SIZE.convertToPixels();
double caseSize = getCaseSize().convertToPixels();
double actuatorSize = caseSize / 2;
width = height = caseSize;
x = (controlPoints[0].getX() + controlPoints[3].getX() - width) / 2;
@@ -207,7 +210,7 @@ public class TactileMicroSwitch extends AbstractLabeledComponent<String> impleme
body[0] = new Area(new Rectangle2D.Double(x, y, width, height));
width = height = indentSize;
width = height = actuatorSize;
x = (controlPoints[0].getX() + controlPoints[3].getX() - width) / 2;
y = (controlPoints[0].getY() + controlPoints[3].getY() - height) / 2;
@@ -244,7 +247,7 @@ public class TactileMicroSwitch extends AbstractLabeledComponent<String> impleme
g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1f * alpha / MAX_ALPHA));
}
g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : getBodyColor());
drawingObserver.stopTracking();
drawingObserver.startTracking();
g2d.fill(mainArea);
drawingObserver.stopTracking();
@@ -391,6 +394,59 @@ public class TactileMicroSwitch extends AbstractLabeledComponent<String> impleme
this.labelColor = labelColor;
}
@EditableProperty(name = "Case Size")
public Size getCaseSize() {
if (caseSize == null) {
caseSize = CASE_SIZE;
}
return caseSize;
}
public void setCaseSize(Size caseSize) {
this.caseSize = caseSize;
this.body = null;
}
@EditableProperty(name = "Pin Spacing")
public Size getPinSpacing() {
if (pinSpacing == null) {
pinSpacing = PIN_SPACING;
}
return pinSpacing;
}
public void setPinSpacing(Size pinSpacing) {
this.pinSpacing = pinSpacing;
this.body = null;
updateControlPoints();
}
@EditableProperty(name = "Row Spacing")
public Size getRowSpacing() {
if (rowSpacing == null) {
rowSpacing = ROW_SPACING;
}
return rowSpacing;
}
public void setRowSpacing(Size rowSpacing) {
this.rowSpacing = rowSpacing;
this.body = null;
updateControlPoints();
}
@EditableProperty(name = "Pin Pairing Mode")
public PinPairingMode getPinPairingMode() {
if (pinPairingMode == null) {
pinPairingMode = PinPairingMode.Opposite;
}
return pinPairingMode;
}
public void setPinPairingMode(PinPairingMode pinPairingMode) {
this.pinPairingMode = pinPairingMode;
}
@Override
public boolean canPointMoveFreely(int pointIndex) {
return false;
@@ -418,4 +474,56 @@ public class TactileMicroSwitch extends AbstractLabeledComponent<String> impleme
return new Rectangle2D.Double(minX - margin, minY - margin, maxX - minX + 2 * margin,
maxY - minY + 2 * margin);
}
@Override
public int getPositionCount() {
return 2;
}
@Override
public String getPositionName(int position) {
return position == 0 ? "Released" : "Pressed";
}
@Override
public boolean arePointsConnected(int index1, int index2, int position) {
// Only connect pins when switch is pressed (position = 1)
if (position != 1) {
return false;
}
PinPairingMode mode = getPinPairingMode();
switch (mode) {
case Opposite:
// Pins on opposite sides (same row): 0-2 and 1-3
return (index1 == 0 && index2 == 2) || (index1 == 2 && index2 == 0) ||
(index1 == 1 && index2 == 3) || (index1 == 3 && index2 == 1);
case Diagonal:
// Diagonally opposite pins: 0-3 and 1-2
return (index1 == 0 && index2 == 3) || (index1 == 3 && index2 == 0) ||
(index1 == 1 && index2 == 2) || (index1 == 2 && index2 == 1);
case Adjacent:
// Pins next to each other (same column): 0-1 and 2-3
return (index1 == 0 && index2 == 1) || (index1 == 1 && index2 == 0) ||
(index1 == 2 && index2 == 3) || (index1 == 3 && index2 == 2);
default:
return false;
}
}
@DynamicEditableProperty(source = TactileMicroSwitchPositionPropertyValueSource.class)
@EditableProperty(name = "Selected Position")
@Override
public Integer getSelectedPosition() {
return selectedPosition;
}
public void setSelectedPosition(Integer selectedPosition) {
this.selectedPosition = selectedPosition;
}
public enum PinPairingMode {
Opposite, Diagonal, Adjacent;
}
}
@@ -0,0 +1,17 @@
package org.diylc.components.electromechanical;
import org.diylc.common.PropertyWrapper;
import org.diylc.components.guitar.LeverSwitch;
import org.diylc.core.AbstractDynamicPropertyValueSource;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
public class TactileMicroSwitchPositionPropertyValueSource extends AbstractDynamicPropertyValueSource {
@Override
public List<Object> getAvailableValues() {
return List.of("Released", "Pressed");
}
}
@@ -59,8 +59,8 @@ import org.diylc.core.measures.SizeUnit;
import org.diylc.utils.Constants;
import org.diylc.utils.SwitchUtils;
@ComponentDescriptor(name = "Freeway 3X3-03 Toggle", category = "Guitar",
author = "Branislav Stojkovic", description = "Freeway 3X3-03 Toggle Switch",
@ComponentDescriptor(name = "Freeway 3X3-03 Switch", category = "Guitar",
author = "Branislav Stojkovic", description = "Freeway 3X3-03 (Standard) Toggle Switch",
zOrder = IDIYComponent.COMPONENT, instanceNamePrefix = "SW",
keywordPolicy = KeywordPolicy.SHOW_TAG, keywordTag = "Guitar Wiring Diagram",
transformer = Freeway3x3_03Transformer.class, enableCache = true)
@@ -0,0 +1,493 @@
/*
*
* 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.components.guitar;
import com.kitfox.svg.SVGDiagram;
import com.kitfox.svg.SVGElement;
import com.kitfox.svg.SVGUniverse;
import com.kitfox.svg.ShapeElement;
import org.apache.log4j.Logger;
import org.apache.poi.util.IOUtils;
import org.diylc.appframework.miscutils.ConfigurationManager;
import org.diylc.awt.StringUtils;
import org.diylc.common.*;
import org.diylc.components.AbstractTransparentComponent;
import org.diylc.components.transform.Freeway3x3_03Transformer;
import org.diylc.core.*;
import org.diylc.core.annotations.ComponentDescriptor;
import org.diylc.core.annotations.DynamicEditableProperty;
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;
import java.awt.*;
import java.awt.geom.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import static org.diylc.core.measures.SizeUnit.px;
@ComponentDescriptor(name = "Freeway 3X3-05 (Ultra) Switch", category = "Guitar",
author = "Martin Morrell", description = "Freeway 3X3-05 (Ultra) Toggle Switch",
zOrder = IDIYComponent.COMPONENT, instanceNamePrefix = "SW",
keywordPolicy = KeywordPolicy.SHOW_TAG, keywordTag = "Guitar Wiring Diagram",
transformer = Freeway3x3_03Transformer.class, enableCache = false)
public class Freeway3x3_05 extends AbstractTransparentComponent<Void> implements ISwitch {
private static final Logger LOG = Logger.getLogger(Freeway3x3_05.class);
private static final long serialVersionUID = 1L;
private static Color BASE_COLOR = FR4_COLOR;// Color.DARK_GRAY;
private static Color PAD_COLOR = COPPER_COLOR;
private static Color LABEL_COLOR = Color.WHITE;
private static Color CASE_COLOR = METAL_COLOR;
private static Color MARKER_COLOR = Color.lightGray;
private static final double DIAMETER_SWITCH = 34.0;
private static final double PAD_RADIUS = 15.3;
private static final double PAD_SIZE = 1.5;
private static final int NUM_CONTACTS = 28;
private static final double CONTACT_SPACING_RAD = Math.PI * (360.0 / NUM_CONTACTS) / 180.0;
private static final double CONTACT_SPACING_INITIAL = (CONTACT_SPACING_RAD * 14) + CONTACT_SPACING_RAD / 2;
private static final double GROUND_DIAMETER = 20;
private static final double CENTRE_LEN = 17;
private static final double CENTRE_WID = 10;
private static final double MARKER_OFFSET = 3.1;
private static final String[] PAD_NAMES = new String[] {
"1B", "2B", "3B", "6B", "5B", "4B", "CB", "TB", "6B", "5B", "4B", "1B", "2B", "3B",
"6A", "5A", "4A", "1A", "2A", "3A", "TA", "CA", "1A", "2A", "3A", "6A", "5A", "4A"
};
private static final AffineTransform SCALE_TX = AffineTransform.getScaleInstance(0.615d, 0.615d);
private Point2D[] controlPoints = new Point2D[] {new Point2D.Double(0, 0)};
private Orientation orientation = Orientation.DEFAULT;
private Integer selectedPosition;
private Boolean showMarkers;
private transient SVGDiagram svgDiagram;
private transient List<Shape> pads;
private transient List<Shape> labels;
private transient Shape base;
private transient Shape caseShape;
private transient double[] xOffsetsPx;
private transient double[] yOffsetsPx;
public Freeway3x3_05() {
super();
updateControlPoints();
}
@Override
public String getControlPointNodeName(int index) {
return PAD_NAMES[index];
}
@Override
public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode,
Project project, IDrawingObserver drawingObserver) {
double x = controlPoints[0].getX();
double y = controlPoints[0].getY();
if (getOrientation() != Orientation.DEFAULT) {
double theta = orientation.toRadians();
g2d.rotate(theta, x, y);
}
Composite oldComposite = g2d.getComposite();
if (alpha < MAX_ALPHA) {
g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1f * alpha / MAX_ALPHA));
}
double offsetX = getXOffsetsPx()[0];
double offsetY = getYOffsetsPx()[0];
//double origin = DIAMETER_SWITCH / 2;
int diameter = (int)(new Size(DIAMETER_SWITCH, SizeUnit.mm)).convertToPixels();
double originX = x - offsetX + diameter / 2.0;
double originY = y - offsetY + diameter / 2.0;
//Main circular shape
Color finalBorderColor;
if (outlineMode) {
Theme theme = (Theme) ConfigurationManager.getInstance().readObject(IPlugInPort.THEME_KEY,
Constants.DEFAULT_THEME);
finalBorderColor =
componentState == ComponentState.SELECTED || componentState == ComponentState.DRAGGING
? SELECTION_COLOR
: theme.getOutlineColor();
} else {
finalBorderColor =
componentState == ComponentState.SELECTED || componentState == ComponentState.DRAGGING
? SELECTION_COLOR
: BASE_COLOR.darker();
}
Shape base = new Ellipse2D.Double( originX, originY, diameter, diameter);
drawingObserver.startTracking();
g2d.setColor(BASE_COLOR);
g2d.fill(base);
drawingObserver.stopTracking();
g2d.setStroke(new BasicStroke(1f));
g2d.setColor(finalBorderColor);
g2d.draw(base);
//Ground circular shape
int gndDiameter = (int)(new Size(GROUND_DIAMETER, SizeUnit.mm)).convertToPixels();
int gndShift = (int)(new Size( (DIAMETER_SWITCH - GROUND_DIAMETER) / 2, SizeUnit.mm).convertToPixels());
Shape gnd = new Ellipse2D.Double( originX + gndShift, originY + gndShift, gndDiameter, gndDiameter);
g2d.setPaint(PAD_COLOR);
g2d.fill(gnd);
g2d.setColor(PAD_COLOR.darker());
g2d.draw(gnd);
//Centre rectangle casing block
int rectLen = (int)(new Size(CENTRE_LEN, SizeUnit.mm)).convertToPixels();
int rectWid = (int)(new Size(CENTRE_WID, SizeUnit.mm)).convertToPixels();
int rectShiftX = (int)(new Size( (DIAMETER_SWITCH - CENTRE_WID) / 2, SizeUnit.mm).convertToPixels());
int rectShiftY = (int)(new Size( (DIAMETER_SWITCH - CENTRE_LEN) / 2, SizeUnit.mm).convertToPixels());
int rectangleArc = (int)(new Size(1.0, SizeUnit.mm)).convertToPixels();
Shape casing = new RoundRectangle2D.Double(originX + rectShiftX, originY + rectShiftY, rectWid, rectLen, rectangleArc, rectangleArc);
g2d.setPaint(CASE_COLOR);
g2d.fill(casing);
g2d.setColor(CASE_COLOR.darker());
g2d.draw(casing);
//Pads
double circleShift = (int)(new Size( DIAMETER_SWITCH / 2, SizeUnit.mm).convertToPixels());
double circleCentreX = originX + circleShift;
double circleCentreY = originY + circleShift;
double radChange = CONTACT_SPACING_RAD * 0.45;
double extendedRadius = (int)new Size(PAD_RADIUS + PAD_SIZE, SizeUnit.mm).convertToPixels();
double reducedRadius = (int)new Size(PAD_RADIUS - PAD_SIZE, SizeUnit.mm).convertToPixels();
for (int i = 0; i < NUM_CONTACTS; i++) {
double rad = ((i + 0.5) * CONTACT_SPACING_RAD);
double rotExX = Math.cos(rad) * (extendedRadius * 1.01) + circleCentreX;
double rotExY = Math.sin(rad) * (extendedRadius * 1.01) + circleCentreY;
double rotExPosX = Math.cos(rad + radChange) * extendedRadius + circleCentreX;
double rotExPosY = Math.sin(rad + radChange) * extendedRadius + circleCentreY;
double rotExNegX = Math.cos(rad - radChange) * extendedRadius + circleCentreX;
double rotExNegY = Math.sin(rad - radChange) * extendedRadius + circleCentreY;
double rotReX = Math.cos(rad) * (reducedRadius * 1.01) + circleCentreX;
double rotReY = Math.sin(rad) * (reducedRadius * 1.01) + circleCentreY;
double rotRePosX = Math.cos(rad + radChange) * reducedRadius + circleCentreX;
double rotRePosY = Math.sin(rad + radChange) * reducedRadius + circleCentreY;
double rotReNegX = Math.cos(rad - radChange) * reducedRadius + circleCentreX;
double rotReNegY = Math.sin(rad - radChange) * reducedRadius + circleCentreY;
Path2D pad = new Path2D.Double();
pad.moveTo(rotExNegX, rotExNegY);
pad.quadTo(rotExX, rotExY, rotExPosX, rotExPosY);
pad.lineTo(rotRePosX, rotRePosY);
pad.quadTo(rotReX, rotReY,rotReNegX, rotReNegY);
pad.lineTo(rotExNegX, rotExNegY);
pad.closePath();
g2d.setPaint(PAD_COLOR);
g2d.fill(pad);
g2d.setColor(PAD_COLOR.darker());
g2d.draw(pad);
}
//Labels
double offset = (int) new Size(MARKER_OFFSET, SizeUnit.mm).convertToPixels();
for (int i = 0; i < NUM_CONTACTS; i++) {
Point2D p = getControlPoint(i);
double px = Math.sin(CONTACT_SPACING_INITIAL + i * CONTACT_SPACING_RAD) * (PAD_RADIUS - MARKER_OFFSET);
double py = Math.cos(CONTACT_SPACING_INITIAL + i * CONTACT_SPACING_RAD) * (PAD_RADIUS - MARKER_OFFSET);
g2d.setPaint(LABEL_COLOR);
StringUtils.drawCenteredText(g2d, PAD_NAMES[i],
(new Size(px + DIAMETER_SWITCH / 2.0, SizeUnit.mm).convertToPixels()) + originX,
(new Size(py + DIAMETER_SWITCH / 2.0, SizeUnit.mm).convertToPixels()) + originY,
HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
}
//Add other text
int makeLen = (int)(new Size(12.0, SizeUnit.mm)).convertToPixels();
int modelLen = (int)(new Size(8.0, SizeUnit.mm)).convertToPixels();
double mWidth = 1.8;
int modelWid = (int)(new Size(mWidth, SizeUnit.mm)).convertToPixels();
int makeShiftX = (int)(new Size( (DIAMETER_SWITCH / 2) - 7.25, SizeUnit.mm).convertToPixels());
int makeShiftY = (int)(new Size( (DIAMETER_SWITCH / 2) - 6.0, SizeUnit.mm).convertToPixels());
int modelShiftX = (int)(new Size( (DIAMETER_SWITCH / 2) + 5.25, SizeUnit.mm).convertToPixels());
int modelShiftY = (int)(new Size( (DIAMETER_SWITCH / 2) - 4.0, SizeUnit.mm).convertToPixels());
int modelArc = (int)(new Size(mWidth, SizeUnit.mm)).convertToPixels();
Shape make = new RoundRectangle2D.Double(originX + makeShiftX, originY + makeShiftY, modelWid, makeLen, modelArc, modelArc);
Shape model = new RoundRectangle2D.Double(originX + modelShiftX, originY + modelShiftY, modelWid, modelLen, modelArc, modelArc);
g2d.setPaint(BASE_COLOR);
g2d.fill(make);
g2d.fill(model);
g2d.setColor(PAD_COLOR.darker());
g2d.draw(make);
g2d.draw(model);
/*String makeTxt = "FREE-WAY·ULTRA";
String modelTxt = "3X3-05";
g2d.setPaint(LABEL_COLOR);
StringUtils.drawCenteredText(g2d, makeTxt,
(new Size(0.0, SizeUnit.mm).convertToPixels()) + originX + makeShiftX,
(new Size(0.0, SizeUnit.mm).convertToPixels()) + originY + makeShiftY,
HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
StringUtils.drawCenteredText(g2d, modelTxt,
(new Size(0.0, SizeUnit.mm).convertToPixels()) + originX + modelShiftX,
(new Size(0.0, SizeUnit.mm).convertToPixels()) + originY + modelShiftY,
HorizontalAlignment.CENTER, VerticalAlignment.CENTER);*/
if (getShowMarkers()) {
g2d.setColor(MARKER_COLOR);
for (int i = 0; i < getControlPointCount(); i++) {
Point2D p = getControlPoint(i);
Point2D labelPoint = new Point2D.Double(offset, 0);
double px = Math.sin(CONTACT_SPACING_INITIAL + i * CONTACT_SPACING_RAD) * (PAD_RADIUS + MARKER_OFFSET);
double py = Math.cos(CONTACT_SPACING_INITIAL + i * CONTACT_SPACING_RAD) * (PAD_RADIUS + MARKER_OFFSET);
String marker = PAD_NAMES[i];
if(marker.equals("CA")) {
marker = "A";
} else if (marker.equals("CB")) {
marker = "B";
} else if (marker.startsWith("T")) {
continue;
} else {
marker = marker.substring(0, 1);
}
StringUtils.drawCenteredText(g2d, marker,
(new Size(px + DIAMETER_SWITCH / 2.0, SizeUnit.mm).convertToPixels()) + originX,
(new Size(py + DIAMETER_SWITCH / 2.0, SizeUnit.mm).convertToPixels()) + originY,
HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
}
}
g2d.setComposite(oldComposite);
}
public double[] getXOffsetsPx() {
if (xOffsetsPx == null) {
xOffsetsPx = new double[NUM_CONTACTS];
for (int i = 0; i < NUM_CONTACTS; i++) {
double x = Math.sin(CONTACT_SPACING_INITIAL + i * CONTACT_SPACING_RAD) * PAD_RADIUS;
xOffsetsPx[i] = new Size(x + DIAMETER_SWITCH, SizeUnit.mm).convertToPixels();
}
}
return xOffsetsPx;
}
public double[] getYOffsetsPx() {
if (yOffsetsPx == null) {
yOffsetsPx = new double[NUM_CONTACTS];
for (int i = 0; i < NUM_CONTACTS; i++) {
double y = Math.cos(CONTACT_SPACING_INITIAL + i * CONTACT_SPACING_RAD) * PAD_RADIUS;
yOffsetsPx[i] = new Size(y + DIAMETER_SWITCH , SizeUnit.mm).convertToPixels();
}
}
return yOffsetsPx;
}
@Override
public void drawIcon(Graphics2D g2d, int width, int height) {
Shape base = new Ellipse2D.Double( 0, 0, width, height);
g2d.setColor(BASE_COLOR);
g2d.fill(base);
double contactOffset = 1;
Shape contacts = new Ellipse2D.Double( contactOffset, contactOffset, width - contactOffset * 2, height - contactOffset * 2);
g2d.setColor(PAD_COLOR);
g2d.fill(contacts);
double centreOffset = 4;
Shape centre = new Ellipse2D.Double( centreOffset, centreOffset, width - centreOffset * 2, height - centreOffset * 2);
g2d.setColor(BASE_COLOR);
g2d.fill(centre);
double gndOffset = 9;
Shape gnd = new Ellipse2D.Double( gndOffset, gndOffset, width - gndOffset * 2, height - gndOffset * 2);
g2d.setColor(PAD_COLOR);
g2d.fill(gnd);
double arc = 2.0;
Shape casing = new RoundRectangle2D.Double(12, 10, 8, 12, arc, arc);
g2d.setColor(CASE_COLOR);
g2d.fill(casing);
}
@Override
public int getControlPointCount() {
return NUM_CONTACTS;
}
@Override
public Point2D getControlPoint(int index) {
return controlPoints[index];
}
private void updateControlPoints() {
Point2D firstPoint = controlPoints[0];
controlPoints = new Point2D[NUM_CONTACTS];
controlPoints[0] = firstPoint;
double[] xOffsetsPx = getXOffsetsPx();
double[] yOffsetsPx = getYOffsetsPx();
double theta = orientation.toRadians();
AffineTransform tx =
AffineTransform.getRotateInstance(theta, firstPoint.getX(), firstPoint.getY());
for (int i = 1; i < controlPoints.length; i++) {
controlPoints[i] = new Point2D.Double(firstPoint.getX() + (xOffsetsPx[i] - xOffsetsPx[0]),
firstPoint.getY() + (yOffsetsPx[i] - yOffsetsPx[0]));
tx.transform(controlPoints[i], controlPoints[i]);
}
}
@Override
public boolean isControlPointSticky(int index) {
return true;
}
@Override
public VisibilityPolicy getControlPointVisibilityPolicy(int index) {
return VisibilityPolicy.ALWAYS;
}
@Override
public void setControlPoint(Point2D point, int index) {
controlPoints[index].setLocation(point);
}
@Override
public Void getValue() {
return null;
}
@Override
public void setValue(Void value) {}
@Override
public boolean canPointMoveFreely(int pointIndex) {
return false;
}
@EditableProperty
public Orientation getOrientation() {
if (orientation == null)
orientation = Orientation.DEFAULT;
return orientation;
}
public void setOrientation(Orientation orientation) {
this.orientation = orientation;
updateControlPoints();
}
@Override
public int getPositionCount() {
return 6;
}
@Override
public String getPositionName(int position) {
return Integer.toString(position + 1);
}
@DynamicEditableProperty(source = Freeway3x4_03SwitchPositionPropertyValueSource.class)
@EditableProperty(name = "Selected Position")
@Override
public Integer getSelectedPosition() {
return selectedPosition;
}
public void setSelectedPosition(Integer selectedPosition) {
this.selectedPosition = selectedPosition;
}
@EditableProperty(name = "Markers")
public Boolean getShowMarkers() {
if (showMarkers == null) {
showMarkers = false;
}
return showMarkers;
}
public void setShowMarkers(Boolean showMarkers) {
this.showMarkers = showMarkers;
}
@Override
public boolean arePointsConnected(int index1, int index2, int position) {
if (position == 0 &&
((index1 == 0 && index2 == 6) || (index1 == 6 && index2 == 11) || (index1 == 0 && index2 == 11) ||
(index1 == 17 && index2 == 21) || (index1 == 21 && index2 == 22) || (index1 == 17 && index2 == 22))
) {
return true;
}
if (position == 1 &&
((index1 == 1 && index2 == 6) || (index1 == 6 && index2 == 12) || (index1 == 1 && index2 == 12) ||
(index1 == 18 && index2 == 21) || (index1 == 21 && index2 == 23) || (index1 == 18 && index2 == 23))
) {
return true;
}
if (position == 2 &&
((index1 == 2 && index2 == 6) || (index1 == 6 && index2 == 13) || (index1 == 2 && index2 == 13) ||
(index1 == 19 && index2 == 21) || (index1 == 21 && index2 == 24) || (index1 == 19 && index2 == 24))
) {
return true;
}
if (position == 3 &&
((index1 == 5 && index2 == 6) || (index1 == 6 && index2 == 10) || (index1 == 5 && index2 == 10) ||
(index1 == 16 && index2 == 21) || (index1 == 21 && index2 == 27) || (index1 == 16 && index2 == 27))
) {
return true;
}
if (position == 4 &&
((index1 == 4 && index2 == 6) || (index1 == 6 && index2 == 9) || (index1 == 4 && index2 == 9) ||
(index1 == 15 && index2 == 21) || (index1 == 21 && index2 == 26) || (index1 == 15 && index2 == 26))
) {
return true;
}
if (position == 5 &&
((index1 == 3 && index2 == 6) || (index1 == 6 && index2 == 8) || (index1 == 3 && index2 == 8) ||
(index1 == 14 && index2 == 21) || (index1 == 21 && index2 == 25) || (index1 == 14 && index2 == 25))
) {
return true;
}
return false;
}
}
@@ -321,7 +321,8 @@ public class LeverSwitch extends AbstractAngledComponent<LeverSwitch.LeverSwitch
case DP4T:
controlPoints = new Point2D[10];
for (int i = 0; i < controlPoints.length; i++) {
controlPoints[i] = new Point2D.Double(x + (i % 2 == 1 ? terminalLength : 0), y + i * terminalSpacing);
double yPos = i % 2 == 1 ? y + (i - 2) * terminalSpacing : y + i * terminalSpacing;
controlPoints[i] = new Point2D.Double(x + (i % 2 == 1 ? terminalLength : 0), yPos);
}
break;
case DP5T:
@@ -45,7 +45,7 @@ import org.diylc.core.measures.Size;
import org.diylc.core.measures.SizeUnit;
@ComponentDescriptor(name = "Auto-Wrap Label", author = "Branislav Stojkovic", category = "Misc",
description = "User defined label with auto-wrapped text", instanceNamePrefix = "L", zOrder = IDIYComponent.TEXT, flexibleZOrder = true,
description = "User defined label with auto-wrapped text", instanceNamePrefix = "LAB", zOrder = IDIYComponent.TEXT, flexibleZOrder = true,
bomPolicy = BomPolicy.NEVER_SHOW, transformer = TextTransformer.class)
public class WrapLabel extends AbstractComponent<String> {
@@ -35,9 +35,12 @@ import java.awt.geom.Rectangle2D;
import java.awt.geom.RoundRectangle2D;
import org.diylc.appframework.miscutils.ConfigurationManager;
import org.diylc.awt.StringUtils;
import org.diylc.common.HorizontalAlignment;
import org.diylc.common.IPlugInPort;
import org.diylc.common.ObjectCache;
import org.diylc.common.Orientation;
import org.diylc.common.VerticalAlignment;
import org.diylc.components.transform.PotentiometerTransformer;
import org.diylc.core.ComponentState;
import org.diylc.core.CreationMethod;
@@ -66,10 +69,12 @@ public class PotentiometerPanel extends AbstractPotentiometer implements ILayere
protected static Size PIN_SIZE = new Size(0.05d, SizeUnit.in);
protected static Size NUT_SIZE = new Size(0.4d, SizeUnit.in);
protected static Size SHAFT_SIZE = new Size(1 / 4d, SizeUnit.in);
protected static Size MARKER_OFFSET = new Size(0.05d, SizeUnit.in);
protected static Color BODY_COLOR = Color.lightGray;
protected static Color WAFER_COLOR = Color.decode("#CD8500");
protected static Color BORDER_COLOR = Color.gray;
protected static Color NUT_COLOR = Color.decode("#CBD5DB");
protected static Color MARKER_COLOR_DEFAULT = Color.gray;
protected Size bodyDiameter = BODY_DIAMETER;
@@ -83,6 +88,8 @@ public class PotentiometerPanel extends AbstractPotentiometer implements ILayere
@Deprecated
protected boolean showShaft = false;
protected View view;
private Boolean showMarkers;
private Color markerColor;
// Array of 7 elements: 3 lug connectors, 1 pot body and 3 lugs
transient protected Area[] body = null;
@@ -353,6 +360,50 @@ public class PotentiometerPanel extends AbstractPotentiometer implements ILayere
y = panelHeight / 2 + fontMetrics.getAscent();
g2d.drawString(getValueForDisplay(), (int) (bodyRect.getX() + x), (int) (bodyRect.getY() + y));
// Draw markers if enabled.
if (getShowMarkers()) {
g2d.setColor(outlineMode ? theme.getOutlineColor() : getMarkerColor());
// Get circle center and radius
Rectangle2D circleBounds = body[3].getBounds2D();
double centerX = circleBounds.getCenterX();
double centerY = circleBounds.getCenterY();
double radius = getBodyDiameter().convertToPixels() / 2;
// Fixed distance from circle edge (inside the circle)
double markerOffset = MARKER_OFFSET.convertToPixels() + 2;
double markerDistance = radius - markerOffset;
// Draw markers "1", "2", "3" along radial line from center to each lug, close to circle edge
// Reverse numbering when view is ShaftDown
boolean reverseOrder = getView() == View.ShaftDown;
for (int i = 0; i < 3; i++) {
Point2D lugPoint = controlPoints[i];
// Calculate direction from center to lug
double dx = lugPoint.getX() - centerX;
double dy = lugPoint.getY() - centerY;
double distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 0) {
// Normalize direction vector
double unitX = dx / distance;
double unitY = dy / distance;
// Position marker at fixed distance from circle edge along radial line
double markerX = centerX + unitX * markerDistance;
double markerY = centerY + unitY * markerDistance;
// Determine marker number based on view
int markerNumber = reverseOrder ? (3 - i) : (i + 1);
StringUtils.drawCenteredText(g2d, Integer.toString(markerNumber), markerX, markerY,
HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
}
}
}
}
@Override
@@ -474,6 +525,30 @@ public class PotentiometerPanel extends AbstractPotentiometer implements ILayere
public void setWaferColor(Color waferColor) {
this.waferColor = waferColor;
}
@EditableProperty(name = "Markers")
public Boolean getShowMarkers() {
if (showMarkers == null) {
showMarkers = false;
}
return showMarkers;
}
public void setShowMarkers(Boolean showMarkers) {
this.showMarkers = showMarkers;
}
@EditableProperty(name = "Marker Color")
public Color getMarkerColor() {
if (markerColor == null) {
markerColor = MARKER_COLOR_DEFAULT;
}
return markerColor;
}
public void setMarkerColor(Color markerColor) {
this.markerColor = markerColor;
}
@Override
public String getInternalLinkName(int index1, int index2) {
@@ -157,6 +157,6 @@ public class TransformerCore extends AbstractComponent<Void> {
@Override
public boolean canPointMoveFreely(int pointIndex) {
return false;
return true;
}
}
@@ -27,18 +27,19 @@ import java.awt.geom.Point2D;
import org.diylc.common.IComponentTransformer;
import org.diylc.common.Orientation;
import org.diylc.components.electromechanical.CliffJack1_4;
import org.diylc.components.electromechanical.CliffJack1_8;
import org.diylc.core.IDIYComponent;
public class CliffJackTransformer implements IComponentTransformer {
@Override
public boolean canRotate(IDIYComponent<?> component) {
return component.getClass().equals(CliffJack1_4.class);
return component.getClass().equals(CliffJack1_4.class) || component.getClass().equals(CliffJack1_8.class);
}
@Override
public boolean canMirror(IDIYComponent<?> component) {
return component.getClass().equals(CliffJack1_4.class);
return component.getClass().equals(CliffJack1_4.class) || component.getClass().equals(CliffJack1_8.class);
}
@Override
@@ -55,8 +56,15 @@ public class CliffJackTransformer implements IComponentTransformer {
component.setControlPoint(p, index);
}
CliffJack1_4 jack = (CliffJack1_4) component;
Orientation o = jack.getOrientation();
Orientation o;
if (component instanceof CliffJack1_4) {
o = ((CliffJack1_4) component).getOrientation();
} else if (component instanceof CliffJack1_8) {
o = ((CliffJack1_8) component).getOrientation();
} else {
return;
}
int oValue = o.ordinal();
oValue += direction;
if (oValue < 0)
@@ -64,18 +72,33 @@ public class CliffJackTransformer implements IComponentTransformer {
if (oValue >= Orientation.values().length)
oValue = 0;
o = Orientation.values()[oValue];
jack.setOrientation(o);
if (component instanceof CliffJack1_4) {
((CliffJack1_4) component).setOrientation(o);
} else if (component instanceof CliffJack1_8) {
((CliffJack1_8) component).setOrientation(o);
}
}
@Override
public void mirror(IDIYComponent<?> component, Point2D center, int direction) {
CliffJack1_4 ic = (CliffJack1_4) component;
if (!(component instanceof CliffJack1_4) && !(component instanceof CliffJack1_8)) {
return;
}
IDIYComponent<?> ic = component;
if (direction == IComponentTransformer.HORIZONTAL) {
double dx = 2 * (center.getX() - ic.getControlPoint(1).getX());
double dy = 0;
Orientation o = ic.getOrientation();
Orientation o;
if (component instanceof CliffJack1_4) {
o = ((CliffJack1_4) component).getOrientation();
} else {
o = ((CliffJack1_8) component).getOrientation();
}
switch (o) {
case DEFAULT:
dy += (ic.getControlPoint(0).getY() - ic.getControlPoint(ic.getControlPointCount() - 1).getY());
@@ -98,12 +121,22 @@ public class CliffJackTransformer implements IComponentTransformer {
ic.setControlPoint(new Point2D.Double(p.getX() + dx, p.getY() + dy), i);
}
ic.setOrientation(o);
if (component instanceof CliffJack1_4) {
((CliffJack1_4) component).setOrientation(o);
} else {
((CliffJack1_8) component).setOrientation(o);
}
} else {
double dx = 0;
double dy = 2 * (center.getY() - ic.getControlPoint(1).getY());
Orientation o = ic.getOrientation();
Orientation o;
if (component instanceof CliffJack1_4) {
o = ((CliffJack1_4) component).getOrientation();
} else {
o = ((CliffJack1_8) component).getOrientation();
}
switch (o) {
case DEFAULT:
// dx += ic.getControlPoint(0).getX() - ic.getControlPoint(ic.getControlPointCount() - 1).getX();
@@ -128,7 +161,11 @@ public class CliffJackTransformer implements IComponentTransformer {
ic.setControlPoint(new Point2D.Double(p.getX() + dx, p.getY() + dy), i);
}
ic.setOrientation(o);
if (component instanceof CliffJack1_4) {
((CliffJack1_4) component).setOrientation(o);
} else {
((CliffJack1_8) component).setOrientation(o);
}
}
}
}
@@ -0,0 +1,161 @@
/*
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.components.transform;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import org.diylc.common.IComponentTransformer;
import org.diylc.components.electromechanical.OpenJack1_4;
import org.diylc.components.electromechanical.OpenJack1_8;
import org.diylc.core.IDIYComponent;
public class OpenJackTransformer implements IComponentTransformer {
@Override
public boolean canRotate(IDIYComponent<?> component) {
return component.getClass().equals(OpenJack1_4.class) || component.getClass().equals(OpenJack1_8.class);
}
@Override
public boolean canMirror(IDIYComponent<?> component) {
return component.getClass().equals(OpenJack1_4.class) || component.getClass().equals(OpenJack1_8.class);
}
@Override
public boolean mirroringChangesCircuit() {
return false;
}
@Override
public void rotate(IDIYComponent<?> component, Point2D center, int direction) {
AffineTransform rotate = AffineTransform.getRotateInstance(Math.PI / 2 * direction, center.getX(), center.getY());
for (int index = 0; index < component.getControlPointCount(); index++) {
Point2D p = new Point2D.Double();
rotate.transform(component.getControlPoint(index), p);
component.setControlPoint(p, index);
}
Integer angle;
if (component instanceof OpenJack1_4) {
angle = ((OpenJack1_4) component).getAngle();
} else if (component instanceof OpenJack1_8) {
angle = ((OpenJack1_8) component).getAngle();
} else {
return;
}
if (angle == null) {
angle = 0;
}
int newAngle = angle + direction * 90;
// Normalize to 0-360 range
while (newAngle < 0) {
newAngle += 360;
}
while (newAngle >= 360) {
newAngle -= 360;
}
if (component instanceof OpenJack1_4) {
((OpenJack1_4) component).setAngle(newAngle);
} else if (component instanceof OpenJack1_8) {
((OpenJack1_8) component).setAngle(newAngle);
}
}
@Override
public void mirror(IDIYComponent<?> component, Point2D center, int direction) {
if (!(component instanceof OpenJack1_4) && !(component instanceof OpenJack1_8)) {
return;
}
IDIYComponent<?> ic = component;
if (direction == IComponentTransformer.HORIZONTAL) {
// Mirror control points horizontally
for (int i = 0; i < ic.getControlPointCount(); i++) {
Point2D p = ic.getControlPoint(i);
double dx = 2 * (center.getX() - p.getX());
ic.setControlPoint(new Point2D.Double(p.getX() + dx, p.getY()), i);
}
Integer angle;
if (component instanceof OpenJack1_4) {
angle = ((OpenJack1_4) component).getAngle();
} else {
angle = ((OpenJack1_8) component).getAngle();
}
if (angle == null) {
angle = 0;
}
// Mirror horizontally: reflect angle across vertical axis
int newAngle = 360 - angle;
while (newAngle >= 360) {
newAngle -= 360;
}
if (component instanceof OpenJack1_4) {
((OpenJack1_4) component).setAngle(newAngle);
} else {
((OpenJack1_8) component).setAngle(newAngle);
}
} else {
// Mirror control points vertically
for (int i = 0; i < ic.getControlPointCount(); i++) {
Point2D p = ic.getControlPoint(i);
double dy = 2 * (center.getY() - p.getY());
ic.setControlPoint(new Point2D.Double(p.getX(), p.getY() + dy), i);
}
Integer angle;
if (component instanceof OpenJack1_4) {
angle = ((OpenJack1_4) component).getAngle();
} else {
angle = ((OpenJack1_8) component).getAngle();
}
if (angle == null) {
angle = 0;
}
// Mirror vertically: reflect angle across horizontal axis
int newAngle = 180 - angle;
while (newAngle < 0) {
newAngle += 360;
}
while (newAngle >= 360) {
newAngle -= 360;
}
if (component instanceof OpenJack1_4) {
((OpenJack1_4) component).setAngle(newAngle);
} else {
((OpenJack1_8) component).setAngle(newAngle);
}
}
}
}
@@ -0,0 +1,247 @@
/*
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.awt;
import static org.junit.Assert.*;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
public class StringUtilsTest {
private FontMetrics fontMetrics;
private Graphics2D g2d;
@Before
public void setUp() {
BufferedImage image = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB);
g2d = image.createGraphics();
g2d.setFont(new Font("Arial", Font.PLAIN, 12));
fontMetrics = g2d.getFontMetrics();
}
@Test
public void testWrapWithNewline() {
String text = "Line 1\nLine 2\nLine 3";
int maxWidth = 1000; // Large enough to not wrap
List<String> lines = StringUtils.wrap(text, fontMetrics, maxWidth);
assertEquals("Should split into 3 lines", 3, lines.size());
assertEquals("Line 1", lines.get(0));
assertEquals("Line 2", lines.get(1));
assertEquals("Line 3", lines.get(2));
}
@Test
public void testWrapWithCarriageReturn() {
String text = "Line 1\rLine 2\rLine 3";
int maxWidth = 1000;
List<String> lines = StringUtils.wrap(text, fontMetrics, maxWidth);
assertEquals("Should split into 3 lines", 3, lines.size());
assertEquals("Line 1", lines.get(0));
assertEquals("Line 2", lines.get(1));
assertEquals("Line 3", lines.get(2));
}
@Test
public void testWrapWithCarriageReturnLineFeed() {
String text = "Line 1\r\nLine 2\r\nLine 3";
int maxWidth = 1000;
List<String> lines = StringUtils.wrap(text, fontMetrics, maxWidth);
assertEquals("Should split into 3 lines", 3, lines.size());
assertEquals("Line 1", lines.get(0));
assertEquals("Line 2", lines.get(1));
assertEquals("Line 3", lines.get(2));
}
@Test
public void testWrapWithMixedNewlines() {
String text = "Line 1\nLine 2\rLine 3\r\nLine 4";
int maxWidth = 1000;
List<String> lines = StringUtils.wrap(text, fontMetrics, maxWidth);
assertEquals("Should split into 4 lines", 4, lines.size());
assertEquals("Line 1", lines.get(0));
assertEquals("Line 2", lines.get(1));
assertEquals("Line 3", lines.get(2));
assertEquals("Line 4", lines.get(3));
}
@Test
public void testWrapWithNewlineAndBrTag() {
String text = "Line 1\nLine 2<br>Line 3";
int maxWidth = 1000;
List<String> lines = StringUtils.wrap(text, fontMetrics, maxWidth);
assertEquals("Should split into 3 lines", 3, lines.size());
assertEquals("Line 1", lines.get(0));
assertEquals("Line 2", lines.get(1));
assertEquals("Line 3", lines.get(2));
}
@Test
public void testWrapWithNewlineAndWrapping() {
String text = "First line\nSecond line with very long text that should wrap";
int maxWidth = fontMetrics.stringWidth("Second line with very long text that should"); // Force wrapping
List<String> lines = StringUtils.wrap(text, fontMetrics, maxWidth);
assertTrue("Should have at least 2 lines (newline + wrapped)", lines.size() >= 2);
assertEquals("First line", lines.get(0));
// The second line should be wrapped, so we should have more lines
assertTrue("Second line should be wrapped", lines.size() >= 3);
}
@Test
public void testWrapWithEmptyLines() {
String text = "Line 1\n\nLine 3";
int maxWidth = 1000;
List<String> lines = StringUtils.wrap(text, fontMetrics, maxWidth);
assertEquals("Should split into 3 lines including empty line", 3, lines.size());
assertEquals("Line 1", lines.get(0));
assertEquals("", lines.get(1));
assertEquals("Line 3", lines.get(2));
}
@Test
public void testWrapWithNewlineAtEnd() {
String text = "Line 1\nLine 2\n";
int maxWidth = 1000;
List<String> lines = StringUtils.wrap(text, fontMetrics, maxWidth);
assertEquals("Should split into 2 lines (no trailing empty line)", 2, lines.size());
assertEquals("Line 1", lines.get(0));
assertEquals("Line 2", lines.get(1));
}
@Test
public void testWrapWithNoNewlines() {
String text = "This is a single line of text";
int maxWidth = 1000;
List<String> lines = StringUtils.wrap(text, fontMetrics, maxWidth);
assertEquals("Should have 1 line", 1, lines.size());
assertEquals("This is a single line of text", lines.get(0));
}
@Test
public void testWrapWithBrTagOnly() {
String text = "Line 1<br>Line 2<br>Line 3";
int maxWidth = 1000;
List<String> lines = StringUtils.wrap(text, fontMetrics, maxWidth);
assertEquals("Should split into 3 lines", 3, lines.size());
assertEquals("Line 1", lines.get(0));
assertEquals("Line 2", lines.get(1));
assertEquals("Line 3", lines.get(2));
}
@Test
public void testWrapWithNewlineAndBrTagMixed() {
String text = "Line 1\nLine 2<br>Line 3\nLine 4<br>Line 5";
int maxWidth = 1000;
List<String> lines = StringUtils.wrap(text, fontMetrics, maxWidth);
assertEquals("Should split into 5 lines", 5, lines.size());
assertEquals("Line 1", lines.get(0));
assertEquals("Line 2", lines.get(1));
assertEquals("Line 3", lines.get(2));
assertEquals("Line 4", lines.get(3));
assertEquals("Line 5", lines.get(4));
}
@Test
public void testWrapWithMultipleConsecutiveNewlines() {
String text = "Line 1\n\n\nLine 4";
int maxWidth = 1000;
List<String> lines = StringUtils.wrap(text, fontMetrics, maxWidth);
assertEquals("Should split into 4 lines with 2 empty lines", 4, lines.size());
assertEquals("Line 1", lines.get(0));
assertEquals("", lines.get(1));
assertEquals("", lines.get(2));
assertEquals("Line 4", lines.get(3));
}
@Test
public void testWrapWithMultipleConsecutiveNewlinesAtStart() {
String text = "\n\nLine 3";
int maxWidth = 1000;
List<String> lines = StringUtils.wrap(text, fontMetrics, maxWidth);
assertEquals("Should split into 3 lines with 2 empty lines at start", 3, lines.size());
assertEquals("", lines.get(0));
assertEquals("", lines.get(1));
assertEquals("Line 3", lines.get(2));
}
@Test
public void testWrapWithMultipleConsecutiveNewlinesAtEnd() {
String text = "Line 1\n\n\n";
int maxWidth = 1000;
List<String> lines = StringUtils.wrap(text, fontMetrics, maxWidth);
// Trailing newlines: the last newline at end doesn't create an empty line
// So \n\n\n at end creates 2 empty lines (from first 2 newlines)
assertEquals("Should split into 3 lines (2 empty from trailing newlines)", 3, lines.size());
assertEquals("Line 1", lines.get(0));
assertEquals("", lines.get(1));
assertEquals("", lines.get(2));
}
@Test
public void testWrapWithMultipleConsecutiveNewlinesMixed() {
String text = "Line 1\n\nLine 3\n\n\nLine 6";
int maxWidth = 1000;
List<String> lines = StringUtils.wrap(text, fontMetrics, maxWidth);
assertEquals("Should split into 6 lines", 6, lines.size());
assertEquals("Line 1", lines.get(0));
assertEquals("", lines.get(1));
assertEquals("Line 3", lines.get(2));
assertEquals("", lines.get(3));
assertEquals("", lines.get(4));
assertEquals("Line 6", lines.get(5));
}
}
@@ -0,0 +1,23 @@
package org.diylc.netlist;
import org.diylc.components.guitar.Freeway3x3_05;
import org.junit.Test;
public class Freeway3x3_05Tests extends AbstractSwitchTests {
@Test
public void testFreeway3x3_05() {
Freeway3x3_05 sw = new Freeway3x3_05();
String[] validCombinations = new String[]{
"0,0,6", "0,6,11", "0,0,11", "0,17,21", "0,21,22", "0,17,22",
"1,1,6", "1,6,12", "1,1,12", "1,18,21", "1,21,23", "1,18,23",
"2,2,6", "2,6,13", "2,2,13", "2,19,21", "2,21,24", "2,19,24",
"3,5,6", "3,6,10", "3,5,10", "3,16,21", "3,21,27", "3,16,27",
"4,4,6", "4,6,9", "4,4,9", "4,15,21", "4,21,26", "4,15,26",
"5,3,6", "5,6,8", "5,3,8", "5,14,21", "5,21,25", "5,14,25",
};
testSwitch(sw, validCombinations);
}
}
@@ -0,0 +1,49 @@
package org.diylc.netlist;
import org.junit.Test;
import org.diylc.components.electromechanical.TactileMicroSwitch;
import org.diylc.components.electromechanical.TactileMicroSwitch.PinPairingMode;
public class TactileMicroSwitchTests extends AbstractSwitchTests {
@Test
public void testTactileMicroSwitchOpposite() {
TactileMicroSwitch tactileSwitch = new TactileMicroSwitch();
tactileSwitch.setPinPairingMode(PinPairingMode.Opposite);
// Position 0 (Released): no connections
// Position 1 (Pressed): pins on opposite sides (same row): 0-2 and 1-3
String[] validCombinations = new String[] {
"1,0,2", "1,2,0", // Position 1: 0-2 (both directions)
"1,1,3", "1,3,1" // Position 1: 1-3 (both directions)
};
testSwitch(tactileSwitch, validCombinations);
}
@Test
public void testTactileMicroSwitchDiagonal() {
TactileMicroSwitch tactileSwitch = new TactileMicroSwitch();
tactileSwitch.setPinPairingMode(PinPairingMode.Diagonal);
// Position 0 (Released): no connections
// Position 1 (Pressed): diagonally opposite pins: 0-3 and 1-2
String[] validCombinations = new String[] {
"1,0,3", "1,3,0", // Position 1: 0-3 (both directions)
"1,1,2", "1,2,1" // Position 1: 1-2 (both directions)
};
testSwitch(tactileSwitch, validCombinations);
}
@Test
public void testTactileMicroSwitchAdjacent() {
TactileMicroSwitch tactileSwitch = new TactileMicroSwitch();
tactileSwitch.setPinPairingMode(PinPairingMode.Adjacent);
// Position 0 (Released): no connections
// Position 1 (Pressed): pins next to each other (same column): 0-1 and 2-3
String[] validCombinations = new String[] {
"1,0,1", "1,1,0", // Position 1: 0-1 (both directions)
"1,2,3", "1,3,2" // Position 1: 2-3 (both directions)
};
testSwitch(tactileSwitch, validCombinations);
}
}
+1 -1
View File
@@ -7,7 +7,7 @@
<parent>
<groupId>org.diylc</groupId>
<artifactId>diylc-parent</artifactId>
<version>5.8.0</version>
<version>5.9.0</version>
</parent>
<artifactId>diylc-swing</artifactId>
@@ -36,7 +36,7 @@ public class CopyAction extends AbstractAction {
public void actionPerformed(ActionEvent e) {
ActionFactory.LOG.info("Copy triggered");
clipboard.setContents(ComponentTransferableFactory.getInstance()
.build(plugInPort.getSelectedComponents(), plugInPort.getCurrentProject().getGroups()),
.build(plugInPort.getSelectedComponents(), plugInPort.getCurrentProject().getGroupsEx()),
clipboardOwner);
}
}
}
@@ -36,7 +36,7 @@ public class CutAction extends AbstractAction {
public void actionPerformed(ActionEvent e) {
ActionFactory.LOG.info("Cut triggered");
clipboard.setContents(ComponentTransferableFactory.getInstance()
.build(plugInPort.getSelectedComponents(), plugInPort.getCurrentProject().getGroups()),
.build(plugInPort.getSelectedComponents(), plugInPort.getCurrentProject().getGroupsEx()),
clipboardOwner);
plugInPort.deleteSelectedComponents();
}
@@ -5,11 +5,7 @@ import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.*;
import javax.swing.AbstractAction;
import org.diylc.appframework.miscutils.ConfigurationManager;
import org.diylc.swingframework.CheckBoxListDialog;
@@ -69,7 +65,7 @@ public class ExportBlocksAction extends AbstractAction {
dialog.setVisible(true);
if (dialog.getSelectedButtonCaption() != "OK")
if (!Objects.equals(dialog.getSelectedButtonCaption(), "OK"))
return;
Object[] selected = dialog.getSelectedOptions();
@@ -133,4 +129,4 @@ public class ExportBlocksAction extends AbstractAction {
}, true);
}
}
}
}
@@ -5,13 +5,7 @@ import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.*;
import javax.swing.AbstractAction;
import org.diylc.appframework.miscutils.ConfigurationManager;
import org.diylc.swingframework.CheckBoxListDialog;
@@ -73,7 +67,7 @@ public class ExportVariantsAction extends AbstractAction {
ActionFactory.LOG.warn("Could not find type for: " + className);
}
Collections.sort(types, new Comparator<ComponentType>() {
types.sort(new Comparator<ComponentType>() {
@Override
public int compare(ComponentType o1, ComponentType o2) {
@@ -86,7 +80,7 @@ public class ExportVariantsAction extends AbstractAction {
dialog.setVisible(true);
if (dialog.getSelectedButtonCaption() != "OK")
if (!Objects.equals(dialog.getSelectedButtonCaption(), "OK"))
return;
Object[] selected = dialog.getSelectedOptions();
@@ -131,7 +125,7 @@ public class ExportVariantsAction extends AbstractAction {
BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(file));
ProjectFileManager.xStreamSerializer.toXML(variantPkg, out);
out.close();
ActionFactory.LOG.info("Exported variants succesfully");
ActionFactory.LOG.info("Exported variants successfully");
} catch (IOException e) {
ActionFactory.LOG.error("Could not export variants", e);
}
@@ -153,4 +147,4 @@ public class ExportVariantsAction extends AbstractAction {
}, true);
}
}
}
}
@@ -88,7 +88,7 @@ public class ImportAction extends AbstractAction {
// presenter
plugInPort.pasteComponents(ComponentTransferableFactory.getInstance().build(
presenter.getCurrentProject().getComponents(),
presenter.getCurrentProject().getGroups()), false, false);
presenter.getCurrentProject().getGroupsEx()), false, false);
// Cleanup components in the temp presenter, don't need
// them anymore
presenter.selectAll(0);
@@ -113,7 +113,7 @@ public class ImportNetlistAction extends AbstractAction {
if (!outputWarnings.isEmpty())
ActionFactory.LOG.warn("Component creation produced warnings:\n"
+ String.join("\n", outputWarnings));
plugInPort.pasteComponents(new ComponentTransferable(components), false, false);
plugInPort.pasteComponents(new ComponentTransferable(components, "Imported Netlist"), false, false);
}
} catch (Exception e) {
swingUI.showMessage("Could not import netlist file: " + e.getMessage(), "Error",
@@ -130,4 +130,4 @@ public class ImportNetlistAction extends AbstractAction {
}, true);
}
}
}
}
@@ -41,6 +41,7 @@ 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.core.measures.ResizeDimensions;
import org.diylc.swingframework.ruler.IRulerListener;
import org.diylc.swingframework.ruler.Ruler.InchSubdivision;
import org.diylc.swingframework.ruler.RulerScrollPane;
@@ -53,7 +54,6 @@ import org.diylc.common.BadPositionException;
import org.diylc.common.EventType;
import org.diylc.common.IPlugIn;
import org.diylc.common.IPlugInPort;
import org.diylc.core.IDIYComponent;
import org.diylc.core.measures.Size;
import org.diylc.core.measures.SizeUnit;
import org.diylc.presenter.Presenter;
@@ -68,6 +68,8 @@ public class CanvasPlugin implements IPlugIn{
private RulerScrollPane scrollPane;
private CanvasPanel canvasPanel;
private ComponentPopupMenu popupMenu;
private ResizeInfoTooltip resizeInfoTooltip;
private boolean resizingInProgress = false;
private IPlugInPort plugInPort;
private ISwingUI swingUI;
@@ -193,6 +195,7 @@ public class CanvasPlugin implements IPlugIn{
public CanvasPanel getCanvasPanel() {
if (canvasPanel == null) {
canvasPanel = new CanvasPanel(plugInPort, configManager);
resizeInfoTooltip = new ResizeInfoTooltip(canvasPanel, configManager);
canvasPanel.addMouseListener(new MouseAdapter() {
private MouseEvent pressedEvent;
@@ -277,6 +280,15 @@ public class CanvasPlugin implements IPlugIn{
e.isShiftDown(), e.isAltDown()));
plugInPort.mouseMoved(e.getPoint(), Utils.isMac() ? e.isMetaDown() : e.isControlDown(),
e.isShiftDown(), e.isAltDown());
// Update tooltip position if resizing is in progress and tooltip is enabled, otherwise hide it
if (resizeInfoTooltip != null) {
if (resizingInProgress && configManager.readBoolean(IPlugInPort.SHOW_RESIZE_DIMENSIONS_TOOLTIP_KEY, true)) {
resizeInfoTooltip.updatePosition(e.getPoint());
} else {
resizeInfoTooltip.hideTooltip();
}
}
}
});
@@ -505,11 +517,13 @@ public class CanvasPlugin implements IPlugIn{
canvasPanel.repaint();
// Refresh selection bounds after we're done with painting to ensure we have traced the
// component areas
boolean panToSelection = params.length > 0 && Boolean.TRUE.equals(params[0]);
this.resizingInProgress = params.length > 1 && Boolean.TRUE.equals(params[1]);
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
if (params.length > 0 && Boolean.TRUE.equals(params[0])) {
if (panToSelection) {
Rectangle2D selectionBounds = plugInPort.getSelectionBounds(true);
if (selectionBounds != null) {
Rectangle visibleRect = scrollPane.getVisibleRect();
@@ -519,8 +533,28 @@ public class CanvasPlugin implements IPlugIn{
);
}
}
if (configManager.readBoolean(IPlugInPort.SHOW_RULERS_KEY, true))
if (configManager.readBoolean(IPlugInPort.SHOW_RULERS_KEY, true)) {
scrollPane.setSelectionRectangle(plugInPort.getSelectionBounds(true));
}
if (CanvasPlugin.this.resizingInProgress && getCanvasPanel().getMousePosition() != null) {
// Only show tooltip if enabled in configuration
if (configManager.readBoolean(IPlugInPort.SHOW_RESIZE_DIMENSIONS_TOOLTIP_KEY, true)) {
ResizeDimensions resizeDimensions = plugInPort.getResizeDimensions();
if (resizeInfoTooltip != null) {
resizeInfoTooltip.update(resizeDimensions, getCanvasPanel().getMousePosition());
}
} else {
// Hide tooltip if disabled
if (resizeInfoTooltip != null) {
resizeInfoTooltip.hideTooltip();
}
}
} else {
// Hide tooltip when not resizing
if (resizeInfoTooltip != null) {
resizeInfoTooltip.hideTooltip();
}
}
}
});
break;
@@ -563,25 +597,4 @@ public class CanvasPlugin implements IPlugIn{
double extraSpace = CanvasPlugin.this.plugInPort.getExtraSpace();
getScrollPane().setZeroLocation(new Point2D.Double(extraSpace, extraSpace));
}
class LockAction extends AbstractAction {
private static final long serialVersionUID = 1L;
private IDIYComponent<?> component;
private boolean locked;
public LockAction(IDIYComponent<?> component, boolean locked) {
super();
this.locked = locked;
this.component = component;
putValue(AbstractAction.NAME, component.getName());
}
@Override
public void actionPerformed(ActionEvent e) {
plugInPort.lockComponent(component, locked);
}
}
}
@@ -0,0 +1,225 @@
/*
*
* 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 java.text.DecimalFormat;
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;
import org.diylc.core.measures.ResizeDimensions;
import org.diylc.core.measures.Size;
/**
* A lightweight tooltip window that displays resize dimensions and tracks the mouse cursor.
*/
public class ResizeInfoTooltip {
private static final int OFFSET_X = 16;
private static final int OFFSET_Y = 16;
private static final DecimalFormat SIZE_FORMAT = new DecimalFormat("0.00");
private JWindow tooltipWindow;
private JLabel tooltipLabel;
private Component parentComponent;
private ResizeDimensions currentDimensions;
public ResizeInfoTooltip(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 new dimensions and mouse position.
*/
public void update(ResizeDimensions dimensions, Point mousePosition) {
this.currentDimensions = dimensions;
if (dimensions == null || mousePosition == null) {
hideTooltip();
return;
}
if (tooltipWindow == null) {
initializeTooltip();
if (tooltipWindow == null) {
return;
}
}
// Format the dimensions
String text = formatDimensions(dimensions);
tooltipLabel.setText(text);
tooltipLabel.setPreferredSize(null); // Reset to calculate preferred size
tooltipWindow.pack();
// Position the tooltip near the mouse cursor
updateTooltipPosition(mousePosition);
tooltipWindow.setVisible(true);
}
/**
* Updates the tooltip position based on the current mouse position.
*/
public void updatePosition(Point mousePosition) {
if (currentDimensions != 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);
}
private String formatDimensions(ResizeDimensions dimensions) {
StringBuilder sb = new StringBuilder("<html>");
// Format width
Size width = dimensions.width();
String widthUnit = width.getUnit().toString();
sb.append(String.format("W: %s %s", SIZE_FORMAT.format(width.getValue()), widthUnit));
// Format height
Size height = dimensions.height();
String heightUnit = height.getUnit().toString();
sb.append(String.format("<br>H: %s %s", SIZE_FORMAT.format(height.getValue()), heightUnit));
// Format length if available
Size length = dimensions.length();
if (length != null) {
String lengthUnit = length.getUnit().toString();
sb.append(String.format("<br>L: %s %s", SIZE_FORMAT.format(length.getValue()), lengthUnit));
}
sb.append("</html>");
return sb.toString();
}
/**
* Hides the tooltip.
*/
public void hideTooltip() {
if (tooltipWindow != null) {
tooltipWindow.setVisible(false);
}
currentDimensions = null;
}
/**
* Disposes the tooltip window.
*/
public void dispose() {
if (tooltipWindow != null) {
tooltipWindow.dispose();
tooltipWindow = null;
}
}
}
@@ -158,6 +158,10 @@ public class ConfigPlugin implements IPlugIn {
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.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),
@@ -18,6 +18,7 @@ import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import javax.swing.AbstractListModel;
import javax.swing.DefaultListCellRenderer;
@@ -36,6 +37,7 @@ import javax.swing.event.ListSelectionListener;
import org.diylc.common.ComponentType;
import org.diylc.common.IPlugInPort;
import org.diylc.core.ComponentGroup;
import org.diylc.core.IDIYComponent;
import org.diylc.lang.LangUtil;
import org.diylc.swing.ISwingUI;
@@ -60,9 +62,10 @@ public class ExplorerPane extends JPanel {
private JTextField searchField;
private JScrollPane componentScrollPane;
private ComponentListModel componentListModel;
private JList<IDIYComponent<?>> componentList;
private JList<ComponentGroupItem> componentList;
private ComponentPopupMenu popupMenu;
private JComboBox<Sort> sortBox;
private ListSelectionListener selectionListener;
public ExplorerPane(ISwingUI swingUI, IPlugInPort plugInPort) {
super();
@@ -144,9 +147,9 @@ public class ExplorerPane extends JPanel {
return componentScrollPane;
}
public JList<IDIYComponent<?>> getComponentList() {
public JList<ComponentGroupItem> getComponentList() {
if (componentList == null) {
componentList = new DragDropList<IDIYComponent<?>>(new IDragDropListListener() {
componentList = new DragDropList<ComponentGroupItem>(new IDragDropListListener() {
@Override
public boolean dropComplete(int selectedIndex, int dropTargetIndex) {
@@ -154,7 +157,82 @@ public class ExplorerPane extends JPanel {
swingUI.showMessage(REORDERING_NOT_POSSIBLE, "Error", ISwingUI.ERROR_MESSAGE);
return false;
}
plugInPort.moveSelectionToZIndex(getComponentListModel().getSize() - dropTargetIndex);
// The list is displayed in reverse order (highest z-index first, index 0 = last in component list)
// Component list is in normal order (lowest z-index first, index 0 = first component)
// In INSERT mode, dropTargetIndex is where the item will be inserted AFTER removal
int listSize = getComponentListModel().getSize();
int componentCount = plugInPort.getCurrentProject().getComponents().size();
// The list size may be different from component count due to groups
// listSize = componentCount - groupedComponents + groups
// We need to find which component will be at dropTargetIndex in the final list
// and convert that to a component index
// Find the ComponentGroupItem that will be at dropTargetIndex after removal
// We want to insert before the component that will be at dropTargetIndex after insertion
List<ComponentGroupItem> components = getComponentListModel().getComponents();
int componentAtTargetListIndex;
if (dropTargetIndex >= listSize - 1) {
// Dropping at the end
plugInPort.moveSelectionToZIndex(componentCount);
return true;
} else if (selectedIndex < dropTargetIndex) {
// Dragging up: removal happens before target
// After removal, the component that was at dropTargetIndex is now at dropTargetIndex-1
// After insertion at dropTargetIndex, it will be at dropTargetIndex+1
// So we want to insert before the component currently at dropTargetIndex
componentAtTargetListIndex = dropTargetIndex;
} else {
// Dragging down: removal happens after target
// After removal, the component at dropTargetIndex stays at dropTargetIndex
// After insertion at dropTargetIndex, it will be at dropTargetIndex+1
// So we want to insert before the component currently at dropTargetIndex
componentAtTargetListIndex = dropTargetIndex;
}
if (componentAtTargetListIndex >= listSize) {
// Insert at the end
plugInPort.moveSelectionToZIndex(componentCount);
return true;
}
// Get the ComponentGroupItem at that position
ComponentGroupItem targetItem = components.get(componentAtTargetListIndex);
// Find the highest z-index component in this group (or the component itself if single)
// Since the list is sorted by z-index (reversed), this is the component that determines
// the position of this group in the list
IDIYComponent<?> targetComponent = null;
int maxComponentIndex = -1;
List<IDIYComponent<?>> allComponents = plugInPort.getCurrentProject().getComponents();
for (IDIYComponent<?> comp : targetItem.getGroup()) {
int compIndex = allComponents.indexOf(comp);
if (compIndex > maxComponentIndex) {
maxComponentIndex = compIndex;
targetComponent = comp;
}
}
if (targetComponent == null || maxComponentIndex < 0) {
return false;
}
// We want to insert before the component at maxComponentIndex
// moveSelectionToZIndex will automatically decrement zIndex for each selected component
// that's before the target, so we can use maxComponentIndex directly
// However, we need to add 1 because we want to insert at dropTargetIndex, not before it
int targetComponentIndex = maxComponentIndex + 1;
if (targetComponentIndex < 0) {
targetComponentIndex = 0;
} else if (targetComponentIndex > componentCount) {
targetComponentIndex = componentCount;
}
plugInPort.moveSelectionToZIndex(targetComponentIndex);
return true;
}
});
@@ -162,20 +240,28 @@ public class ExplorerPane extends JPanel {
componentList.setFont(DEFAULT_FONT);
componentList.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
componentList.setCellRenderer(new ComponentListCellRenderer(plugInPort.getComponentTypes()));
componentList.addListSelectionListener(new ListSelectionListener() {
selectionListener = new ListSelectionListener() {
@Override
public void valueChanged(ListSelectionEvent e) {
if (e.getValueIsAdjusting())
return;
List<IDIYComponent<?>> selectedValuesList = componentList.getSelectedValuesList();
if (selectedValuesList.size() > 0 && !new HashSet<IDIYComponent<?>>(selectedValuesList)
List<ComponentGroupItem> selectedValuesList = componentList.getSelectedValuesList();
Set<IDIYComponent<?>> expandedSelection = new HashSet<>();
for (ComponentGroupItem selected : selectedValuesList) {
// Always add all components from the group item
expandedSelection.addAll(selected.getGroup());
}
if (expandedSelection.size() > 0 && !expandedSelection
.equals(new HashSet<IDIYComponent<?>>(plugInPort.getSelectedComponents()))) {
plugInPort.setSelection(selectedValuesList, true);
plugInPort.setSelection(expandedSelection, true);
}
}
});
};
componentList.addListSelectionListener(selectionListener);
componentList.addMouseListener(new MouseAdapter() {
@@ -205,8 +291,13 @@ public class ExplorerPane extends JPanel {
if (plugInPort.getNewComponentTypeSlot() == null && (e.isPopupTrigger()
|| (pressedEvent != null && pressedEvent.isPopupTrigger()))) {
final List<IDIYComponent<?>> selectedValues = componentList.getSelectedValuesList();
getComponentPopupMenu().prepareAndShowAt(selectedValues, selectedValues, e.getX(),
final List<ComponentGroupItem> selectedValues = componentList.getSelectedValuesList();
// Extract actual components from selected items
List<IDIYComponent<?>> components = new ArrayList<>();
for (ComponentGroupItem item : selectedValues) {
components.addAll(item.getGroup());
}
getComponentPopupMenu().prepareAndShowAt(components, components, e.getX(),
e.getY());
}
}
@@ -238,46 +329,105 @@ public class ExplorerPane extends JPanel {
}
public void setSelection(Set<IDIYComponent<?>> selectedComponents) {
List<IDIYComponent<?>> selectedValuesList = componentList.getSelectedValuesList();
if (new HashSet<IDIYComponent<?>>(selectedValuesList)
.equals(new HashSet<IDIYComponent<?>>(plugInPort.getSelectedComponents())))
List<ComponentGroupItem> selectedValuesList = componentList.getSelectedValuesList();
// Extract components from selected values
Set<IDIYComponent<?>> currentSelected = new HashSet<>();
for (ComponentGroupItem item : selectedValuesList) {
currentSelected.addAll(item.getGroup());
}
if (currentSelected.equals(selectedComponents))
return;
final JList<IDIYComponent<?>> list = this.getComponentList();
final JList<ComponentGroupItem> list = this.getComponentList();
if (selectedComponents.size() == 0) {
list.clearSelection();
} else {
// Temporarily remove listener to prevent infinite loop
list.removeListSelectionListener(selectionListener);
try {
if (selectedComponents.size() == 0) {
list.clearSelection();
} else {
// Build a map from component to its group (if any)
Set<ComponentGroup> groups = plugInPort.getComponentGroups();
Map<IDIYComponent<?>, Set<IDIYComponent<?>>> componentToGroup = new java.util.HashMap<>();
if (groups != null && !groups.isEmpty()) {
// Build a map from UUID to component for quick lookup
List<IDIYComponent<?>> allComponents = plugInPort.getCurrentProject().getComponents();
Map<UUID, IDIYComponent<?>> idToComponent = allComponents.stream()
.collect(Collectors.toMap(IDIYComponent::getId, comp -> comp));
for (ComponentGroup group : groups) {
// Convert ComponentGroup (with UUIDs) to Set<IDIYComponent<?>>
Set<IDIYComponent<?>> componentSet = group.getComponentIds().stream()
.map(idToComponent::get)
.filter(java.util.Objects::nonNull)
.collect(Collectors.toSet());
for (IDIYComponent<?> component : componentSet) {
componentToGroup.put(component, componentSet);
}
}
}
int[] indices = new int[selectedComponents.size()];
int n = 0;
List<IDIYComponent<?>> components = this.getComponentListModel().getComponents();
for (int i = 0; i < components.size(); i++) {
indices[n] = -1; // this should not happen
if (selectedComponents.contains(components.get(i)))
indices[n++] = i;
if (n == indices.length)
break;
// Find indices of items to select
List<ComponentGroupItem> components = this.getComponentListModel().getComponents();
Set<Integer> selectedIndices = new HashSet<>();
Set<Set<IDIYComponent<?>>> selectedGroups = new HashSet<>();
for (IDIYComponent<?> selected : selectedComponents) {
Set<IDIYComponent<?>> group = componentToGroup.get(selected);
if (group != null) {
// Component is in a group
if (!selectedGroups.contains(group)) {
// Find the ComponentGroupItem for this group
for (int i = 0; i < components.size(); i++) {
ComponentGroupItem groupItem = components.get(i);
if (groupItem.getGroup().equals(group)) {
selectedIndices.add(i);
selectedGroups.add(group);
break;
}
}
}
} else {
// Component is not in any group, find the ComponentGroupItem that contains only this component
for (int i = 0; i < components.size(); i++) {
ComponentGroupItem groupItem = components.get(i);
if (groupItem.getGroup().size() == 1 && groupItem.getGroup().contains(selected)) {
selectedIndices.add(i);
break;
}
}
}
}
int[] indices = selectedIndices.stream().mapToInt(i -> i).toArray();
list.getSelectionModel().setValueIsAdjusting(true);
list.setSelectedIndices(indices);
list.getSelectionModel().setValueIsAdjusting(false);
if (indices.length > 0) {
int firstVisibleIndex = list.getFirstVisibleIndex();
int lastVisibleIndex = list.getLastVisibleIndex();
// if none of the selected items is visible, jump to the first one
if (!Arrays.stream(indices)
.anyMatch(index -> firstVisibleIndex <= index && index <= lastVisibleIndex))
list.ensureIndexIsVisible(indices[0]);
}
}
list.getSelectionModel().setValueIsAdjusting(true);
list.setSelectedIndices(indices);
list.getSelectionModel().setValueIsAdjusting(false);
int firstVisibleIndex = list.getFirstVisibleIndex();
int lastVisibleIndex = list.getFirstVisibleIndex();
// if none of the selected items is visible, jump to the first one
if (!Arrays.stream(indices)
.anyMatch(index -> firstVisibleIndex <= index && index <= lastVisibleIndex))
list.ensureIndexIsVisible(indices[0]);
} finally {
// Re-add listener
list.addListSelectionListener(selectionListener);
}
}
private class ComponentListModel extends AbstractListModel<IDIYComponent<?>> {
private class ComponentListModel extends AbstractListModel<ComponentGroupItem> {
private static final long serialVersionUID = 1L;
private List<IDIYComponent<?>> componentsRaw;
private List<IDIYComponent<?>> components;
private List<ComponentGroupItem> components;
private String searchText;
private Sort sort = Sort.Z_INDEX;
@@ -289,7 +439,7 @@ public class ExplorerPane extends JPanel {
refresh(oldSize);
}
public List<IDIYComponent<?>> getComponents() {
public List<ComponentGroupItem> getComponents() {
return components;
}
@@ -310,27 +460,32 @@ public class ExplorerPane extends JPanel {
}
private void refresh(int oldSize) {
List<IDIYComponent<?>> sortedComponents;
if (sort == Sort.NAME) {
this.components =
sortedComponents =
componentsRaw.stream().sorted(Comparator.comparing(x -> x.toString().toLowerCase()))
.collect(Collectors.toList());
} else if (sort == Sort.TYPE) {
this.components = componentsRaw.stream()
sortedComponents = componentsRaw.stream()
.sorted(Comparator.comparing(x -> x.getClass().getName().toLowerCase())
.thenComparing(x -> x.toString().toLowerCase()))
.collect(Collectors.toList());
} else {
List<IDIYComponent<?>> componentsClone = new ArrayList<IDIYComponent<?>>(componentsRaw);
Collections.reverse(componentsClone);
this.components = componentsClone;
sortedComponents = componentsClone;
}
if (searchText != null && !searchText.trim().isEmpty()) {
this.components = this.components.stream().filter(component -> {
sortedComponents = sortedComponents.stream().filter(component -> {
return component.getName().toLowerCase().contains(searchText);
}).collect(Collectors.toList());
}
// Build list with groups and ungrouped components
this.components = buildComponentListWithGroups(sortedComponents);
int newSize = this.components.size();
fireContentsChanged(this, 0, Math.min(newSize, oldSize));
@@ -339,6 +494,67 @@ public class ExplorerPane extends JPanel {
else if (newSize > oldSize)
fireIntervalAdded(this, oldSize, newSize);
}
/**
* Builds the component list, using ComponentGroupItem for all items.
* For grouped components, creates one ComponentGroupItem per group.
* For ungrouped components, creates a ComponentGroupItem with a single component.
*/
private List<ComponentGroupItem> buildComponentListWithGroups(List<IDIYComponent<?>> sortedComponents) {
Set<ComponentGroup> groups = plugInPort.getComponentGroups();
// Build a map from component to its group (if any) and group name
Map<IDIYComponent<?>, Set<IDIYComponent<?>>> componentToGroup = new java.util.HashMap<>();
Map<Set<IDIYComponent<?>>, String> groupToName = new java.util.HashMap<>();
if (groups != null && !groups.isEmpty()) {
// Build a map from UUID to component for quick lookup
List<IDIYComponent<?>> allComponents = plugInPort.getCurrentProject().getComponents();
Map<UUID, IDIYComponent<?>> idToComponent = allComponents.stream()
.collect(Collectors.toMap(IDIYComponent::getId, comp -> comp));
for (ComponentGroup group : groups) {
// Convert ComponentGroup (with UUIDs) to Set<IDIYComponent<?>>
Set<IDIYComponent<?>> componentSet = group.getComponentIds().stream()
.map(idToComponent::get)
.filter(java.util.Objects::nonNull)
.collect(Collectors.toSet());
for (IDIYComponent<?> component : componentSet) {
componentToGroup.put(component, componentSet);
}
// Store the group name
if (group.getName() != null && !group.getName().isEmpty()) {
groupToName.put(componentSet, group.getName());
}
}
}
// Track which groups we've already added
Set<Set<IDIYComponent<?>>> addedGroups = new HashSet<>();
List<ComponentGroupItem> result = new ArrayList<>();
for (IDIYComponent<?> component : sortedComponents) {
Set<IDIYComponent<?>> group = componentToGroup.get(component);
if (group == null) {
// Component is not in any group, create a ComponentGroupItem with just this component
Set<IDIYComponent<?>> singleComponentGroup = new HashSet<>();
singleComponentGroup.add(component);
result.add(new ComponentGroupItem(singleComponentGroup, null));
} else {
// Component is in a group
if (!addedGroups.contains(group)) {
// This is the first component from this group we've seen, add the group item
String groupName = groupToName.get(group);
result.add(new ComponentGroupItem(group, groupName));
addedGroups.add(group);
}
// Otherwise, skip this component as we already have a group item for it
}
}
return result;
}
@Override
public int getSize() {
@@ -346,12 +562,12 @@ public class ExplorerPane extends JPanel {
}
@Override
public IDIYComponent<?> getElementAt(int index) {
public ComponentGroupItem getElementAt(int index) {
return this.components.get(index);
}
}
private static class ComponentListCellRenderer implements ListCellRenderer<IDIYComponent<?>> {
private static class ComponentListCellRenderer implements ListCellRenderer<ComponentGroupItem> {
// private static Color[] layerColors = new Color[] { Color.gray, Color.decode("#ff6363"),
// Color.decode("#293462"), Color.decode("#138588"),
@@ -367,32 +583,70 @@ public class ExplorerPane extends JPanel {
}
@Override
public Component getListCellRendererComponent(JList<? extends IDIYComponent<?>> list,
IDIYComponent<?> value, int index, boolean isSelected, boolean cellHasFocus) {
ComponentType componentType = componentTypes.get(value.getClass().getCanonicalName());
public Component getListCellRendererComponent(JList<? extends ComponentGroupItem> list,
ComponentGroupItem value, int index, boolean isSelected, boolean cellHasFocus) {
JLabel renderer = (JLabel) defaultRenderer.getListCellRendererComponent(list, value, index,
isSelected, cellHasFocus);
int layerId = (int) Math.round(componentType.getZOrder());
ComponentGroupItem groupItem = value;
if (groupItem.getComponentCount() == 1) {
// Render as individual component (ungrouped)
IDIYComponent<?> component = groupItem.getGroup().iterator().next();
ComponentType componentType = componentTypes.get(component.getClass().getCanonicalName());
if (componentType == null) {
renderer.setText(component.toString());
return renderer;
}
String valueForDisplay = value.getValueForDisplay();
if (isSelected) {
if (valueForDisplay == null || valueForDisplay.trim().isEmpty()) {
renderer.setText(String.format("[L%s] %s", layerId, value.getName()));
int layerId = (int) Math.round(componentType.getZOrder());
String valueForDisplay = component.getValueForDisplay();
if (isSelected) {
if (valueForDisplay == null || valueForDisplay.trim().isEmpty()) {
renderer.setText(String.format("[L%s] %s", layerId, component.getName()));
} else {
renderer
.setText(String.format("[L%s] %s (%s)", layerId, component.getName(), valueForDisplay));
}
} else {
renderer
.setText(String.format("[L%s] %s (%s)", layerId, value.getName(), valueForDisplay));
if (valueForDisplay == null || valueForDisplay.trim().isEmpty()) {
renderer.setText(String.format("<html><font color='#c0c0c0'>[L%s]</font> %s</html>",
layerId, component.getName()));
} else {
renderer.setText(String.format(
"<html><font color='#c0c0c0'>[L%s]</font> %s (<font color='#666666'>%s</font>)</html>",
layerId, component.getName(), valueForDisplay));
}
}
} else {
if (valueForDisplay == null || valueForDisplay.trim().isEmpty()) {
renderer.setText(String.format("<html><font color='#c0c0c0'>[L%s]</font> %s</html>",
layerId, value.getName()));
// Render as group
// Calculate maximum layer from components in the group
int maxLayerId = 0;
for (IDIYComponent<?> component : groupItem.getGroup()) {
ComponentType componentType = componentTypes.get(component.getClass().getCanonicalName());
if (componentType != null) {
int layerId = (int) Math.round(componentType.getZOrder());
if (layerId > maxLayerId) {
maxLayerId = layerId;
}
}
}
// Format: name (component count), similar to regular components showing name (value)
String groupName = groupItem.getGroupName() != null && !groupItem.getGroupName().isEmpty()
? groupItem.getGroupName()
: "Group";
String componentCount = String.valueOf(groupItem.getComponentCount());
if (isSelected) {
renderer.setText(String.format("<html>[L%s] <b>%s</b> (<b>%s</b>)</html>", maxLayerId, groupName, componentCount));
} else {
renderer.setText(String.format(
"<html><font color='#c0c0c0'>[L%s]</font> %s (<font color='#666666'>%s</font>)</html>",
layerId, value.getName(), valueForDisplay));
maxLayerId, groupName, componentCount));
}
}
@@ -414,4 +668,52 @@ public class ExplorerPane extends JPanel {
return LangUtil.translate(label);
}
}
/**
* Wrapper class to represent a group of components in the explorer list.
*/
public static class ComponentGroupItem {
private final Set<IDIYComponent<?>> group;
private final String groupName;
public ComponentGroupItem(Set<IDIYComponent<?>> group, String groupName) {
this.group = group;
this.groupName = groupName;
}
public Set<IDIYComponent<?>> getGroup() {
return group;
}
public int getComponentCount() {
return group.size();
}
public String getGroupName() {
return groupName;
}
@Override
public String toString() {
if (groupName != null && !groupName.isEmpty()) {
return groupName;
}
return "Group of " + group.size() + " components";
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null || getClass() != obj.getClass())
return false;
ComponentGroupItem other = (ComponentGroupItem) obj;
return group.equals(other.group);
}
@Override
public int hashCode() {
return group.hashCode();
}
}
}
@@ -363,6 +363,7 @@ Shaft Up|Achse nach oben
Shape|Form
Shapes|Formen
Show Grid|Raster anzeigen
Show Resize Dimensions|Größenänderungsdimensionen anzeigen
Show Rulers|Lineale anzeigen
Shroud|Wanne
Shrouded|Abgeschirmt
@@ -352,6 +352,7 @@ Shape
Shapes
Show Bracket
Show Grid
Show Resize Dimensions
Show Rulers
Shroud
Shrouded
@@ -351,6 +351,7 @@ Shaft Up|Eje Arriba
Shape|Forma
Shapes|Formas
Show Grid|Mostrar Cuadrícula
Show Resize Dimensions|Mostrar Dimensiones de Redimensionamiento
Show Rulers|Mostrar Reglas
Shroud|Envoltura
Shrouded|Con Envoltura
@@ -348,6 +348,7 @@ Shaft Up|Tige vers le haut
Shape|Forme
Shapes|Formes
Show Grid|Afficher la grille
Show Resize Dimensions|Afficher les dimensions de redimensionnement
Show Rulers|Afficher les règles
Shroud|Enveloppe
Shrouded|Enveloppé
@@ -363,6 +363,7 @@ Shaft Up|Shaft Up
Shape|forma
Shapes|forme
Show Grid|Mostra griglia
Show Resize Dimensions|Mostra dimensioni ridimensionamento
Show Rulers|Mostra righelli
Shroud|Copri
Shrouded|Coperto
@@ -363,6 +363,7 @@ Shaft Up|As omhoog
Shape|Vorm
Shapes|Vormen
Show Grid|Raster weergeven
Show Resize Dimensions|Grootte-aanpassingsafmetingen weergeven
Show Rulers|Linialen weergeven
Shroud|Beschermrand
Shrouded|Afgeschermd
@@ -351,6 +351,7 @@ Shaft Up|Jogar para cima
Shape|Forma
Shapes|Formas
Show Grid|Mostrar Grid
Show Resize Dimensions|Mostrar Dimensões de Redimensionamento
Show Rulers|Mostrar Réguas
Shroud|Envoltura
Shrouded|Envolto
@@ -344,6 +344,7 @@ Shaft Up|Osovina Gore
Shape|Oblik
Shapes|Oblici
Show Grid|Prikaz Rešetke
Show Resize Dimensions|Prikaz Dimenzija Promene Veličine
Show Rulers|Prikaz Lenjira
Shroud|Kućište
Shrouded|Sa Kućištem
@@ -352,6 +352,7 @@ Shape|आकार
Shapes|आकार
Show Bracket|ब्रैकेट दिखाएं
Show Grid|ग्रिड दिखाएं
Show Resize Dimensions|आकार बदलने के आयाम दिखाएं
Show Rulers|रूलर दिखाएं
Shroud|श्राउड
Shrouded|श्राउडेड
@@ -352,6 +352,7 @@ Shape|形状
Shapes|形状
Show Bracket|显示支架
Show Grid|显示网格
Show Resize Dimensions|显示调整大小尺寸
Show Rulers|显示标尺
Shroud|护罩
Shrouded|带护罩
+1 -1
View File
@@ -6,7 +6,7 @@
<groupId>org.diylc</groupId>
<artifactId>diylc-parent</artifactId>
<version>5.8.0</version>
<version>5.9.0</version>
<packaging>pom</packaging>
<name>DIY Layout Creator</name>