introduce component ids. Refactor how groups are modeled in the Project

This commit is contained in:
Bane Stojkovic
2026-01-07 23:08:04 +01:00
parent 7da4444c18
commit a8e8e72458
14 changed files with 189 additions and 99 deletions
@@ -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,7 +45,7 @@ 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;
public ComponentTransferable() {
super();
@@ -56,7 +57,7 @@ public class ComponentTransferable implements Transferable, Serializable {
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 +85,7 @@ public class ComponentTransferable implements Transferable, Serializable {
return components;
}
public Set<Set<IDIYComponent<?>>> getGroups() {
public Set<ComponentGroup> getGroups() {
return groups;
}
}
@@ -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,8 @@
package org.diylc.common;
import java.util.Set;
import org.diylc.core.IDIYComponent;
import org.diylc.core.ComponentGroup;
/**
* Interface for component grouping operations.
@@ -31,12 +32,12 @@ import org.diylc.core.IDIYComponent;
public interface IGroupProcessor {
/**
* Returns the set of component groups in the current project.
* Each group is a set of components that are grouped together.
*
* 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<Set<IDIYComponent<?>>> getComponentGroups();
Set<ComponentGroup> getComponentGroups();
/**
* Groups all selected components.
@@ -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()
@@ -0,0 +1,39 @@
package org.diylc.core;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
public class ComponentGroup {
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) {
return new ComponentGroup(components.stream()
.map(IDIYComponent::getId).collect(Collectors.toSet()));
}
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.
*/
@@ -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);
groupsEx.add(newGroup);
}
groups.clear();
}
}
return groupsEx;
}
public Set<IDIYComponent<?>> getLockedComponents() {
if (lockedComponents == null)
lockedComponents = new HashSet<IDIYComponent<?>>();
@@ -356,12 +360,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()) {
@@ -224,7 +224,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;
@@ -553,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));
}
notifyProjectModifiedIfNeeded(oldProject, "Add " + componentTypeSlot.getName(), true, true);
@@ -1731,7 +1721,7 @@ public class Presenter implements IPlugInPort {
// First remove the selected components from other groups.
ungroupComponents(selectedComponents);
// Then group them together.
currentProject.getGroups().add(new HashSet<IDIYComponent<?>>(selectedComponents));
currentProject.getGroupsEx().add(ComponentGroup.from(selectedComponents));
// Notify the listeners.
notifyProjectModifiedIfNeeded(oldProject, "Group", false, true);
}
@@ -2186,11 +2176,11 @@ public class Presenter implements IPlugInPort {
* @param 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());
Iterator<ComponentGroup> groupIterator = currentProject.getGroupsEx().iterator();
while (groupIterator.hasNext()) {
Set<IDIYComponent<?>> group = groupIterator.next();
group.removeAll(components);
if (group.isEmpty()) {
ComponentGroup group = groupIterator.next();
if (group.getComponentIds().stream().anyMatch(id -> componentIds.contains(componentIds))) {
groupIterator.remove();
}
}
@@ -2205,15 +2195,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 -> id.equals(component.getId()))) {
return currentProject.getComponents().stream()
.filter(comp -> group.getComponentIds().contains(comp.getId()))
.collect(Collectors.toSet());
}
}
return components;
return new HashSet<>();
}
@Override
@@ -2914,7 +2905,7 @@ public class Presenter implements IPlugInPort {
}
@Override
public Set<Set<IDIYComponent<?>>> getComponentGroups() {
return currentProject.getGroups();
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");
@@ -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();
}
@@ -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);
@@ -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;
@@ -347,12 +349,23 @@ public class ExplorerPane extends JPanel {
list.clearSelection();
} else {
// Build a map from component to its group (if any)
Set<Set<IDIYComponent<?>>> groups = plugInPort.getComponentGroups();
Set<ComponentGroup> groups = plugInPort.getComponentGroups();
Map<IDIYComponent<?>, Set<IDIYComponent<?>>> componentToGroup = new java.util.HashMap<>();
if (groups != null && !groups.isEmpty()) {
for (Set<IDIYComponent<?>> group : groups) {
for (IDIYComponent<?> component : group) {
componentToGroup.put(component, group);
// 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);
}
}
}
@@ -488,14 +501,31 @@ public class ExplorerPane extends JPanel {
* For ungrouped components, creates a ComponentGroupItem with a single component.
*/
private List<ComponentGroupItem> buildComponentListWithGroups(List<IDIYComponent<?>> sortedComponents) {
Set<Set<IDIYComponent<?>>> groups = plugInPort.getComponentGroups();
Set<ComponentGroup> groups = plugInPort.getComponentGroups();
// Build a map from component to its group (if any)
// 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()) {
for (Set<IDIYComponent<?>> group : groups) {
for (IDIYComponent<?> component : group) {
componentToGroup.put(component, group);
// 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());
}
}
}
@@ -510,12 +540,13 @@ public class ExplorerPane extends JPanel {
// 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));
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
result.add(new ComponentGroupItem(group));
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
@@ -604,7 +635,9 @@ public class ExplorerPane extends JPanel {
}
}
String text = "Group of " + groupItem.getComponentCount() + " components";
String text = groupItem.getGroupName() != null && !groupItem.getGroupName().isEmpty()
? groupItem.getGroupName()
: "Group of " + groupItem.getComponentCount() + " components";
if (isSelected) {
renderer.setText(String.format("<html>[L%s] <b>%s</b></html>", maxLayerId, text));
} else {
@@ -636,9 +669,11 @@ public class ExplorerPane extends JPanel {
*/
public static class ComponentGroupItem {
private final Set<IDIYComponent<?>> group;
private final String groupName;
public ComponentGroupItem(Set<IDIYComponent<?>> group) {
public ComponentGroupItem(Set<IDIYComponent<?>> group, String groupName) {
this.group = group;
this.groupName = groupName;
}
public Set<IDIYComponent<?>> getGroup() {
@@ -649,8 +684,15 @@ public class ExplorerPane extends JPanel {
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";
}