Explorer group (#992)

* show groups in explorer

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

* Group naming added

* cleanup

* release
This commit is contained in:
Bane Stojkovic
2026-01-10 20:44:52 +01:00
committed by GitHub
parent 2383f9ae77
commit 1cef0c0977
21 changed files with 608 additions and 177 deletions
+1 -1
View File
@@ -7,7 +7,7 @@
<parent>
<groupId>org.diylc</groupId>
<artifactId>diylc-parent</artifactId>
<version>5.8.0</version>
<version>5.9.0</version>
</parent>
<artifactId>diylc-swing</artifactId>
@@ -36,7 +36,7 @@ public class CopyAction extends AbstractAction {
public void actionPerformed(ActionEvent e) {
ActionFactory.LOG.info("Copy triggered");
clipboard.setContents(ComponentTransferableFactory.getInstance()
.build(plugInPort.getSelectedComponents(), plugInPort.getCurrentProject().getGroups()),
.build(plugInPort.getSelectedComponents(), plugInPort.getCurrentProject().getGroupsEx()),
clipboardOwner);
}
}
}
@@ -36,7 +36,7 @@ public class CutAction extends AbstractAction {
public void actionPerformed(ActionEvent e) {
ActionFactory.LOG.info("Cut triggered");
clipboard.setContents(ComponentTransferableFactory.getInstance()
.build(plugInPort.getSelectedComponents(), plugInPort.getCurrentProject().getGroups()),
.build(plugInPort.getSelectedComponents(), plugInPort.getCurrentProject().getGroupsEx()),
clipboardOwner);
plugInPort.deleteSelectedComponents();
}
@@ -88,7 +88,7 @@ public class ImportAction extends AbstractAction {
// presenter
plugInPort.pasteComponents(ComponentTransferableFactory.getInstance().build(
presenter.getCurrentProject().getComponents(),
presenter.getCurrentProject().getGroups()), false, false);
presenter.getCurrentProject().getGroupsEx()), false, false);
// Cleanup components in the temp presenter, don't need
// them anymore
presenter.selectAll(0);
@@ -113,7 +113,7 @@ public class ImportNetlistAction extends AbstractAction {
if (!outputWarnings.isEmpty())
ActionFactory.LOG.warn("Component creation produced warnings:\n"
+ String.join("\n", outputWarnings));
plugInPort.pasteComponents(new ComponentTransferable(components), false, false);
plugInPort.pasteComponents(new ComponentTransferable(components, "Imported Netlist"), false, false);
}
} catch (Exception e) {
swingUI.showMessage("Could not import netlist file: " + e.getMessage(), "Error",
@@ -130,4 +130,4 @@ public class ImportNetlistAction extends AbstractAction {
}, true);
}
}
}
}
@@ -18,6 +18,7 @@ import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import javax.swing.AbstractListModel;
import javax.swing.DefaultListCellRenderer;
@@ -36,6 +37,7 @@ import javax.swing.event.ListSelectionListener;
import org.diylc.common.ComponentType;
import org.diylc.common.IPlugInPort;
import org.diylc.core.ComponentGroup;
import org.diylc.core.IDIYComponent;
import org.diylc.lang.LangUtil;
import org.diylc.swing.ISwingUI;
@@ -60,9 +62,10 @@ public class ExplorerPane extends JPanel {
private JTextField searchField;
private JScrollPane componentScrollPane;
private ComponentListModel componentListModel;
private JList<IDIYComponent<?>> componentList;
private JList<ComponentGroupItem> componentList;
private ComponentPopupMenu popupMenu;
private JComboBox<Sort> sortBox;
private ListSelectionListener selectionListener;
public ExplorerPane(ISwingUI swingUI, IPlugInPort plugInPort) {
super();
@@ -144,9 +147,9 @@ public class ExplorerPane extends JPanel {
return componentScrollPane;
}
public JList<IDIYComponent<?>> getComponentList() {
public JList<ComponentGroupItem> getComponentList() {
if (componentList == null) {
componentList = new DragDropList<IDIYComponent<?>>(new IDragDropListListener() {
componentList = new DragDropList<ComponentGroupItem>(new IDragDropListListener() {
@Override
public boolean dropComplete(int selectedIndex, int dropTargetIndex) {
@@ -154,7 +157,82 @@ public class ExplorerPane extends JPanel {
swingUI.showMessage(REORDERING_NOT_POSSIBLE, "Error", ISwingUI.ERROR_MESSAGE);
return false;
}
plugInPort.moveSelectionToZIndex(getComponentListModel().getSize() - dropTargetIndex);
// The list is displayed in reverse order (highest z-index first, index 0 = last in component list)
// Component list is in normal order (lowest z-index first, index 0 = first component)
// In INSERT mode, dropTargetIndex is where the item will be inserted AFTER removal
int listSize = getComponentListModel().getSize();
int componentCount = plugInPort.getCurrentProject().getComponents().size();
// The list size may be different from component count due to groups
// listSize = componentCount - groupedComponents + groups
// We need to find which component will be at dropTargetIndex in the final list
// and convert that to a component index
// Find the ComponentGroupItem that will be at dropTargetIndex after removal
// We want to insert before the component that will be at dropTargetIndex after insertion
List<ComponentGroupItem> components = getComponentListModel().getComponents();
int componentAtTargetListIndex;
if (dropTargetIndex >= listSize - 1) {
// Dropping at the end
plugInPort.moveSelectionToZIndex(componentCount);
return true;
} else if (selectedIndex < dropTargetIndex) {
// Dragging up: removal happens before target
// After removal, the component that was at dropTargetIndex is now at dropTargetIndex-1
// After insertion at dropTargetIndex, it will be at dropTargetIndex+1
// So we want to insert before the component currently at dropTargetIndex
componentAtTargetListIndex = dropTargetIndex;
} else {
// Dragging down: removal happens after target
// After removal, the component at dropTargetIndex stays at dropTargetIndex
// After insertion at dropTargetIndex, it will be at dropTargetIndex+1
// So we want to insert before the component currently at dropTargetIndex
componentAtTargetListIndex = dropTargetIndex;
}
if (componentAtTargetListIndex >= listSize) {
// Insert at the end
plugInPort.moveSelectionToZIndex(componentCount);
return true;
}
// Get the ComponentGroupItem at that position
ComponentGroupItem targetItem = components.get(componentAtTargetListIndex);
// Find the highest z-index component in this group (or the component itself if single)
// Since the list is sorted by z-index (reversed), this is the component that determines
// the position of this group in the list
IDIYComponent<?> targetComponent = null;
int maxComponentIndex = -1;
List<IDIYComponent<?>> allComponents = plugInPort.getCurrentProject().getComponents();
for (IDIYComponent<?> comp : targetItem.getGroup()) {
int compIndex = allComponents.indexOf(comp);
if (compIndex > maxComponentIndex) {
maxComponentIndex = compIndex;
targetComponent = comp;
}
}
if (targetComponent == null || maxComponentIndex < 0) {
return false;
}
// We want to insert before the component at maxComponentIndex
// moveSelectionToZIndex will automatically decrement zIndex for each selected component
// that's before the target, so we can use maxComponentIndex directly
// However, we need to add 1 because we want to insert at dropTargetIndex, not before it
int targetComponentIndex = maxComponentIndex + 1;
if (targetComponentIndex < 0) {
targetComponentIndex = 0;
} else if (targetComponentIndex > componentCount) {
targetComponentIndex = componentCount;
}
plugInPort.moveSelectionToZIndex(targetComponentIndex);
return true;
}
});
@@ -162,20 +240,28 @@ public class ExplorerPane extends JPanel {
componentList.setFont(DEFAULT_FONT);
componentList.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
componentList.setCellRenderer(new ComponentListCellRenderer(plugInPort.getComponentTypes()));
componentList.addListSelectionListener(new ListSelectionListener() {
selectionListener = new ListSelectionListener() {
@Override
public void valueChanged(ListSelectionEvent e) {
if (e.getValueIsAdjusting())
return;
List<IDIYComponent<?>> selectedValuesList = componentList.getSelectedValuesList();
if (selectedValuesList.size() > 0 && !new HashSet<IDIYComponent<?>>(selectedValuesList)
List<ComponentGroupItem> selectedValuesList = componentList.getSelectedValuesList();
Set<IDIYComponent<?>> expandedSelection = new HashSet<>();
for (ComponentGroupItem selected : selectedValuesList) {
// Always add all components from the group item
expandedSelection.addAll(selected.getGroup());
}
if (expandedSelection.size() > 0 && !expandedSelection
.equals(new HashSet<IDIYComponent<?>>(plugInPort.getSelectedComponents()))) {
plugInPort.setSelection(selectedValuesList, true);
plugInPort.setSelection(expandedSelection, true);
}
}
});
};
componentList.addListSelectionListener(selectionListener);
componentList.addMouseListener(new MouseAdapter() {
@@ -205,8 +291,13 @@ public class ExplorerPane extends JPanel {
if (plugInPort.getNewComponentTypeSlot() == null && (e.isPopupTrigger()
|| (pressedEvent != null && pressedEvent.isPopupTrigger()))) {
final List<IDIYComponent<?>> selectedValues = componentList.getSelectedValuesList();
getComponentPopupMenu().prepareAndShowAt(selectedValues, selectedValues, e.getX(),
final List<ComponentGroupItem> selectedValues = componentList.getSelectedValuesList();
// Extract actual components from selected items
List<IDIYComponent<?>> components = new ArrayList<>();
for (ComponentGroupItem item : selectedValues) {
components.addAll(item.getGroup());
}
getComponentPopupMenu().prepareAndShowAt(components, components, e.getX(),
e.getY());
}
}
@@ -238,46 +329,105 @@ public class ExplorerPane extends JPanel {
}
public void setSelection(Set<IDIYComponent<?>> selectedComponents) {
List<IDIYComponent<?>> selectedValuesList = componentList.getSelectedValuesList();
if (new HashSet<IDIYComponent<?>>(selectedValuesList)
.equals(new HashSet<IDIYComponent<?>>(plugInPort.getSelectedComponents())))
List<ComponentGroupItem> selectedValuesList = componentList.getSelectedValuesList();
// Extract components from selected values
Set<IDIYComponent<?>> currentSelected = new HashSet<>();
for (ComponentGroupItem item : selectedValuesList) {
currentSelected.addAll(item.getGroup());
}
if (currentSelected.equals(selectedComponents))
return;
final JList<IDIYComponent<?>> list = this.getComponentList();
final JList<ComponentGroupItem> list = this.getComponentList();
if (selectedComponents.size() == 0) {
list.clearSelection();
} else {
// Temporarily remove listener to prevent infinite loop
list.removeListSelectionListener(selectionListener);
try {
if (selectedComponents.size() == 0) {
list.clearSelection();
} else {
// Build a map from component to its group (if any)
Set<ComponentGroup> groups = plugInPort.getComponentGroups();
Map<IDIYComponent<?>, Set<IDIYComponent<?>>> componentToGroup = new java.util.HashMap<>();
if (groups != null && !groups.isEmpty()) {
// Build a map from UUID to component for quick lookup
List<IDIYComponent<?>> allComponents = plugInPort.getCurrentProject().getComponents();
Map<UUID, IDIYComponent<?>> idToComponent = allComponents.stream()
.collect(Collectors.toMap(IDIYComponent::getId, comp -> comp));
for (ComponentGroup group : groups) {
// Convert ComponentGroup (with UUIDs) to Set<IDIYComponent<?>>
Set<IDIYComponent<?>> componentSet = group.getComponentIds().stream()
.map(idToComponent::get)
.filter(java.util.Objects::nonNull)
.collect(Collectors.toSet());
for (IDIYComponent<?> component : componentSet) {
componentToGroup.put(component, componentSet);
}
}
}
int[] indices = new int[selectedComponents.size()];
int n = 0;
List<IDIYComponent<?>> components = this.getComponentListModel().getComponents();
for (int i = 0; i < components.size(); i++) {
indices[n] = -1; // this should not happen
if (selectedComponents.contains(components.get(i)))
indices[n++] = i;
if (n == indices.length)
break;
// Find indices of items to select
List<ComponentGroupItem> components = this.getComponentListModel().getComponents();
Set<Integer> selectedIndices = new HashSet<>();
Set<Set<IDIYComponent<?>>> selectedGroups = new HashSet<>();
for (IDIYComponent<?> selected : selectedComponents) {
Set<IDIYComponent<?>> group = componentToGroup.get(selected);
if (group != null) {
// Component is in a group
if (!selectedGroups.contains(group)) {
// Find the ComponentGroupItem for this group
for (int i = 0; i < components.size(); i++) {
ComponentGroupItem groupItem = components.get(i);
if (groupItem.getGroup().equals(group)) {
selectedIndices.add(i);
selectedGroups.add(group);
break;
}
}
}
} else {
// Component is not in any group, find the ComponentGroupItem that contains only this component
for (int i = 0; i < components.size(); i++) {
ComponentGroupItem groupItem = components.get(i);
if (groupItem.getGroup().size() == 1 && groupItem.getGroup().contains(selected)) {
selectedIndices.add(i);
break;
}
}
}
}
int[] indices = selectedIndices.stream().mapToInt(i -> i).toArray();
list.getSelectionModel().setValueIsAdjusting(true);
list.setSelectedIndices(indices);
list.getSelectionModel().setValueIsAdjusting(false);
if (indices.length > 0) {
int firstVisibleIndex = list.getFirstVisibleIndex();
int lastVisibleIndex = list.getLastVisibleIndex();
// if none of the selected items is visible, jump to the first one
if (!Arrays.stream(indices)
.anyMatch(index -> firstVisibleIndex <= index && index <= lastVisibleIndex))
list.ensureIndexIsVisible(indices[0]);
}
}
list.getSelectionModel().setValueIsAdjusting(true);
list.setSelectedIndices(indices);
list.getSelectionModel().setValueIsAdjusting(false);
int firstVisibleIndex = list.getFirstVisibleIndex();
int lastVisibleIndex = list.getFirstVisibleIndex();
// if none of the selected items is visible, jump to the first one
if (!Arrays.stream(indices)
.anyMatch(index -> firstVisibleIndex <= index && index <= lastVisibleIndex))
list.ensureIndexIsVisible(indices[0]);
} finally {
// Re-add listener
list.addListSelectionListener(selectionListener);
}
}
private class ComponentListModel extends AbstractListModel<IDIYComponent<?>> {
private class ComponentListModel extends AbstractListModel<ComponentGroupItem> {
private static final long serialVersionUID = 1L;
private List<IDIYComponent<?>> componentsRaw;
private List<IDIYComponent<?>> components;
private List<ComponentGroupItem> components;
private String searchText;
private Sort sort = Sort.Z_INDEX;
@@ -289,7 +439,7 @@ public class ExplorerPane extends JPanel {
refresh(oldSize);
}
public List<IDIYComponent<?>> getComponents() {
public List<ComponentGroupItem> getComponents() {
return components;
}
@@ -310,27 +460,32 @@ public class ExplorerPane extends JPanel {
}
private void refresh(int oldSize) {
List<IDIYComponent<?>> sortedComponents;
if (sort == Sort.NAME) {
this.components =
sortedComponents =
componentsRaw.stream().sorted(Comparator.comparing(x -> x.toString().toLowerCase()))
.collect(Collectors.toList());
} else if (sort == Sort.TYPE) {
this.components = componentsRaw.stream()
sortedComponents = componentsRaw.stream()
.sorted(Comparator.comparing(x -> x.getClass().getName().toLowerCase())
.thenComparing(x -> x.toString().toLowerCase()))
.collect(Collectors.toList());
} else {
List<IDIYComponent<?>> componentsClone = new ArrayList<IDIYComponent<?>>(componentsRaw);
Collections.reverse(componentsClone);
this.components = componentsClone;
sortedComponents = componentsClone;
}
if (searchText != null && !searchText.trim().isEmpty()) {
this.components = this.components.stream().filter(component -> {
sortedComponents = sortedComponents.stream().filter(component -> {
return component.getName().toLowerCase().contains(searchText);
}).collect(Collectors.toList());
}
// Build list with groups and ungrouped components
this.components = buildComponentListWithGroups(sortedComponents);
int newSize = this.components.size();
fireContentsChanged(this, 0, Math.min(newSize, oldSize));
@@ -339,6 +494,67 @@ public class ExplorerPane extends JPanel {
else if (newSize > oldSize)
fireIntervalAdded(this, oldSize, newSize);
}
/**
* Builds the component list, using ComponentGroupItem for all items.
* For grouped components, creates one ComponentGroupItem per group.
* For ungrouped components, creates a ComponentGroupItem with a single component.
*/
private List<ComponentGroupItem> buildComponentListWithGroups(List<IDIYComponent<?>> sortedComponents) {
Set<ComponentGroup> groups = plugInPort.getComponentGroups();
// Build a map from component to its group (if any) and group name
Map<IDIYComponent<?>, Set<IDIYComponent<?>>> componentToGroup = new java.util.HashMap<>();
Map<Set<IDIYComponent<?>>, String> groupToName = new java.util.HashMap<>();
if (groups != null && !groups.isEmpty()) {
// Build a map from UUID to component for quick lookup
List<IDIYComponent<?>> allComponents = plugInPort.getCurrentProject().getComponents();
Map<UUID, IDIYComponent<?>> idToComponent = allComponents.stream()
.collect(Collectors.toMap(IDIYComponent::getId, comp -> comp));
for (ComponentGroup group : groups) {
// Convert ComponentGroup (with UUIDs) to Set<IDIYComponent<?>>
Set<IDIYComponent<?>> componentSet = group.getComponentIds().stream()
.map(idToComponent::get)
.filter(java.util.Objects::nonNull)
.collect(Collectors.toSet());
for (IDIYComponent<?> component : componentSet) {
componentToGroup.put(component, componentSet);
}
// Store the group name
if (group.getName() != null && !group.getName().isEmpty()) {
groupToName.put(componentSet, group.getName());
}
}
}
// Track which groups we've already added
Set<Set<IDIYComponent<?>>> addedGroups = new HashSet<>();
List<ComponentGroupItem> result = new ArrayList<>();
for (IDIYComponent<?> component : sortedComponents) {
Set<IDIYComponent<?>> group = componentToGroup.get(component);
if (group == null) {
// Component is not in any group, create a ComponentGroupItem with just this component
Set<IDIYComponent<?>> singleComponentGroup = new HashSet<>();
singleComponentGroup.add(component);
result.add(new ComponentGroupItem(singleComponentGroup, null));
} else {
// Component is in a group
if (!addedGroups.contains(group)) {
// This is the first component from this group we've seen, add the group item
String groupName = groupToName.get(group);
result.add(new ComponentGroupItem(group, groupName));
addedGroups.add(group);
}
// Otherwise, skip this component as we already have a group item for it
}
}
return result;
}
@Override
public int getSize() {
@@ -346,12 +562,12 @@ public class ExplorerPane extends JPanel {
}
@Override
public IDIYComponent<?> getElementAt(int index) {
public ComponentGroupItem getElementAt(int index) {
return this.components.get(index);
}
}
private static class ComponentListCellRenderer implements ListCellRenderer<IDIYComponent<?>> {
private static class ComponentListCellRenderer implements ListCellRenderer<ComponentGroupItem> {
// private static Color[] layerColors = new Color[] { Color.gray, Color.decode("#ff6363"),
// Color.decode("#293462"), Color.decode("#138588"),
@@ -367,32 +583,70 @@ public class ExplorerPane extends JPanel {
}
@Override
public Component getListCellRendererComponent(JList<? extends IDIYComponent<?>> list,
IDIYComponent<?> value, int index, boolean isSelected, boolean cellHasFocus) {
ComponentType componentType = componentTypes.get(value.getClass().getCanonicalName());
public Component getListCellRendererComponent(JList<? extends ComponentGroupItem> list,
ComponentGroupItem value, int index, boolean isSelected, boolean cellHasFocus) {
JLabel renderer = (JLabel) defaultRenderer.getListCellRendererComponent(list, value, index,
isSelected, cellHasFocus);
int layerId = (int) Math.round(componentType.getZOrder());
ComponentGroupItem groupItem = value;
if (groupItem.getComponentCount() == 1) {
// Render as individual component (ungrouped)
IDIYComponent<?> component = groupItem.getGroup().iterator().next();
ComponentType componentType = componentTypes.get(component.getClass().getCanonicalName());
if (componentType == null) {
renderer.setText(component.toString());
return renderer;
}
String valueForDisplay = value.getValueForDisplay();
if (isSelected) {
if (valueForDisplay == null || valueForDisplay.trim().isEmpty()) {
renderer.setText(String.format("[L%s] %s", layerId, value.getName()));
int layerId = (int) Math.round(componentType.getZOrder());
String valueForDisplay = component.getValueForDisplay();
if (isSelected) {
if (valueForDisplay == null || valueForDisplay.trim().isEmpty()) {
renderer.setText(String.format("[L%s] %s", layerId, component.getName()));
} else {
renderer
.setText(String.format("[L%s] %s (%s)", layerId, component.getName(), valueForDisplay));
}
} else {
renderer
.setText(String.format("[L%s] %s (%s)", layerId, value.getName(), valueForDisplay));
if (valueForDisplay == null || valueForDisplay.trim().isEmpty()) {
renderer.setText(String.format("<html><font color='#c0c0c0'>[L%s]</font> %s</html>",
layerId, component.getName()));
} else {
renderer.setText(String.format(
"<html><font color='#c0c0c0'>[L%s]</font> %s (<font color='#666666'>%s</font>)</html>",
layerId, component.getName(), valueForDisplay));
}
}
} else {
if (valueForDisplay == null || valueForDisplay.trim().isEmpty()) {
renderer.setText(String.format("<html><font color='#c0c0c0'>[L%s]</font> %s</html>",
layerId, value.getName()));
// Render as group
// Calculate maximum layer from components in the group
int maxLayerId = 0;
for (IDIYComponent<?> component : groupItem.getGroup()) {
ComponentType componentType = componentTypes.get(component.getClass().getCanonicalName());
if (componentType != null) {
int layerId = (int) Math.round(componentType.getZOrder());
if (layerId > maxLayerId) {
maxLayerId = layerId;
}
}
}
// Format: name (component count), similar to regular components showing name (value)
String groupName = groupItem.getGroupName() != null && !groupItem.getGroupName().isEmpty()
? groupItem.getGroupName()
: "Group";
String componentCount = String.valueOf(groupItem.getComponentCount());
if (isSelected) {
renderer.setText(String.format("<html>[L%s] <b>%s</b> (<b>%s</b>)</html>", maxLayerId, groupName, componentCount));
} else {
renderer.setText(String.format(
"<html><font color='#c0c0c0'>[L%s]</font> %s (<font color='#666666'>%s</font>)</html>",
layerId, value.getName(), valueForDisplay));
maxLayerId, groupName, componentCount));
}
}
@@ -414,4 +668,52 @@ public class ExplorerPane extends JPanel {
return LangUtil.translate(label);
}
}
/**
* Wrapper class to represent a group of components in the explorer list.
*/
public static class ComponentGroupItem {
private final Set<IDIYComponent<?>> group;
private final String groupName;
public ComponentGroupItem(Set<IDIYComponent<?>> group, String groupName) {
this.group = group;
this.groupName = groupName;
}
public Set<IDIYComponent<?>> getGroup() {
return group;
}
public int getComponentCount() {
return group.size();
}
public String getGroupName() {
return groupName;
}
@Override
public String toString() {
if (groupName != null && !groupName.isEmpty()) {
return groupName;
}
return "Group of " + group.size() + " components";
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null || getClass() != obj.getClass())
return false;
ComponentGroupItem other = (ComponentGroupItem) obj;
return group.equals(other.group);
}
@Override
public int hashCode() {
return group.hashCode();
}
}
}