diff --git a/diylc/diylc-core/src/main/java/org/diylc/clipboard/ComponentTransferable.java b/diylc/diylc-core/src/main/java/org/diylc/clipboard/ComponentTransferable.java index cc810d6d..bc881b8f 100644 --- a/diylc/diylc-core/src/main/java/org/diylc/clipboard/ComponentTransferable.java +++ b/diylc/diylc-core/src/main/java/org/diylc/clipboard/ComponentTransferable.java @@ -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> components; - private Set>> groups; + private Set groups; public ComponentTransferable() { super(); @@ -56,7 +57,7 @@ public class ComponentTransferable implements Transferable, Serializable { this.groups = null; } - public ComponentTransferable(List> components, Set>> groups) { + public ComponentTransferable(List> components, Set groups) { super(); this.components = components; this.groups = groups; @@ -84,7 +85,7 @@ public class ComponentTransferable implements Transferable, Serializable { return components; } - public Set>> getGroups() { + public Set getGroups() { return groups; } } diff --git a/diylc/diylc-core/src/main/java/org/diylc/clipboard/ComponentTransferableFactory.java b/diylc/diylc-core/src/main/java/org/diylc/clipboard/ComponentTransferableFactory.java index bae0ca81..bde929c6 100644 --- a/diylc/diylc-core/src/main/java/org/diylc/clipboard/ComponentTransferableFactory.java +++ b/diylc/diylc-core/src/main/java/org/diylc/clipboard/ComponentTransferableFactory.java @@ -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> selectedComponents, Set>> groups) { - Set> originalComponentSet = new HashSet>(selectedComponents); + public ComponentTransferable build(Collection> selectedComponents, Set groups) { + List> originalComponents = new ArrayList>(selectedComponents); + + Map idMap = new HashMap<>(); + List> 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>> clonedGroups = new HashSet>>(); - - for (Set> group : groups) { - if (group.isEmpty()) - continue; - - if (originalComponentSet.contains(group.iterator().next())) { - Set> clonedGroup = new HashSet>(); - clonedGroups.add(clonedGroup); - for (int i = 0; i < originalComponents.size(); i++) { - if (group.contains(originalComponents.get(i))) { - clonedGroup.add(clonedComponents.get(i)); - } - } - } - } + + Set 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); } diff --git a/diylc/diylc-core/src/main/java/org/diylc/common/IGroupProcessor.java b/diylc/diylc-core/src/main/java/org/diylc/common/IGroupProcessor.java index 4b84fcf7..fdb107e5 100644 --- a/diylc/diylc-core/src/main/java/org/diylc/common/IGroupProcessor.java +++ b/diylc/diylc-core/src/main/java/org/diylc/common/IGroupProcessor.java @@ -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>> getComponentGroups(); + Set getComponentGroups(); /** * Groups all selected components. diff --git a/diylc/diylc-core/src/main/java/org/diylc/components/AbstractComponent.java b/diylc/diylc-core/src/main/java/org/diylc/components/AbstractComponent.java index 9a158270..a980c67a 100644 --- a/diylc/diylc-core/src/main/java/org/diylc/components/AbstractComponent.java +++ b/diylc/diylc-core/src/main/java/org/diylc/components/AbstractComponent.java @@ -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 implements IDIYComponent { 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 implements IDIYComponent { 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 implements IDIYComponent { 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() diff --git a/diylc/diylc-core/src/main/java/org/diylc/core/ComponentGroup.java b/diylc/diylc-core/src/main/java/org/diylc/core/ComponentGroup.java new file mode 100644 index 00000000..80372490 --- /dev/null +++ b/diylc/diylc-core/src/main/java/org/diylc/core/ComponentGroup.java @@ -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 componentIds; + private String name; + + public ComponentGroup(Set componentIds) { + this.componentIds = componentIds; + } + + public ComponentGroup(Set componentIds, String name) { + this.componentIds = componentIds; + this.name = name; + } + + public static ComponentGroup from(Collection> components) { + return new ComponentGroup(components.stream() + .map(IDIYComponent::getId).collect(Collectors.toSet())); + } + + public Set getComponentIds() { + return componentIds; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/diylc/diylc-core/src/main/java/org/diylc/core/IDIYComponent.java b/diylc/diylc-core/src/main/java/org/diylc/core/IDIYComponent.java index d3fce225..0ab5721d 100644 --- a/diylc/diylc-core/src/main/java/org/diylc/core/IDIYComponent.java +++ b/diylc/diylc-core/src/main/java/org/diylc/core/IDIYComponent.java @@ -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 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. */ diff --git a/diylc/diylc-core/src/main/java/org/diylc/core/Project.java b/diylc/diylc-core/src/main/java/org/diylc/core/Project.java index 73422c3a..6331b2ee 100644 --- a/diylc/diylc-core/src/main/java/org/diylc/core/Project.java +++ b/diylc/diylc-core/src/main/java/org/diylc/core/Project.java @@ -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> components; private Set>> groups; + private Set groupsEx; private Set lockedLayers; private Set> lockedComponents; private Set hiddenLayers; @@ -75,6 +72,7 @@ public class Project implements Serializable, Cloneable { public Project() { components = new ArrayList>(); groups = new HashSet>>(); + groupsEx = new HashSet<>(); lockedLayers = new HashSet(); hiddenLayers = new HashSet(); lockedComponents = new HashSet>(); @@ -153,22 +151,28 @@ public class Project implements Serializable, Cloneable { /** * List of components sorted by z-order ascending. - * + * s * @return */ public List> getComponents() { return components; } - /** - * Set of grouped components. - * - * @return - */ - public Set>> getGroups() { - return groups; + public Set getGroupsEx() { + if (groupsEx == null) { + groupsEx = new HashSet<>(); + + if (groups != null && !groups.isEmpty()) { + for (Set> group : groups) { + ComponentGroup newGroup = ComponentGroup.from(group); + groupsEx.add(newGroup); + } + groups.clear(); + } + } + return groupsEx; } - + public Set> getLockedComponents() { if (lockedComponents == null) lockedComponents = new HashSet>(); @@ -356,12 +360,8 @@ public class Project implements Serializable, Cloneable { } } - for (Set> group : this.groups) { - Set> cloneGroup = new HashSet>(); - 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()) { diff --git a/diylc/diylc-core/src/main/java/org/diylc/presenter/InstantiationManager.java b/diylc/diylc-core/src/main/java/org/diylc/presenter/InstantiationManager.java index 4b77e91d..fcc31161 100644 --- a/diylc/diylc-core/src/main/java/org/diylc/presenter/InstantiationManager.java +++ b/diylc/diylc-core/src/main/java/org/diylc/presenter/InstantiationManager.java @@ -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()); } } diff --git a/diylc/diylc-core/src/main/java/org/diylc/presenter/Presenter.java b/diylc/diylc-core/src/main/java/org/diylc/presenter/Presenter.java index 318e77cb..22dd058c 100644 --- a/diylc/diylc-core/src/main/java/org/diylc/presenter/Presenter.java +++ b/diylc/diylc-core/src/main/java/org/diylc/presenter/Presenter.java @@ -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>(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>(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> components) { - Iterator>> groupIterator = currentProject.getGroups().iterator(); + Set componentIds = components.stream().map(IDIYComponent::getId).collect(Collectors.toSet()); + Iterator groupIterator = currentProject.getGroupsEx().iterator(); while (groupIterator.hasNext()) { - Set> 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> findAllGroupedComponents(IDIYComponent component) { - Set> components = new HashSet>(); - components.add(component); - for (Set> 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>> getComponentGroups() { - return currentProject.getGroups(); + public Set getComponentGroups() { + return currentProject.getGroupsEx(); } } diff --git a/diylc/diylc-core/src/main/java/org/diylc/serialization/ProjectFileManager.java b/diylc/diylc-core/src/main/java/org/diylc/serialization/ProjectFileManager.java index e3032671..00874444 100644 --- a/diylc/diylc-core/src/main/java/org/diylc/serialization/ProjectFileManager.java +++ b/diylc/diylc-core/src/main/java/org/diylc/serialization/ProjectFileManager.java @@ -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"); diff --git a/diylc/diylc-swing/src/main/java/org/diylc/swing/actions/edit/CopyAction.java b/diylc/diylc-swing/src/main/java/org/diylc/swing/actions/edit/CopyAction.java index 43c7c7a3..5665b162 100644 --- a/diylc/diylc-swing/src/main/java/org/diylc/swing/actions/edit/CopyAction.java +++ b/diylc/diylc-swing/src/main/java/org/diylc/swing/actions/edit/CopyAction.java @@ -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); } -} \ No newline at end of file +} diff --git a/diylc/diylc-swing/src/main/java/org/diylc/swing/actions/edit/CutAction.java b/diylc/diylc-swing/src/main/java/org/diylc/swing/actions/edit/CutAction.java index 048caabe..ef5d6573 100644 --- a/diylc/diylc-swing/src/main/java/org/diylc/swing/actions/edit/CutAction.java +++ b/diylc/diylc-swing/src/main/java/org/diylc/swing/actions/edit/CutAction.java @@ -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(); } diff --git a/diylc/diylc-swing/src/main/java/org/diylc/swing/actions/file/ImportAction.java b/diylc/diylc-swing/src/main/java/org/diylc/swing/actions/file/ImportAction.java index f2f07987..5c06edda 100644 --- a/diylc/diylc-swing/src/main/java/org/diylc/swing/actions/file/ImportAction.java +++ b/diylc/diylc-swing/src/main/java/org/diylc/swing/actions/file/ImportAction.java @@ -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); diff --git a/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/explorer/ExplorerPane.java b/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/explorer/ExplorerPane.java index 1b8d2017..2109fff6 100644 --- a/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/explorer/ExplorerPane.java +++ b/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/explorer/ExplorerPane.java @@ -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>> groups = plugInPort.getComponentGroups(); + Set groups = plugInPort.getComponentGroups(); Map, Set>> componentToGroup = new java.util.HashMap<>(); if (groups != null && !groups.isEmpty()) { - for (Set> group : groups) { - for (IDIYComponent component : group) { - componentToGroup.put(component, group); + // Build a map from UUID to component for quick lookup + List> allComponents = plugInPort.getCurrentProject().getComponents(); + Map> idToComponent = allComponents.stream() + .collect(Collectors.toMap(IDIYComponent::getId, comp -> comp)); + + for (ComponentGroup group : groups) { + // Convert ComponentGroup (with UUIDs) to Set> + Set> 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 buildComponentListWithGroups(List> sortedComponents) { - Set>> groups = plugInPort.getComponentGroups(); + Set 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, Set>> componentToGroup = new java.util.HashMap<>(); + Map>, String> groupToName = new java.util.HashMap<>(); if (groups != null && !groups.isEmpty()) { - for (Set> group : groups) { - for (IDIYComponent component : group) { - componentToGroup.put(component, group); + // Build a map from UUID to component for quick lookup + List> allComponents = plugInPort.getCurrentProject().getComponents(); + Map> idToComponent = allComponents.stream() + .collect(Collectors.toMap(IDIYComponent::getId, comp -> comp)); + + for (ComponentGroup group : groups) { + // Convert ComponentGroup (with UUIDs) to Set> + Set> 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> 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("[L%s] %s", maxLayerId, text)); } else { @@ -636,9 +669,11 @@ public class ExplorerPane extends JPanel { */ public static class ComponentGroupItem { private final Set> group; + private final String groupName; - public ComponentGroupItem(Set> group) { + public ComponentGroupItem(Set> group, String groupName) { this.group = group; + this.groupName = groupName; } public Set> 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"; }