Building blocks (#1072)

* composite block support

* naming

* Added ability to set node name (or names) for some connectivity components

* cleanup

* Include building blocks in the Recently Used category

* Prompt to confirm the overwrite when saving over the existing building blocks; When saving building blocks, present a drop down list with most recently used building blocks

* add alpha to composite

* Update CompositeComponentTest.java

* cleanup

* Update CompositeComponent.java

* cleanup

* Update CompositeComponent.java

* <org.diylc.appframework.update.Change> 					<changeType>IMPROVEMENT</changeType> 					<description>When saving building blocks, present a drop down list with most recently used building blocks for quick selection</description> 				</org.diylc.appframework.update.Change>

* Diode components now label their terminal nodes as 'A' (anode) and 'K' (cathode) instead of '1' and '2'

* Capacitor components now label their terminal nodes as '+' and '-' instead of '1' and '2' when 'Polarized' property is ticked
This commit is contained in:
Bane Stojkovic
2026-09-04 15:32:46 +02:00
committed by GitHub
parent 26d6ca48fd
commit 047196499a
40 changed files with 1939 additions and 95 deletions
@@ -1,9 +1,18 @@
package org.diylc.swing.actions.edit;
import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.swing.AbstractAction;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import org.diylc.appframework.miscutils.ConfigurationManager;
import org.diylc.common.IBlockProcessor;
import org.diylc.common.IPlugInPort;
import org.diylc.swing.ActionFactory;
import org.diylc.utils.IconLoader;
@@ -12,6 +21,9 @@ public class SaveAsBlockAction extends AbstractAction {
private static final long serialVersionUID = 1L;
/** How many recently used building blocks to offer in the name drop-down. */
private static final int MAX_RECENT_BLOCKS = 10;
private IPlugInPort plugInPort;
public SaveAsBlockAction(IPlugInPort plugInPort) {
@@ -24,10 +36,70 @@ public class SaveAsBlockAction extends AbstractAction {
@Override
public void actionPerformed(ActionEvent e) {
ActionFactory.LOG.info("Save as building block triggered");
String templateName = JOptionPane.showInputDialog(null, "Block name:",
"Save as Building Block", JOptionPane.PLAIN_MESSAGE);
if (templateName != null && !templateName.trim().isEmpty()) {
plugInPort.saveSelectionAsBlock(templateName);
JComboBox<String> nameBox =
new JComboBox<String>(getRecentBlockNames().toArray(new String[0]));
nameBox.setEditable(true);
// Start empty so an existing block is never overwritten by accident. The drop-down still
// offers the most recently used blocks (most recent first), so a user editing a block can
// pick its name back with a single click instead of retyping it.
nameBox.setSelectedItem("");
JPanel panel = new JPanel(new BorderLayout(5, 5));
panel.add(new JLabel("Block name:"), BorderLayout.NORTH);
panel.add(nameBox, BorderLayout.CENTER);
// Loop so that declining the overwrite prompt returns to the name dialog (with whatever was
// typed still in it) rather than cancelling the whole action.
while (true) {
int result = JOptionPane.showConfirmDialog(null, panel, "Save as Building Block",
JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE);
if (result != JOptionPane.OK_OPTION)
return;
// getEditor().getItem() captures in-progress typing as well as a picked item
Object selected = nameBox.getEditor().getItem();
String blockName = selected == null ? null : selected.toString().trim();
if (blockName == null || blockName.isEmpty())
return;
if (blockExists(blockName)) {
int overwrite = JOptionPane.showConfirmDialog(null,
"A building block named \"" + blockName + "\" already exists. Overwrite it?",
"Save as Building Block", JOptionPane.YES_NO_CANCEL_OPTION,
JOptionPane.WARNING_MESSAGE);
if (overwrite == JOptionPane.NO_OPTION)
continue; // back to the name dialog
if (overwrite != JOptionPane.YES_OPTION)
return; // Cancel or dialog closed
}
plugInPort.saveSelectionAsBlock(blockName);
return;
}
}
}
@SuppressWarnings("unchecked")
private boolean blockExists(String blockName) {
Map<String, ?> blocks = (Map<String, ?>) ConfigurationManager.getInstance()
.readObject(IBlockProcessor.BLOCKS_KEY, null);
return blocks != null && blocks.containsKey(blockName);
}
@SuppressWarnings("unchecked")
private List<String> getRecentBlockNames() {
List<String> recent = (List<String>) ConfigurationManager.getInstance()
.readObject(IPlugInPort.RECENT_COMPONENTS_KEY, null);
List<String> blockNames = new ArrayList<String>();
if (recent != null) {
for (String identifier : recent) {
if (identifier.startsWith(IBlockProcessor.BLOCK_PREFIX)) {
blockNames.add(identifier.substring(IBlockProcessor.BLOCK_PREFIX.length()));
if (blockNames.size() >= MAX_RECENT_BLOCKS)
break;
}
}
}
return blockNames;
}
}
@@ -45,8 +45,10 @@ import org.apache.log4j.Logger;
import org.diylc.appframework.miscutils.IConfigListener;
import org.diylc.appframework.miscutils.IConfigurationManager;
import org.diylc.common.BlockInstantiationMode;
import org.diylc.common.ComponentType;
import org.diylc.common.DrawOption;
import org.diylc.common.IBlockProcessor;
import org.diylc.common.IPlugInPort;
import org.diylc.common.IBlockProcessor.InvalidBlockException;
import org.diylc.swing.plugins.tree.TreePanel;
@@ -223,6 +225,17 @@ public class CanvasPanel extends JComponent implements Autoscroll {
(List<String>) configManager.readObject(IPlugInPort.RECENT_COMPONENTS_KEY, null);
if (recent != null && !recent.isEmpty()) {
String clazz = recent.get(0);
if (clazz.startsWith(IBlockProcessor.BLOCK_PREFIX)) {
String blockName = clazz.substring(IBlockProcessor.BLOCK_PREFIX.length());
try {
CanvasPanel.this.plugInPort.loadBlock(blockName, BlockInstantiationMode.COMPOSITE);
// hack: fake mouse movement to repaint
CanvasPanel.this.plugInPort.mouseMoved(getMousePosition(), false, false, false);
} catch (InvalidBlockException ex) {
LOG.error("Could not repeat last building block: " + blockName);
}
return;
}
Map<String, List<ComponentType>> componentTypes =
CanvasPanel.this.plugInPort.getComponentTypes();
for (Map.Entry<String, List<ComponentType>> entry : componentTypes.entrySet()) {
@@ -249,10 +262,10 @@ public class CanvasPanel extends JComponent implements Autoscroll {
String typeName = shortcutMap.get("F" + i);
if (typeName == null)
return;
if (typeName.startsWith("block:")) {
String blockName = typeName.substring(6);
if (typeName.startsWith(IBlockProcessor.BLOCK_PREFIX)) {
String blockName = typeName.substring(IBlockProcessor.BLOCK_PREFIX.length());
try {
plugInPort.loadBlock(blockName);
plugInPort.loadBlock(blockName, BlockInstantiationMode.COMPOSITE);
} catch (InvalidBlockException e) {
LOG.error("Could not find block assigned to shortcut: " + blockName);
}
@@ -53,6 +53,7 @@ import org.diylc.appframework.miscutils.Utils;
import org.diylc.swingframework.openide.DropDownButtonFactory;
import com.google.common.collect.Lists;
import org.diylc.common.BlockInstantiationMode;
import org.diylc.common.ComponentType;
import org.diylc.common.IPlugInPort;
import org.diylc.common.IBlockProcessor.InvalidBlockException;
@@ -316,7 +317,7 @@ public class ComponentButtonFactory {
@Override
public void actionPerformed(ActionEvent e) {
try {
plugInPort.loadBlock(blockName);
plugInPort.loadBlock(blockName, BlockInstantiationMode.COMPOSITE);
} catch (InvalidBlockException e1) {
LOG.error("Error loading building block", e1);
}
@@ -48,6 +48,7 @@ import org.diylc.appframework.miscutils.IConfigListener;
import org.diylc.common.ComponentType;
import org.diylc.common.Favorite;
import org.diylc.common.IBlockProcessor;
import org.diylc.common.IPlugInPort;
import org.diylc.common.Favorite.FavoriteType;
import org.diylc.core.IDIYComponent;
@@ -59,6 +60,7 @@ import org.diylc.swing.ISwingUI;
import org.diylc.utils.IconLoader;
import org.diylc.swing.plugins.tree.TreePanel;
import org.diylc.swing.plugins.tree.TreePanel.DeleteBlockAction;
import org.diylc.swing.plugins.tree.TreePanel.InsertBlockAsGroupAction;
import org.diylc.swing.plugins.tree.TreePanel.SelectAllAction;
/**
@@ -286,16 +288,22 @@ class ComponentTabbedPane extends JTabbedPane {
private void refreshRecentComponentsToolbar(List<String> recentComponentClassList) {
Container toolbar = getRecentToolbar();
toolbar.removeAll();
for (String componentClassName : recentComponentClassList) {
ComponentType componentType;
for (String identifier : recentComponentClassList) {
try {
componentType = ComponentProcessor.getInstance().extractComponentTypeFrom(
(Class<? extends IDIYComponent<?>>) Class.forName(componentClassName));
Component button = ComponentButtonFactory.create(plugInPort, componentType,
createVariantPopup(componentType, null));
toolbar.add(button);
if (identifier.startsWith(IBlockProcessor.BLOCK_PREFIX)) {
String blockName = identifier.substring(IBlockProcessor.BLOCK_PREFIX.length());
Component button = ComponentButtonFactory.createBuildingBlockButton(plugInPort, blockName,
createVariantPopup(null, blockName));
toolbar.add(button);
} else {
ComponentType componentType = ComponentProcessor.getInstance().extractComponentTypeFrom(
(Class<? extends IDIYComponent<?>>) Class.forName(identifier));
Component button = ComponentButtonFactory.create(plugInPort, componentType,
createVariantPopup(componentType, null));
toolbar.add(button);
}
} catch (Exception e) {
LOG.error("Could not create recent component button for " + componentClassName, e);
LOG.error("Could not create recent component button for " + identifier, e);
}
}
}
@@ -335,7 +343,8 @@ class ComponentTabbedPane extends JTabbedPane {
public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
variantPopup.removeAll();
final String identifier = componentType == null ? "block:" + blockName
final String identifier = componentType == null
? IBlockProcessor.BLOCK_PREFIX + blockName
: componentType.getInstanceClass().getCanonicalName();
JMenu shortcutSubmenu = new JMenu("Assign Shortcut");
@@ -456,6 +465,7 @@ class ComponentTabbedPane extends JTabbedPane {
}
}
} else {
variantPopup.add(new InsertBlockAsGroupAction(plugInPort, blockName));
variantPopup.add(shortcutSubmenu);
variantPopup.add(new DeleteBlockAction(plugInPort, swingUI, blockName));
}
@@ -37,8 +37,10 @@ import org.apache.log4j.Logger;
import org.diylc.appframework.miscutils.ConfigurationManager;
import org.diylc.appframework.miscutils.IConfigListener;
import org.diylc.common.BlockInstantiationMode;
import org.diylc.common.ComponentType;
import org.diylc.common.Favorite;
import org.diylc.common.IBlockProcessor;
import org.diylc.common.IPlugInPort;
import org.diylc.common.Favorite.FavoriteType;
import org.diylc.common.IBlockProcessor.InvalidBlockException;
@@ -142,7 +144,7 @@ public class CustomTreeModel implements TreeModel {
&& System.currentTimeMillis() - previousActionTime > 100) {
previousActionTime = System.currentTimeMillis();
try {
plugInPort.loadBlock(fav.getName());
plugInPort.loadBlock(fav.getName(), BlockInstantiationMode.COMPOSITE);
} catch (InvalidBlockException e1) {
e1.printStackTrace();
}
@@ -155,8 +157,37 @@ public class CustomTreeModel implements TreeModel {
}
if (RECENTLY_USED.equals(category)) {
for (String className : this.recentComponents) {
ComponentType type = this.typesByClass.get(className);
for (String identifier : this.recentComponents) {
if (identifier.startsWith(IBlockProcessor.BLOCK_PREFIX)) {
final String blockName = identifier.substring(IBlockProcessor.BLOCK_PREFIX.length());
// skip blocks that no longer exist
if (!this.blocks.contains(blockName))
continue;
if (this.searchText != null && !blockName.toLowerCase().contains(this.searchText))
continue;
visibleTypes.add(new TreeNode(blockName, new MouseAdapter() {
long previousActionTime = 0;
@Override
public void mouseClicked(MouseEvent e) {
if (e == null || SwingUtilities.isLeftMouseButton(e)
&& System.currentTimeMillis() - previousActionTime > 100) {
previousActionTime = System.currentTimeMillis();
try {
plugInPort.loadBlock(blockName, BlockInstantiationMode.COMPOSITE);
} catch (InvalidBlockException e1) {
e1.printStackTrace();
}
}
}
}, true));
continue;
}
final ComponentType type = this.typesByClass.get(identifier);
// skip unknown component classes
if (type == null)
continue;
if (this.searchText == null || type.getName().toLowerCase().contains(this.searchText)) {
visibleTypes.add(new TreeNode(type, new MouseAdapter() {
@@ -182,7 +213,7 @@ public class CustomTreeModel implements TreeModel {
&& System.currentTimeMillis() - previousActionTime > 100) {
previousActionTime = System.currentTimeMillis();
try {
plugInPort.loadBlock(block);
plugInPort.loadBlock(block, BlockInstantiationMode.COMPOSITE);
} catch (InvalidBlockException e1) {
e1.printStackTrace();
}
@@ -233,10 +264,16 @@ public class CustomTreeModel implements TreeModel {
}
if (RECENTLY_USED.equals(category)) {
for (String className : this.recentComponents) {
ComponentType type = typesByClass.get(className);
if (type.getName().toLowerCase().contains(searchText))
return true;
for (String identifier : this.recentComponents) {
if (identifier.startsWith(IBlockProcessor.BLOCK_PREFIX)) {
if (identifier.substring(IBlockProcessor.BLOCK_PREFIX.length()).toLowerCase()
.contains(searchText))
return true;
} else {
ComponentType type = typesByClass.get(identifier);
if (type != null && type.getName().toLowerCase().contains(searchText))
return true;
}
}
}
@@ -60,10 +60,13 @@ import org.apache.log4j.lf5.viewer.categoryexplorer.TreeModelAdapter;
import org.diylc.appframework.miscutils.ConfigurationManager;
import org.diylc.appframework.miscutils.IConfigListener;
import org.diylc.common.BlockInstantiationMode;
import org.diylc.common.ComponentType;
import org.diylc.common.Favorite;
import org.diylc.common.IBlockProcessor;
import org.diylc.common.IPlugInPort;
import org.diylc.common.Favorite.FavoriteType;
import org.diylc.common.IBlockProcessor.InvalidBlockException;
import org.diylc.core.IDIYComponent;
import org.diylc.core.IView;
import org.diylc.core.Template;
@@ -313,7 +316,8 @@ public class TreePanel extends JPanel {
TreeNode payload = (TreeNode) selectedNode;
final ComponentType componentType = payload.getComponentType();
final String identifier = componentType == null ? "block:" + payload.toString()
final String identifier = componentType == null
? IBlockProcessor.BLOCK_PREFIX + payload.toString()
: componentType.getInstanceClass().getCanonicalName();
JMenu shortcutSubmenu = new JMenu("Assign Shortcut");
@@ -434,6 +438,7 @@ public class TreePanel extends JPanel {
}
}
} else if (getTreeModel().isLeaf(selectedNode)) {
popup.add(new InsertBlockAsGroupAction(plugInPort, payload.toString()));
popup.add(shortcutSubmenu);
popup.add(new DeleteBlockAction(plugInPort, swingUI, payload.toString()));
}
@@ -494,7 +499,8 @@ public class TreePanel extends JPanel {
HashMap<String, String> shortcutMap = (HashMap<String, String>) ConfigurationManager
.getInstance().readObject(TreePanel.COMPONENT_SHORTCUT_KEY, null);
String identifier = payload.getComponentType() == null ? "block:" + payload.toString()
String identifier = payload.getComponentType() == null
? IBlockProcessor.BLOCK_PREFIX + payload.toString()
: payload.getComponentType().getInstanceClass().getCanonicalName();
if (shortcutMap != null && shortcutMap.containsValue(identifier)) {
for (String key : shortcutMap.keySet()) {
@@ -565,6 +571,37 @@ public class TreePanel extends JPanel {
}
}
/**
* Left click on a building block node inserts it in composite mode (a single rigid component)
* by default - see design decision D10 in {@code docs/plans/composite-building-blocks.md}.
* This action offers the one extra click needed to fall back to the original behavior:
* every component of the block instantiated separately and auto-grouped.
*/
public static class InsertBlockAsGroupAction extends AbstractAction {
private static final long serialVersionUID = 1L;
private IPlugInPort plugInPort;
private String blockName;
public InsertBlockAsGroupAction(IPlugInPort plugInPort, String blockName) {
super();
this.plugInPort = plugInPort;
this.blockName = blockName;
putValue(AbstractAction.NAME, "Insert as Grouped Components");
}
@Override
public void actionPerformed(ActionEvent e) {
LOG.info(getValue(AbstractAction.NAME) + " triggered");
try {
plugInPort.loadBlock(blockName, BlockInstantiationMode.GROUP);
} catch (InvalidBlockException e1) {
LOG.error("Could not find building block: " + blockName, e1);
}
}
}
public static class DeleteBlockAction extends AbstractAction {
private static final long serialVersionUID = 1L;