Compare commits

...

5 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
21 changed files with 608 additions and 177 deletions
+1 -1
View File
@@ -7,7 +7,7 @@
<parent> <parent>
<groupId>org.diylc</groupId> <groupId>org.diylc</groupId>
<artifactId>diylc-parent</artifactId> <artifactId>diylc-parent</artifactId>
<version>5.8.0</version> <version>5.9.0</version>
</parent> </parent>
<artifactId>diylc-core</artifactId> <artifactId>diylc-core</artifactId>
@@ -29,6 +29,7 @@ import java.io.Serializable;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import org.diylc.core.ComponentGroup;
import org.diylc.core.IDIYComponent; 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"); public static final DataFlavor listFlavor = new DataFlavor(ComponentTransferable.class, "application/diylc");
private List<IDIYComponent<?>> components; private List<IDIYComponent<?>> components;
private Set<Set<IDIYComponent<?>>> groups; private Set<ComponentGroup> groups;
private String name;
public ComponentTransferable() { public ComponentTransferable() {
super(); super();
} }
public ComponentTransferable(List<IDIYComponent<?>> components) { public ComponentTransferable(List<IDIYComponent<?>> components, String name) {
super(); super();
this.components = components; this.components = components;
this.name = name;
this.groups = null; this.groups = null;
} }
public ComponentTransferable(List<IDIYComponent<?>> components, Set<Set<IDIYComponent<?>>> groups) { public ComponentTransferable(List<IDIYComponent<?>> components, Set<ComponentGroup> groups) {
super(); super();
this.components = components; this.components = components;
this.groups = groups; this.groups = groups;
@@ -84,7 +87,11 @@ public class ComponentTransferable implements Transferable, Serializable {
return components; return components;
} }
public Set<Set<IDIYComponent<?>>> getGroups() { public Set<ComponentGroup> getGroups() {
return groups; return groups;
} }
public String getName() {
return name;
}
} }
@@ -17,15 +17,11 @@
*/ */
package org.diylc.clipboard; package org.diylc.clipboard;
import java.util.ArrayList; import java.util.*;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
import org.diylc.core.ComponentGroup;
import org.diylc.core.IDIYComponent; import org.diylc.core.IDIYComponent;
public class ComponentTransferableFactory { public class ComponentTransferableFactory {
@@ -40,13 +36,19 @@ public class ComponentTransferableFactory {
return instance; return instance;
} }
public ComponentTransferable build(Collection<IDIYComponent<?>> selectedComponents, Set<Set<IDIYComponent<?>>> groups) { public ComponentTransferable build(Collection<IDIYComponent<?>> selectedComponents, Set<ComponentGroup> groups) {
Set<IDIYComponent<?>> originalComponentSet = new HashSet<IDIYComponent<?>>(selectedComponents);
List<IDIYComponent<?>> originalComponents = new ArrayList<IDIYComponent<?>>(selectedComponents); List<IDIYComponent<?>> originalComponents = new ArrayList<IDIYComponent<?>>(selectedComponents);
Map<UUID, UUID> idMap = new HashMap<>();
List<IDIYComponent<?>> clonedComponents = originalComponents.stream( List<IDIYComponent<?>> clonedComponents = originalComponents.stream(
).map(x -> { ).map(x -> {
try { try {
return x.clone(); IDIYComponent<?> clone = x.clone();
clone.setId(UUID.randomUUID());
idMap.put(x.getId(), clone.getId());
return clone;
} catch (CloneNotSupportedException e) { } catch (CloneNotSupportedException e) {
LOG.error(e); LOG.error(e);
return null; return null;
@@ -54,22 +56,11 @@ public class ComponentTransferableFactory {
}) })
.filter(Objects::nonNull) .filter(Objects::nonNull)
.collect(Collectors.toList()); .collect(Collectors.toList());
Set<Set<IDIYComponent<?>>> clonedGroups = new HashSet<Set<IDIYComponent<?>>>();
for (Set<IDIYComponent<?>> group : groups) { Set<ComponentGroup> clonedGroups = groups.stream().map(group ->
if (group.isEmpty()) new ComponentGroup(group.getComponentIds().stream()
continue; .map(idMap::get).collect(Collectors.toSet()), group.getName()))
.collect(Collectors.toSet());
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));
}
}
}
}
return new ComponentTransferable(clonedComponents, clonedGroups); return new ComponentTransferable(clonedComponents, clonedGroups);
} }
@@ -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();
}
@@ -31,7 +31,7 @@ import org.diylc.core.ExpansionMode;
import org.diylc.core.IDIYComponent; import org.diylc.core.IDIYComponent;
import org.diylc.core.measures.Size; import org.diylc.core.measures.Size;
public interface ISelectionProcessor { public interface ISelectionProcessor extends IGroupProcessor {
/** /**
* Returns the current {@link ComponentTransferable}. * Returns the current {@link ComponentTransferable}.
@@ -89,16 +89,6 @@ public interface ISelectionProcessor {
*/ */
void rotateSelection(int direction); void rotateSelection(int direction);
/**
* Groups all selected components.
*/
void groupSelectedComponents();
/**
* Ungroups all selected components.
*/
void ungroupSelectedComponents();
/** /**
* Mirrors selected components in the given axis. * 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.Field;
import java.lang.reflect.Modifier; import java.lang.reflect.Modifier;
import java.util.Arrays; import java.util.Arrays;
import java.util.UUID;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.diylc.core.IDIYComponent; import org.diylc.core.IDIYComponent;
@@ -52,6 +53,7 @@ public abstract class AbstractComponent<T> implements IDIYComponent<T> {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
protected UUID id = UUID.randomUUID();
protected String name = ""; protected String name = "";
public static Color SELECTION_COLOR = Color.red; public static Color SELECTION_COLOR = Color.red;
@@ -78,6 +80,19 @@ public abstract class AbstractComponent<T> implements IDIYComponent<T> {
this.name = name; 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 @Override
public boolean canControlPointOverlap(int index) { public boolean canControlPointOverlap(int index) {
return false; return false;
@@ -186,6 +201,10 @@ public abstract class AbstractComponent<T> implements IDIYComponent<T> {
field.setAccessible(true); field.setAccessible(true);
Object value = field.get(this); Object value = field.get(this);
if (field.getName().equalsIgnoreCase("id")) {
value = getId();
}
// Deep copy point arrays. // Deep copy point arrays.
// TODO: something nicer // TODO: something nicer
if (value != null && value.getClass().isArray() if (value != null && value.getClass().isArray()
@@ -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.Point2D;
import java.awt.geom.Rectangle2D; import java.awt.geom.Rectangle2D;
import java.io.Serializable; import java.io.Serializable;
import java.util.UUID;
import org.diylc.core.annotations.ComponentDescriptor; import org.diylc.core.annotations.ComponentDescriptor;
import org.diylc.core.annotations.EditableProperty; 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 WIRING = 5;
public static final int TEXT = 6; public static final int TEXT = 6;
UUID getId();
void setId(UUID id);
/** /**
* @return component instance name. * @return component instance name.
*/ */
@@ -23,13 +23,9 @@ package org.diylc.core;
import java.awt.Font; import java.awt.Font;
import java.io.Serializable; import java.io.Serializable;
import java.util.ArrayList; import java.util.*;
import java.util.HashMap; import java.util.stream.Collectors;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.diylc.appframework.update.VersionNumber; import org.diylc.appframework.update.VersionNumber;
import org.diylc.core.annotations.EditableProperty; import org.diylc.core.annotations.EditableProperty;
@@ -67,6 +63,7 @@ public class Project implements Serializable, Cloneable {
private Integer dotSpacing; private Integer dotSpacing;
private List<IDIYComponent<?>> components; private List<IDIYComponent<?>> components;
private Set<Set<IDIYComponent<?>>> groups; private Set<Set<IDIYComponent<?>>> groups;
private Set<ComponentGroup> groupsEx;
private Set<Integer> lockedLayers; private Set<Integer> lockedLayers;
private Set<IDIYComponent<?>> lockedComponents; private Set<IDIYComponent<?>> lockedComponents;
private Set<Integer> hiddenLayers; private Set<Integer> hiddenLayers;
@@ -75,6 +72,7 @@ public class Project implements Serializable, Cloneable {
public Project() { public Project() {
components = new ArrayList<IDIYComponent<?>>(); components = new ArrayList<IDIYComponent<?>>();
groups = new HashSet<Set<IDIYComponent<?>>>(); groups = new HashSet<Set<IDIYComponent<?>>>();
groupsEx = new HashSet<>();
lockedLayers = new HashSet<Integer>(); lockedLayers = new HashSet<Integer>();
hiddenLayers = new HashSet<Integer>(); hiddenLayers = new HashSet<Integer>();
lockedComponents = new HashSet<IDIYComponent<?>>(); lockedComponents = new HashSet<IDIYComponent<?>>();
@@ -153,20 +151,26 @@ public class Project implements Serializable, Cloneable {
/** /**
* List of components sorted by z-order ascending. * List of components sorted by z-order ascending.
* * s
* @return * @return
*/ */
public List<IDIYComponent<?>> getComponents() { public List<IDIYComponent<?>> getComponents() {
return components; return components;
} }
/** public Set<ComponentGroup> getGroupsEx() {
* Set of grouped components. if (groupsEx == null) {
* groupsEx = new HashSet<>();
* @return
*/ if (groups != null && !groups.isEmpty()) {
public Set<Set<IDIYComponent<?>>> getGroups() { for (Set<IDIYComponent<?>> group : groups) {
return groups; ComponentGroup newGroup = ComponentGroup.from(group, null);
groupsEx.add(newGroup);
}
groups.clear();
}
}
return groupsEx;
} }
public Set<IDIYComponent<?>> getLockedComponents() { public Set<IDIYComponent<?>> getLockedComponents() {
@@ -292,6 +296,11 @@ public class Project implements Serializable, Cloneable {
return false; return false;
} else if (!groups.equals(other.groups)) } else if (!groups.equals(other.groups))
return false; return false;
if (groupsEx == null) {
if (other.groupsEx != null)
return false;
} else if (!groupsEx.equals(other.groupsEx))
return false;
if (lockedLayers == null) { if (lockedLayers == null) {
if (other.lockedLayers != null) if (other.lockedLayers != null)
return false; return false;
@@ -356,12 +365,8 @@ public class Project implements Serializable, Cloneable {
} }
} }
for (Set<IDIYComponent<?>> group : this.groups) { for (ComponentGroup group : this.getGroupsEx()) {
Set<IDIYComponent<?>> cloneGroup = new HashSet<IDIYComponent<?>>(); project.getGroupsEx().add(new ComponentGroup(group.getComponentIds(), group.getName()));
for (IDIYComponent<?> component : group) {
cloneGroup.add(cloneMap.get(component));
}
project.groups.add(cloneGroup);
} }
for (IDIYComponent<?> component : this.getLockedComponents()) { for (IDIYComponent<?> component : this.getLockedComponents()) {
@@ -24,13 +24,7 @@ package org.diylc.presenter;
import java.awt.geom.Point2D; import java.awt.geom.Point2D;
import java.lang.reflect.Constructor; import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList; import java.util.*;
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.stream.Collectors; import java.util.stream.Collectors;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
import org.diylc.appframework.miscutils.ConfigurationManager; import org.diylc.appframework.miscutils.ConfigurationManager;
@@ -68,9 +62,9 @@ public class InstantiationManager {
public static final ComponentType clipboardType = new ComponentType("Clipboard contents", public static final ComponentType clipboardType = new ComponentType("Clipboard contents",
"Components from the clipboard", CreationMethod.SINGLE_CLICK, "Multi", "", "", null, null, 0, false, null, "Components from the clipboard", CreationMethod.SINGLE_CLICK, "Multi", "", "", null, null, 0, false, null,
false, null, KeywordPolicy.NEVER_SHOW, null, false, null, 0); false, null, KeywordPolicy.NEVER_SHOW, null, false, null, 0);
public static final ComponentType blockType = new ComponentType("Building block", // public static final ComponentType blockType = new ComponentType("Building block",
"Components from the building block", CreationMethod.SINGLE_CLICK, "Multi", "", "", null, null, 0, false, null, // "Components from the building block", CreationMethod.SINGLE_CLICK, "Multi", "", "", null, null, 0, false, null,
false, null, KeywordPolicy.NEVER_SHOW, null, false, null, 0); // false, null, KeywordPolicy.NEVER_SHOW, null, false, null, 0);
public InstantiationManager() {} public InstantiationManager() {}
@@ -213,7 +207,10 @@ public class InstantiationManager {
// Update the component type slot so the app knows that something's // Update the component type slot so the app knows that something's
// being instantiated. // 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) { if (snapToGrid) {
scaledPoint = new Point2D.Double(scaledPoint.getX(), scaledPoint.getY()); scaledPoint = new Point2D.Double(scaledPoint.getX(), scaledPoint.getY());
@@ -224,7 +221,7 @@ public class InstantiationManager {
// copy group information if available // copy group information if available
if (componentTransferable.getGroups() != null) { 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.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.Modifier; import java.lang.reflect.Modifier;
import java.util.ArrayList; import java.util.*;
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.function.Supplier; import java.util.function.Supplier;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.swing.JOptionPane; import javax.swing.JOptionPane;
@@ -553,7 +543,7 @@ public class Presenter implements IPlugInPort {
// group components if there's more than one, e.g. building blocks, but not clipboard // group components if there's more than one, e.g. building blocks, but not clipboard
// contents // contents
if (componentSlot.size() > 1 && !componentTypeSlot.getName().toLowerCase().contains("clipboard")) { 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); notifyProjectModifiedIfNeeded(oldProject, "Add " + componentTypeSlot.getName(), true, true);
@@ -1727,11 +1717,32 @@ public class Presenter implements IPlugInPort {
@Override @Override
public void groupSelectedComponents() { public void groupSelectedComponents() {
LOG.info("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(); 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); ungroupComponents(selectedComponents);
// Then group them together. // Then create a new group containing all selected components with the provided name.
currentProject.getGroups().add(new HashSet<IDIYComponent<?>>(selectedComponents)); // 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. // Notify the listeners.
notifyProjectModifiedIfNeeded(oldProject, "Group", false, true); notifyProjectModifiedIfNeeded(oldProject, "Group", false, true);
} }
@@ -2186,14 +2197,11 @@ public class Presenter implements IPlugInPort {
* @param components * @param components
*/ */
private void ungroupComponents(Collection<IDIYComponent<?>> components) { private void ungroupComponents(Collection<IDIYComponent<?>> components) {
Iterator<Set<IDIYComponent<?>>> groupIterator = currentProject.getGroups().iterator(); Set<UUID> componentIds = components.stream().map(IDIYComponent::getId).collect(Collectors.toSet());
while (groupIterator.hasNext()) { // Remove the group if it contains any of the specified components
Set<IDIYComponent<?>> group = groupIterator.next(); currentProject.getGroupsEx().removeIf(
group.removeAll(components); group -> group.getComponentIds() != null && group.getComponentIds().stream()
if (group.isEmpty()) { .anyMatch(id -> id != null && componentIds.contains(id)));
groupIterator.remove();
}
}
} }
/** /**
@@ -2205,15 +2213,16 @@ public class Presenter implements IPlugInPort {
* the minimum, set contains that single component. * the minimum, set contains that single component.
*/ */
private Set<IDIYComponent<?>> findAllGroupedComponents(IDIYComponent<?> component) { private Set<IDIYComponent<?>> findAllGroupedComponents(IDIYComponent<?> component) {
Set<IDIYComponent<?>> components = new HashSet<IDIYComponent<?>>();
components.add(component); for (ComponentGroup group : currentProject.getGroupsEx()) {
for (Set<IDIYComponent<?>> group : currentProject.getGroups()) { if (group.getComponentIds().stream().anyMatch(id -> Objects.equals(id, component.getId()))) {
if (group.contains(component)) { return currentProject.getComponents().stream()
components.addAll(group); .filter(comp -> group.getComponentIds().contains(comp.getId()))
break; .collect(Collectors.toSet());
} }
} }
return components;
return new HashSet<>(Set.of(component));
} }
@Override @Override
@@ -2747,7 +2756,7 @@ public class Presenter implements IPlugInPort {
@Override @Override
public void loadBlock(String blockName) throws InvalidBlockException { public void loadBlock(String blockName) throws InvalidBlockException {
List<IDIYComponent<?>> components = buildingBlockManager.loadBlock(blockName, currentProject.getComponents()); List<IDIYComponent<?>> components = buildingBlockManager.loadBlock(blockName, currentProject.getComponents());
pasteComponents(new ComponentTransferable(components), true, true); pasteComponents(new ComponentTransferable(components, blockName), true, true);
} }
@Override @Override
@@ -2912,4 +2921,9 @@ public class Presenter implements IPlugInPort {
public DrawingManager getDrawingManager() { public DrawingManager getDrawingManager() {
return drawingManager; return drawingManager;
} }
@Override
public Set<ComponentGroup> getComponentGroups() {
return currentProject.getGroupsEx();
}
} }
@@ -112,6 +112,7 @@ public class ProjectFileManager {
xStream.autodetectAnnotations(true); xStream.autodetectAnnotations(true);
xStream.alias("point", java.awt.geom.Point2D.class); xStream.alias("point", java.awt.geom.Point2D.class);
xStream.alias("point", java.awt.geom.Point2D.Double.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("font", java.awt.Font.class);
xStream.alias("project", Project.class); xStream.alias("project", Project.class);
xStream.aliasPackage("diylc", "org.diylc.components"); xStream.aliasPackage("diylc", "org.diylc.components");
@@ -490,9 +490,17 @@
<minor>9</minor> <minor>9</minor>
<build>0</build> <build>0</build>
</versionNumber> </versionNumber>
<releaseDate>2025-12-30 00:00:00.000 CET</releaseDate> <releaseDate>2026-01-10 00:00:00.000 CET</releaseDate>
<name></name> <name></name>
<changes> <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> <org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType> <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> <description>Added balloon tooltip next to cursor showing component width, height and length (where available) while resizing; can be disabled from Config menu</description>
+1 -1
View File
@@ -7,7 +7,7 @@
<parent> <parent>
<groupId>org.diylc</groupId> <groupId>org.diylc</groupId>
<artifactId>diylc-parent</artifactId> <artifactId>diylc-parent</artifactId>
<version>5.8.0</version> <version>5.9.0</version>
</parent> </parent>
<artifactId>diylc-library</artifactId> <artifactId>diylc-library</artifactId>
+1 -1
View File
@@ -7,7 +7,7 @@
<parent> <parent>
<groupId>org.diylc</groupId> <groupId>org.diylc</groupId>
<artifactId>diylc-parent</artifactId> <artifactId>diylc-parent</artifactId>
<version>5.8.0</version> <version>5.9.0</version>
</parent> </parent>
<artifactId>diylc-swing</artifactId> <artifactId>diylc-swing</artifactId>
@@ -36,7 +36,7 @@ public class CopyAction extends AbstractAction {
public void actionPerformed(ActionEvent e) { public void actionPerformed(ActionEvent e) {
ActionFactory.LOG.info("Copy triggered"); ActionFactory.LOG.info("Copy triggered");
clipboard.setContents(ComponentTransferableFactory.getInstance() clipboard.setContents(ComponentTransferableFactory.getInstance()
.build(plugInPort.getSelectedComponents(), plugInPort.getCurrentProject().getGroups()), .build(plugInPort.getSelectedComponents(), plugInPort.getCurrentProject().getGroupsEx()),
clipboardOwner); clipboardOwner);
} }
} }
@@ -36,7 +36,7 @@ public class CutAction extends AbstractAction {
public void actionPerformed(ActionEvent e) { public void actionPerformed(ActionEvent e) {
ActionFactory.LOG.info("Cut triggered"); ActionFactory.LOG.info("Cut triggered");
clipboard.setContents(ComponentTransferableFactory.getInstance() clipboard.setContents(ComponentTransferableFactory.getInstance()
.build(plugInPort.getSelectedComponents(), plugInPort.getCurrentProject().getGroups()), .build(plugInPort.getSelectedComponents(), plugInPort.getCurrentProject().getGroupsEx()),
clipboardOwner); clipboardOwner);
plugInPort.deleteSelectedComponents(); plugInPort.deleteSelectedComponents();
} }
@@ -88,7 +88,7 @@ public class ImportAction extends AbstractAction {
// presenter // presenter
plugInPort.pasteComponents(ComponentTransferableFactory.getInstance().build( plugInPort.pasteComponents(ComponentTransferableFactory.getInstance().build(
presenter.getCurrentProject().getComponents(), presenter.getCurrentProject().getComponents(),
presenter.getCurrentProject().getGroups()), false, false); presenter.getCurrentProject().getGroupsEx()), false, false);
// Cleanup components in the temp presenter, don't need // Cleanup components in the temp presenter, don't need
// them anymore // them anymore
presenter.selectAll(0); presenter.selectAll(0);
@@ -113,7 +113,7 @@ public class ImportNetlistAction extends AbstractAction {
if (!outputWarnings.isEmpty()) if (!outputWarnings.isEmpty())
ActionFactory.LOG.warn("Component creation produced warnings:\n" ActionFactory.LOG.warn("Component creation produced warnings:\n"
+ String.join("\n", outputWarnings)); + String.join("\n", outputWarnings));
plugInPort.pasteComponents(new ComponentTransferable(components), false, false); plugInPort.pasteComponents(new ComponentTransferable(components, "Imported Netlist"), false, false);
} }
} catch (Exception e) { } catch (Exception e) {
swingUI.showMessage("Could not import netlist file: " + e.getMessage(), "Error", swingUI.showMessage("Could not import netlist file: " + e.getMessage(), "Error",
@@ -18,6 +18,7 @@ import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.swing.AbstractListModel; import javax.swing.AbstractListModel;
import javax.swing.DefaultListCellRenderer; import javax.swing.DefaultListCellRenderer;
@@ -36,6 +37,7 @@ import javax.swing.event.ListSelectionListener;
import org.diylc.common.ComponentType; import org.diylc.common.ComponentType;
import org.diylc.common.IPlugInPort; import org.diylc.common.IPlugInPort;
import org.diylc.core.ComponentGroup;
import org.diylc.core.IDIYComponent; import org.diylc.core.IDIYComponent;
import org.diylc.lang.LangUtil; import org.diylc.lang.LangUtil;
import org.diylc.swing.ISwingUI; import org.diylc.swing.ISwingUI;
@@ -60,9 +62,10 @@ public class ExplorerPane extends JPanel {
private JTextField searchField; private JTextField searchField;
private JScrollPane componentScrollPane; private JScrollPane componentScrollPane;
private ComponentListModel componentListModel; private ComponentListModel componentListModel;
private JList<IDIYComponent<?>> componentList; private JList<ComponentGroupItem> componentList;
private ComponentPopupMenu popupMenu; private ComponentPopupMenu popupMenu;
private JComboBox<Sort> sortBox; private JComboBox<Sort> sortBox;
private ListSelectionListener selectionListener;
public ExplorerPane(ISwingUI swingUI, IPlugInPort plugInPort) { public ExplorerPane(ISwingUI swingUI, IPlugInPort plugInPort) {
super(); super();
@@ -144,9 +147,9 @@ public class ExplorerPane extends JPanel {
return componentScrollPane; return componentScrollPane;
} }
public JList<IDIYComponent<?>> getComponentList() { public JList<ComponentGroupItem> getComponentList() {
if (componentList == null) { if (componentList == null) {
componentList = new DragDropList<IDIYComponent<?>>(new IDragDropListListener() { componentList = new DragDropList<ComponentGroupItem>(new IDragDropListListener() {
@Override @Override
public boolean dropComplete(int selectedIndex, int dropTargetIndex) { 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); swingUI.showMessage(REORDERING_NOT_POSSIBLE, "Error", ISwingUI.ERROR_MESSAGE);
return false; 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; return true;
} }
}); });
@@ -162,20 +240,28 @@ public class ExplorerPane extends JPanel {
componentList.setFont(DEFAULT_FONT); componentList.setFont(DEFAULT_FONT);
componentList.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); componentList.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
componentList.setCellRenderer(new ComponentListCellRenderer(plugInPort.getComponentTypes())); componentList.setCellRenderer(new ComponentListCellRenderer(plugInPort.getComponentTypes()));
componentList.addListSelectionListener(new ListSelectionListener() { selectionListener = new ListSelectionListener() {
@Override @Override
public void valueChanged(ListSelectionEvent e) { public void valueChanged(ListSelectionEvent e) {
if (e.getValueIsAdjusting()) if (e.getValueIsAdjusting())
return; return;
List<IDIYComponent<?>> selectedValuesList = componentList.getSelectedValuesList(); List<ComponentGroupItem> selectedValuesList = componentList.getSelectedValuesList();
if (selectedValuesList.size() > 0 && !new HashSet<IDIYComponent<?>>(selectedValuesList) 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()))) { .equals(new HashSet<IDIYComponent<?>>(plugInPort.getSelectedComponents()))) {
plugInPort.setSelection(selectedValuesList, true); plugInPort.setSelection(expandedSelection, true);
} }
} }
}); };
componentList.addListSelectionListener(selectionListener);
componentList.addMouseListener(new MouseAdapter() { componentList.addMouseListener(new MouseAdapter() {
@@ -205,8 +291,13 @@ public class ExplorerPane extends JPanel {
if (plugInPort.getNewComponentTypeSlot() == null && (e.isPopupTrigger() if (plugInPort.getNewComponentTypeSlot() == null && (e.isPopupTrigger()
|| (pressedEvent != null && pressedEvent.isPopupTrigger()))) { || (pressedEvent != null && pressedEvent.isPopupTrigger()))) {
final List<IDIYComponent<?>> selectedValues = componentList.getSelectedValuesList(); final List<ComponentGroupItem> selectedValues = componentList.getSelectedValuesList();
getComponentPopupMenu().prepareAndShowAt(selectedValues, selectedValues, e.getX(), // 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()); e.getY());
} }
} }
@@ -238,46 +329,105 @@ public class ExplorerPane extends JPanel {
} }
public void setSelection(Set<IDIYComponent<?>> selectedComponents) { public void setSelection(Set<IDIYComponent<?>> selectedComponents) {
List<IDIYComponent<?>> selectedValuesList = componentList.getSelectedValuesList(); List<ComponentGroupItem> selectedValuesList = componentList.getSelectedValuesList();
if (new HashSet<IDIYComponent<?>>(selectedValuesList)
.equals(new HashSet<IDIYComponent<?>>(plugInPort.getSelectedComponents()))) // Extract components from selected values
Set<IDIYComponent<?>> currentSelected = new HashSet<>();
for (ComponentGroupItem item : selectedValuesList) {
currentSelected.addAll(item.getGroup());
}
if (currentSelected.equals(selectedComponents))
return; return;
final JList<IDIYComponent<?>> list = this.getComponentList(); final JList<ComponentGroupItem> list = this.getComponentList();
if (selectedComponents.size() == 0) { // Temporarily remove listener to prevent infinite loop
list.clearSelection(); list.removeListSelectionListener(selectionListener);
} else { 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));
int[] indices = new int[selectedComponents.size()]; for (ComponentGroup group : groups) {
int n = 0; // Convert ComponentGroup (with UUIDs) to Set<IDIYComponent<?>>
List<IDIYComponent<?>> components = this.getComponentListModel().getComponents(); Set<IDIYComponent<?>> componentSet = group.getComponentIds().stream()
for (int i = 0; i < components.size(); i++) { .map(idToComponent::get)
indices[n] = -1; // this should not happen .filter(java.util.Objects::nonNull)
if (selectedComponents.contains(components.get(i))) .collect(Collectors.toSet());
indices[n++] = i;
if (n == indices.length) for (IDIYComponent<?> component : componentSet) {
break; componentToGroup.put(component, componentSet);
}
}
}
// 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]);
}
} }
} finally {
list.getSelectionModel().setValueIsAdjusting(true); // Re-add listener
list.setSelectedIndices(indices); list.addListSelectionListener(selectionListener);
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]);
} }
} }
private class ComponentListModel extends AbstractListModel<IDIYComponent<?>> { private class ComponentListModel extends AbstractListModel<ComponentGroupItem> {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
private List<IDIYComponent<?>> componentsRaw; private List<IDIYComponent<?>> componentsRaw;
private List<IDIYComponent<?>> components; private List<ComponentGroupItem> components;
private String searchText; private String searchText;
private Sort sort = Sort.Z_INDEX; private Sort sort = Sort.Z_INDEX;
@@ -289,7 +439,7 @@ public class ExplorerPane extends JPanel {
refresh(oldSize); refresh(oldSize);
} }
public List<IDIYComponent<?>> getComponents() { public List<ComponentGroupItem> getComponents() {
return components; return components;
} }
@@ -310,27 +460,32 @@ public class ExplorerPane extends JPanel {
} }
private void refresh(int oldSize) { private void refresh(int oldSize) {
List<IDIYComponent<?>> sortedComponents;
if (sort == Sort.NAME) { if (sort == Sort.NAME) {
this.components = sortedComponents =
componentsRaw.stream().sorted(Comparator.comparing(x -> x.toString().toLowerCase())) componentsRaw.stream().sorted(Comparator.comparing(x -> x.toString().toLowerCase()))
.collect(Collectors.toList()); .collect(Collectors.toList());
} else if (sort == Sort.TYPE) { } else if (sort == Sort.TYPE) {
this.components = componentsRaw.stream() sortedComponents = componentsRaw.stream()
.sorted(Comparator.comparing(x -> x.getClass().getName().toLowerCase()) .sorted(Comparator.comparing(x -> x.getClass().getName().toLowerCase())
.thenComparing(x -> x.toString().toLowerCase())) .thenComparing(x -> x.toString().toLowerCase()))
.collect(Collectors.toList()); .collect(Collectors.toList());
} else { } else {
List<IDIYComponent<?>> componentsClone = new ArrayList<IDIYComponent<?>>(componentsRaw); List<IDIYComponent<?>> componentsClone = new ArrayList<IDIYComponent<?>>(componentsRaw);
Collections.reverse(componentsClone); Collections.reverse(componentsClone);
this.components = componentsClone; sortedComponents = componentsClone;
} }
if (searchText != null && !searchText.trim().isEmpty()) { if (searchText != null && !searchText.trim().isEmpty()) {
this.components = this.components.stream().filter(component -> { sortedComponents = sortedComponents.stream().filter(component -> {
return component.getName().toLowerCase().contains(searchText); return component.getName().toLowerCase().contains(searchText);
}).collect(Collectors.toList()); }).collect(Collectors.toList());
} }
// Build list with groups and ungrouped components
this.components = buildComponentListWithGroups(sortedComponents);
int newSize = this.components.size(); int newSize = this.components.size();
fireContentsChanged(this, 0, Math.min(newSize, oldSize)); fireContentsChanged(this, 0, Math.min(newSize, oldSize));
@@ -340,18 +495,79 @@ public class ExplorerPane extends JPanel {
fireIntervalAdded(this, oldSize, newSize); 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 @Override
public int getSize() { public int getSize() {
return this.components == null ? 0 : this.components.size(); return this.components == null ? 0 : this.components.size();
} }
@Override @Override
public IDIYComponent<?> getElementAt(int index) { public ComponentGroupItem getElementAt(int index) {
return this.components.get(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"), // private static Color[] layerColors = new Color[] { Color.gray, Color.decode("#ff6363"),
// Color.decode("#293462"), Color.decode("#138588"), // Color.decode("#293462"), Color.decode("#138588"),
@@ -367,32 +583,70 @@ public class ExplorerPane extends JPanel {
} }
@Override @Override
public Component getListCellRendererComponent(JList<? extends IDIYComponent<?>> list, public Component getListCellRendererComponent(JList<? extends ComponentGroupItem> list,
IDIYComponent<?> value, int index, boolean isSelected, boolean cellHasFocus) { ComponentGroupItem value, int index, boolean isSelected, boolean cellHasFocus) {
ComponentType componentType = componentTypes.get(value.getClass().getCanonicalName());
JLabel renderer = (JLabel) defaultRenderer.getListCellRendererComponent(list, value, index, JLabel renderer = (JLabel) defaultRenderer.getListCellRendererComponent(list, value, index,
isSelected, cellHasFocus); isSelected, cellHasFocus);
int layerId = (int) Math.round(componentType.getZOrder()); ComponentGroupItem groupItem = value;
String valueForDisplay = value.getValueForDisplay(); if (groupItem.getComponentCount() == 1) {
if (isSelected) { // Render as individual component (ungrouped)
if (valueForDisplay == null || valueForDisplay.trim().isEmpty()) { IDIYComponent<?> component = groupItem.getGroup().iterator().next();
renderer.setText(String.format("[L%s] %s", layerId, value.getName())); ComponentType componentType = componentTypes.get(component.getClass().getCanonicalName());
if (componentType == null) {
renderer.setText(component.toString());
return renderer;
}
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 { } else {
renderer if (valueForDisplay == null || valueForDisplay.trim().isEmpty()) {
.setText(String.format("[L%s] %s (%s)", layerId, value.getName(), valueForDisplay)); 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 { } else {
if (valueForDisplay == null || valueForDisplay.trim().isEmpty()) { // Render as group
renderer.setText(String.format("<html><font color='#c0c0c0'>[L%s]</font> %s</html>", // Calculate maximum layer from components in the group
layerId, value.getName())); 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 { } else {
renderer.setText(String.format( renderer.setText(String.format(
"<html><font color='#c0c0c0'>[L%s]</font> %s (<font color='#666666'>%s</font>)</html>", "<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); 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();
}
}
} }
+1 -1
View File
@@ -6,7 +6,7 @@
<groupId>org.diylc</groupId> <groupId>org.diylc</groupId>
<artifactId>diylc-parent</artifactId> <artifactId>diylc-parent</artifactId>
<version>5.8.0</version> <version>5.9.0</version>
<packaging>pom</packaging> <packaging>pom</packaging>
<name>DIY Layout Creator</name> <name>DIY Layout Creator</name>