diff --git a/diylc/diylc-core/src/main/java/org/diylc/common/EventType.java b/diylc/diylc-core/src/main/java/org/diylc/common/EventType.java
index 2fbd2288..0c8b8727 100644
--- a/diylc/diylc-core/src/main/java/org/diylc/common/EventType.java
+++ b/diylc/diylc-core/src/main/java/org/diylc/common/EventType.java
@@ -117,5 +117,11 @@ public enum EventType {
* Called when the mouse hovers over sticky control points (nodes) in tooltip mode.
* Parameters: String text (semicolon-separated component and node names, or null), Point point.
*/
- NODE_NAME_HOVER_TOOLTIP;
+ NODE_NAME_HOVER_TOOLTIP,
+ /**
+ * Called when the set of enabled component categories changes. The first parameter is a
+ * Set<String> of disabled category names. All categories not included are
+ * enabled.
+ */
+ COMPONENT_CATEGORIES_CHANGED;
}
diff --git a/diylc/diylc-core/src/main/java/org/diylc/common/IPlugInPort.java b/diylc/diylc-core/src/main/java/org/diylc/common/IPlugInPort.java
index 03cf41c6..3118b19f 100644
--- a/diylc/diylc-core/src/main/java/org/diylc/common/IPlugInPort.java
+++ b/diylc/diylc-core/src/main/java/org/diylc/common/IPlugInPort.java
@@ -83,6 +83,7 @@ public interface IPlugInPort extends ISelectionProcessor, IMouseProcessor, IKeyP
public static final String HARDWARE_ACCELERATION = "hardwareAcceleration";
public static final String EXTRA_SPACE_KEY = "extraSpace";
public static final String FAVORITES_KEY = "favorites";
+ public static final String DISABLED_CATEGORIES_KEY = "disabledComponentCategories";
public static final String RENUMBER_ON_PASTE_KEY = "renumberOnPaste";
public static final String RULER_IN_SUBDIVISION_KEY = "rulerInSubdivision";
public static final String CACHING_ENABLED_KEY = "cachingEnabled";
@@ -213,6 +214,20 @@ public interface IPlugInPort extends ISelectionProcessor, IMouseProcessor, IKeyP
*/
Map> getComponentTypes();
+ /**
+ * Returns the {@link ComponentType}s that should be offered to the user, i.e. the result of
+ * {@link #getComponentTypes()} without the categories the user disabled. Intended for component
+ * browsers; anything that needs to resolve an arbitrary component class must use
+ * {@link #getComponentTypes()} instead.
+ */
+ Map> getVisibleComponentTypes();
+
+ boolean isCategoryEnabled(String category);
+
+ void setCategoryEnabled(String category, boolean enabled);
+
+ void resetCategoryVisibility();
+
/**
* Draws project on the provided {@link Graphics2D}. If the provided filter is not null, it will
* be used to filter the components that are shown.
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 a1f36433..5968cbe7 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
@@ -202,6 +202,14 @@ public class Presenter implements IPlugInPort {
messageDispatcher.dispatchMessage(EventType.NODE_NAME_HOVER_TOOLTIP, null, null);
}
});
+
+ configManager.addConfigListener(IPlugInPort.DISABLED_CATEGORIES_KEY, new IConfigListener() {
+ @Override
+ public void valueChanged(String key, Object value) {
+ messageDispatcher.dispatchMessage(EventType.COMPONENT_CATEGORIES_CHANGED,
+ getDisabledCategories());
+ }
+ });
}
public void installPlugin(Supplier plugInSupplier) {
@@ -408,12 +416,58 @@ public class Presenter implements IPlugInPort {
return projectFileManager.isModified();
}
+ @SuppressWarnings("unchecked")
+ private Set getDisabledCategories() {
+ List list = (List) configManager.readObject(
+ IPlugInPort.DISABLED_CATEGORIES_KEY, new ArrayList());
+ if (list == null) {
+ return new HashSet();
+ }
+ return new HashSet(list);
+ }
+
@SuppressWarnings("unchecked")
@Override
public Map> getComponentTypes() {
return ComponentProcessor.getInstance().getComponentTypes();
}
+ @Override
+ public Map> getVisibleComponentTypes() {
+ Set disabled = getDisabledCategories();
+ Map> all = getComponentTypes();
+ Map> visible = new HashMap>();
+ for (Map.Entry> entry : all.entrySet()) {
+ if (!disabled.contains(entry.getKey())) {
+ visible.put(entry.getKey(), entry.getValue());
+ }
+ }
+ return visible;
+ }
+
+ @Override
+ public boolean isCategoryEnabled(String category) {
+ return !getDisabledCategories().contains(category);
+ }
+
+ @Override
+ public void setCategoryEnabled(String category, boolean enabled) {
+ Set disabled = getDisabledCategories();
+ if (enabled) {
+ disabled.remove(category);
+ } else {
+ disabled.add(category);
+ }
+ List list = new ArrayList(disabled);
+ Collections.sort(list);
+ configManager.writeValue(IPlugInPort.DISABLED_CATEGORIES_KEY, list);
+ }
+
+ @Override
+ public void resetCategoryVisibility() {
+ configManager.writeValue(IPlugInPort.DISABLED_CATEGORIES_KEY, new ArrayList());
+ }
+
@SuppressWarnings({"unchecked"})
private boolean isComponentVisible(IDIYComponent> component) {
ComponentType componentType =
diff --git a/diylc/diylc-core/src/main/resources/update.xml b/diylc/diylc-core/src/main/resources/update.xml
index 139d6d97..0ee9ca77 100644
--- a/diylc/diylc-core/src/main/resources/update.xml
+++ b/diylc/diylc-core/src/main/resources/update.xml
@@ -557,6 +557,10 @@
NEW_FEATURE
TFT Display - 2.8 inch and 2.4 inch ILI9341, 1.8 inch and 0.96 inch ST7735, 1.54 inch ST7789, 1.28 inch round GC9A01
+
+ NEW_FEATURE
+ Added ability to hide component categories in the component tree and toolbar from the Config menu
+
IMPROVEMENT
Bill of Materials now lists the selected variant of microcontroller boards and displays, so different versions of the same board are no longer merged into a single row
diff --git a/diylc/diylc-swing/src/main/java/org/diylc/swing/ActionFactory.java b/diylc/diylc-swing/src/main/java/org/diylc/swing/ActionFactory.java
index 5ac45f23..65813ab5 100644
--- a/diylc/diylc-swing/src/main/java/org/diylc/swing/ActionFactory.java
+++ b/diylc/diylc-swing/src/main/java/org/diylc/swing/ActionFactory.java
@@ -30,12 +30,14 @@ import org.diylc.core.ExpansionMode;
import org.diylc.core.Theme;
import org.diylc.swing.actions.CheckProximityAction;
import org.diylc.swing.actions.ComponentBrowserAction;
+import org.diylc.swing.actions.ComponentCategoryAction;
import org.diylc.swing.actions.ConfigAction;
import org.diylc.swing.actions.ExportGerberAction;
import org.diylc.swing.actions.ExportPNGAction;
import org.diylc.swing.actions.FindAction;
import org.diylc.swing.actions.FlexibleLeadsAction;
import org.diylc.swing.actions.RenumberAction;
+import org.diylc.swing.actions.ResetCategoriesAction;
import org.diylc.swing.actions.ThemeAction;
import org.diylc.swing.actions.ToggleAction;
import org.diylc.swing.actions.edit.BringToFrontAction;
@@ -264,6 +266,15 @@ public class ActionFactory {
return new ComponentBrowserAction(browserType);
}
+ public ComponentCategoryAction createComponentCategoryAction(IPlugInPort plugInPort,
+ String category) {
+ return new ComponentCategoryAction(plugInPort, category);
+ }
+
+ public ResetCategoriesAction createResetCategoriesAction(IPlugInPort plugInPort) {
+ return new ResetCategoriesAction(plugInPort);
+ }
+
public RenumberAction createRenumberAction(IPlugInPort plugInPort, boolean xAxisFirst) {
return new RenumberAction(plugInPort, xAxisFirst);
}
diff --git a/diylc/diylc-swing/src/main/java/org/diylc/swing/actions/ComponentCategoryAction.java b/diylc/diylc-swing/src/main/java/org/diylc/swing/actions/ComponentCategoryAction.java
new file mode 100644
index 00000000..3dbf9ef8
--- /dev/null
+++ b/diylc/diylc-swing/src/main/java/org/diylc/swing/actions/ComponentCategoryAction.java
@@ -0,0 +1,63 @@
+/*
+
+ 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 .
+
+*/
+package org.diylc.swing.actions;
+
+import java.awt.event.ActionEvent;
+import javax.swing.AbstractAction;
+
+import org.diylc.appframework.miscutils.ConfigurationManager;
+import org.diylc.appframework.miscutils.IConfigListener;
+
+import org.diylc.common.IPlugInPort;
+import org.diylc.core.IView;
+
+public class ComponentCategoryAction extends AbstractAction {
+
+ private static final long serialVersionUID = 1L;
+
+ private final IPlugInPort plugInPort;
+ private final String category;
+
+ public ComponentCategoryAction(IPlugInPort plugInPort, String category) {
+ super();
+ this.plugInPort = plugInPort;
+ this.category = category;
+ putValue(AbstractAction.NAME, category);
+ putValue(IView.CHECK_BOX_MENU_ITEM, true);
+ putValue(AbstractAction.SELECTED_KEY, plugInPort.isCategoryEnabled(category));
+
+ ConfigurationManager.getInstance().addConfigListener(
+ IPlugInPort.DISABLED_CATEGORIES_KEY, new IConfigListener() {
+
+ @Override
+ public void valueChanged(String key, Object value) {
+ putValue(AbstractAction.SELECTED_KEY, plugInPort.isCategoryEnabled(category));
+ }
+ });
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ boolean selected = Boolean.TRUE.equals(getValue(AbstractAction.SELECTED_KEY));
+ plugInPort.setCategoryEnabled(category, selected);
+ }
+}
diff --git a/diylc/diylc-swing/src/main/java/org/diylc/swing/actions/ResetCategoriesAction.java b/diylc/diylc-swing/src/main/java/org/diylc/swing/actions/ResetCategoriesAction.java
new file mode 100644
index 00000000..d1e176b3
--- /dev/null
+++ b/diylc/diylc-swing/src/main/java/org/diylc/swing/actions/ResetCategoriesAction.java
@@ -0,0 +1,45 @@
+/*
+
+ 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 .
+
+*/
+package org.diylc.swing.actions;
+
+import java.awt.event.ActionEvent;
+import javax.swing.AbstractAction;
+
+import org.diylc.common.IPlugInPort;
+
+public class ResetCategoriesAction extends AbstractAction {
+
+ private static final long serialVersionUID = 1L;
+
+ private final IPlugInPort plugInPort;
+
+ public ResetCategoriesAction(IPlugInPort plugInPort) {
+ super();
+ this.plugInPort = plugInPort;
+ putValue(AbstractAction.NAME, "Reset All");
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ plugInPort.resetCategoryVisibility();
+ }
+}
diff --git a/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/config/ConfigPlugin.java b/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/config/ConfigPlugin.java
index fa51c130..88becfee 100644
--- a/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/config/ConfigPlugin.java
+++ b/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/config/ConfigPlugin.java
@@ -58,6 +58,7 @@ public class ConfigPlugin implements IPlugIn {
private static final String THEME_MENU = "Theme";
private static final String RULER_MENU = "Ruler Inch Subdivision...";
private static final String SNAP_MENU = "Snap To";
+ private static final String CATEGORIES_MENU = "Component Categories";
private static final String COMPONENT_BROWSER_MENU = "Toolbox";
public static final String COMPONENT_BROWSER = "componentBrowser";
public static final String PROJECT_EXPLORER = "projectExplorer";
@@ -89,7 +90,15 @@ public class ConfigPlugin implements IPlugIn {
IPlugInPort.AUTO_UNIT_CONVERSION_KEY, true), CONFIG_MENU);
swingUI.injectMenuAction(
ActionFactory.getInstance().createConfigAction(plugInPort, "Auto-Edit Mode", IPlugInPort.AUTO_EDIT_KEY, true),
- CONFIG_MENU);
+ CONFIG_MENU);
+ swingUI.injectSubmenu(CATEGORIES_MENU, IconLoader.FolderPreferences.getIcon(), CONFIG_MENU);
+ swingUI.injectMenuAction(
+ ActionFactory.getInstance().createResetCategoriesAction(plugInPort), CATEGORIES_MENU);
+ swingUI.injectMenuAction(null, CATEGORIES_MENU);
+ plugInPort.getComponentTypes().keySet().stream().sorted()
+ .forEach(category -> swingUI.injectMenuAction(
+ ActionFactory.getInstance().createComponentCategoryAction(plugInPort, category),
+ CATEGORIES_MENU));
swingUI.injectMenuAction(
ActionFactory.getInstance().createConfigAction(plugInPort, "Continuous Creation",
IPlugInPort.CONTINUOUS_CREATION_KEY, false), CONFIG_MENU);
diff --git a/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/toolbox/ComponentTabbedPane.java b/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/toolbox/ComponentTabbedPane.java
index 4de76b9c..bccc4934 100644
--- a/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/toolbox/ComponentTabbedPane.java
+++ b/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/toolbox/ComponentTabbedPane.java
@@ -89,6 +89,9 @@ class ComponentTabbedPane extends JTabbedPane {
private List pendingRecentComponents = null;
private List blocks;
+ private final Map categoryPanels = new HashMap();
+ private boolean refreshing = false;
+
public ComponentTabbedPane(IPlugInPort plugInPort, ISwingUI swingUI) {
super();
this.plugInPort = plugInPort;
@@ -100,23 +103,22 @@ class ComponentTabbedPane extends JTabbedPane {
addTab(LangUtil.translate("(Favorites)"), createFavoritesPanel());
addTab(LangUtil.translate("(Recently Used)"), createRecentComponentsPanel());
addTab(LangUtil.translate("(Building Blocks)"), createBuildingBlocksPanel());
- Map> componentTypes = plugInPort.getComponentTypes();
- List categories = new ArrayList(componentTypes.keySet());
- Collections.sort(categories);
- for (String category : categories) {
- JPanel panel = createTab((componentTypes.get(category)));
- addTab(category, panel);
- }
+
+ refreshCategoryTabs();
// restore last selected tab
int lastSelectedTab = ConfigurationManager.getInstance().readInt(LAST_SELECTED_TAB, -1);
- if (lastSelectedTab >= 0)
+ if (lastSelectedTab >= 0 && lastSelectedTab < getTabCount()) {
setSelectedIndex(lastSelectedTab);
+ }
addChangeListener(new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
+ if (refreshing) {
+ return;
+ }
ConfigurationManager.getInstance().writeValue(LAST_SELECTED_TAB, getSelectedIndex());
ComponentTabbedPane.this.plugInPort.setNewComponentTypeSlot(null, null, null, false);
// Refresh recent components if needed
@@ -131,6 +133,50 @@ class ComponentTabbedPane extends JTabbedPane {
setTabLayoutPolicy(JTabbedPane.SCROLL_TAB_LAYOUT);
}
+ public void refreshCategoryTabs() {
+ refreshing = true;
+ int restoredIndex = -1;
+ try {
+ int previousIndex = getSelectedIndex();
+ String selectedTitle =
+ previousIndex >= 0 && previousIndex < getTabCount() ? getTitleAt(previousIndex) : null;
+
+ while (getTabCount() > 3) {
+ removeTabAt(3);
+ }
+
+ Map> visibleComponentTypes =
+ plugInPort.getVisibleComponentTypes();
+ List categories = new ArrayList(visibleComponentTypes.keySet());
+ Collections.sort(categories);
+ for (String category : categories) {
+ Component panel = categoryPanels.get(category);
+ if (panel == null) {
+ panel = createTab(visibleComponentTypes.get(category));
+ categoryPanels.put(category, panel);
+ }
+ addTab(category, panel);
+ }
+
+ if (selectedTitle != null) {
+ for (int i = 0; i < getTabCount(); i++) {
+ if (selectedTitle.equals(getTitleAt(i))) {
+ restoredIndex = i;
+ break;
+ }
+ }
+ }
+ } finally {
+ refreshing = false;
+ }
+
+ if (restoredIndex >= 0) {
+ setSelectedIndex(restoredIndex);
+ } else if (getTabCount() > 0) {
+ setSelectedIndex(0);
+ }
+ }
+
@SuppressWarnings("unchecked")
private void initialize() {
Map> componentTypes = plugInPort.getComponentTypes();
diff --git a/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/toolbox/ToolBox.java b/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/toolbox/ToolBox.java
index a8024803..a099d917 100644
--- a/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/toolbox/ToolBox.java
+++ b/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/toolbox/ToolBox.java
@@ -79,13 +79,13 @@ public class ToolBox implements IPlugIn {
@Override
public EnumSet getSubscribedEventTypes() {
- // TODO Auto-generated method stub
- return null;
+ return EnumSet.of(EventType.COMPONENT_CATEGORIES_CHANGED);
}
@Override
public void processMessage(EventType eventType, Object... params) {
- // TODO Auto-generated method stub
-
+ if (eventType == EventType.COMPONENT_CATEGORIES_CHANGED) {
+ getComponentTabbedPane().refreshCategoryTabs();
+ }
}
}
diff --git a/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/tree/ComponentTree.java b/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/tree/ComponentTree.java
index db14d14d..4b79e43f 100644
--- a/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/tree/ComponentTree.java
+++ b/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/tree/ComponentTree.java
@@ -103,13 +103,13 @@ public class ComponentTree implements IPlugIn {
@Override
public EnumSet getSubscribedEventTypes() {
- // TODO Auto-generated method stub
- return null;
+ return EnumSet.of(EventType.COMPONENT_CATEGORIES_CHANGED);
}
@Override
public void processMessage(EventType eventType, Object... params) {
- // TODO Auto-generated method stub
-
+ if (eventType == EventType.COMPONENT_CATEGORIES_CHANGED) {
+ getTreePanel().getTreeModel().refresh();
+ }
}
}
diff --git a/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/tree/CustomTreeModel.java b/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/tree/CustomTreeModel.java
index 44ae7323..081462c1 100644
--- a/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/tree/CustomTreeModel.java
+++ b/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/tree/CustomTreeModel.java
@@ -76,20 +76,26 @@ public class CustomTreeModel implements TreeModel {
public CustomTreeModel(IPlugInPort plugInPort) {
super();
this.plugInPort = plugInPort;
- this.componentTypes = plugInPort.getComponentTypes();
+ this.visibleLeaves = new HashMap>();
+
+ this.typesByClass = new HashMap();
+ for (Map.Entry> e : plugInPort.getComponentTypes().entrySet()) {
+ for (ComponentType c : e.getValue()) {
+ typesByClass.put(c.getInstanceClass().getCanonicalName(), c);
+ }
+ }
+
+ this.initialize();
+ refresh();
+ }
+
+ public void refresh() {
+ this.componentTypes = plugInPort.getVisibleComponentTypes();
this.categories = new ArrayList(this.componentTypes.keySet());
Collections.sort(this.categories);
this.categories.add(0, BUILDING_BLOCKS);
this.categories.add(0, RECENTLY_USED);
this.categories.add(0, FAVORITES);
- this.visibleLeaves = new HashMap>();
-
- this.typesByClass = new HashMap();
- for (Map.Entry> e : componentTypes.entrySet())
- for (ComponentType c : e.getValue())
- typesByClass.put(c.getInstanceClass().getCanonicalName(), c);
-
- this.initialize();
updateVisibleCategories();
}
diff --git a/diylc/diylc-swing/src/main/java/org/diylc/utils/IconLoader.java b/diylc/diylc-swing/src/main/java/org/diylc/utils/IconLoader.java
index 38c263c6..2a67f525 100644
--- a/diylc/diylc-swing/src/main/java/org/diylc/utils/IconLoader.java
+++ b/diylc/diylc-swing/src/main/java/org/diylc/utils/IconLoader.java
@@ -89,7 +89,7 @@ public enum IconLoader {
"trace_mask.png"), TraceProximity("trace_proximity.png"), Undo("undo.png"), Ungroup(
"ungroup.png"), Unlock("lock_open.png"), Upload("upload.png"), User("user1.png"), Warning(
"warning.png"), Web("web.png"), WindowColors("window_colors.png"), WindowGear(
- "window_gear.png"), Wrench("wrench.png"), ZoomSmall("zoom_small.png");
+ "window_gear.png"), Wrench("wrench.png"), ZoomSmall("zoom_small.png"), FolderPreferences("folder_preferences.png");
protected String name;
diff --git a/diylc/diylc-swing/src/main/resources/diylc-swing-images/folder_preferences.png b/diylc/diylc-swing/src/main/resources/diylc-swing-images/folder_preferences.png
new file mode 100644
index 00000000..2f7fad44
Binary files /dev/null and b/diylc/diylc-swing/src/main/resources/diylc-swing-images/folder_preferences.png differ
diff --git a/diylc/diylc-swing/src/test/java/org/diylc/PresenterTests.java b/diylc/diylc-swing/src/test/java/org/diylc/PresenterTests.java
index 53debf6b..989fb3b1 100644
--- a/diylc/diylc-swing/src/test/java/org/diylc/PresenterTests.java
+++ b/diylc/diylc-swing/src/test/java/org/diylc/PresenterTests.java
@@ -2,12 +2,27 @@ package org.diylc;
import java.awt.Point;
import java.io.IOException;
+import java.util.ArrayList;
+import java.util.EnumSet;
import java.util.List;
+import java.util.Map;
+import java.util.Set;
-import org.junit.Test;
import junit.framework.Assert;
+import org.junit.Test;
+
+import org.diylc.appframework.miscutils.ConfigurationManager;
+import org.diylc.appframework.miscutils.InMemoryConfigurationManager;
import org.diylc.common.ComponentType;
+import org.diylc.common.EventType;
+import org.diylc.common.IPlugIn;
+import org.diylc.common.IPlugInPort;
import org.diylc.core.Template;
+import org.diylc.presenter.Presenter;
+import org.diylc.swing.ActionFactory;
+import org.diylc.swing.actions.ComponentCategoryAction;
+import org.diylc.swing.actions.ResetCategoriesAction;
+import org.diylc.swing.plugins.tree.CustomTreeModel;
public class PresenterTests extends TestBase {
@@ -98,6 +113,114 @@ public class PresenterTests extends TestBase {
presenter.mouseMoved(new Point(1000, 1000), false, false, false);
Assert.assertNull(receivedTooltip[0]);
}
+
+ @Test
+ public void testCategoryVisibilityDefaults() {
+ presenter.resetCategoryVisibility();
+ Map> all = presenter.getComponentTypes();
+ Map> visible = presenter.getVisibleComponentTypes();
+ Assert.assertEquals(all.keySet(), visible.keySet());
+ for (String category : all.keySet()) {
+ Assert.assertTrue(presenter.isCategoryEnabled(category));
+ }
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void testDisableAndEnableCategory() {
+ presenter.resetCategoryVisibility();
+
+ final Set[] lastEventDisabled = new Set[1];
+ IPlugIn testPlugin = new IPlugIn() {
+ @Override
+ public void connect(IPlugInPort plugInPort) {}
+
+ @Override
+ public EnumSet getSubscribedEventTypes() {
+ return EnumSet.of(EventType.COMPONENT_CATEGORIES_CHANGED);
+ }
+
+ @Override
+ public void processMessage(EventType eventType, Object... params) {
+ if (eventType == EventType.COMPONENT_CATEGORIES_CHANGED && params.length > 0) {
+ lastEventDisabled[0] = (Set) params[0];
+ }
+ }
+ };
+ presenter.installPlugin(() -> testPlugin);
+
+ // Disable "Tubes"
+ presenter.setCategoryEnabled("Tubes", false);
+ Assert.assertFalse(presenter.isCategoryEnabled("Tubes"));
+ Assert.assertFalse(presenter.getVisibleComponentTypes().containsKey("Tubes"));
+ Assert.assertTrue(presenter.getComponentTypes().containsKey("Tubes"));
+ Assert.assertNotNull(lastEventDisabled[0]);
+ Assert.assertTrue(lastEventDisabled[0].contains("Tubes"));
+
+ // Also disable "Robotics"
+ presenter.setCategoryEnabled("Robotics", false);
+ Assert.assertFalse(presenter.isCategoryEnabled("Robotics"));
+ Assert.assertFalse(presenter.getVisibleComponentTypes().containsKey("Robotics"));
+ Assert.assertTrue(lastEventDisabled[0].contains("Robotics"));
+ Assert.assertTrue(lastEventDisabled[0].contains("Tubes"));
+
+ // Check config stores sorted list
+ List stored = (List) InMemoryConfigurationManager.getInstance()
+ .readObject(IPlugInPort.DISABLED_CATEGORIES_KEY, new ArrayList());
+ Assert.assertEquals(2, stored.size());
+ Assert.assertEquals("Robotics", stored.get(0));
+ Assert.assertEquals("Tubes", stored.get(1));
+
+ // Re-enable "Tubes"
+ presenter.setCategoryEnabled("Tubes", true);
+ Assert.assertTrue(presenter.isCategoryEnabled("Tubes"));
+ Assert.assertTrue(presenter.getVisibleComponentTypes().containsKey("Tubes"));
+ Assert.assertFalse(lastEventDisabled[0].contains("Tubes"));
+ Assert.assertTrue(lastEventDisabled[0].contains("Robotics"));
+
+ // Reset all
+ presenter.resetCategoryVisibility();
+ Assert.assertTrue(presenter.isCategoryEnabled("Robotics"));
+ Assert.assertTrue(presenter.isCategoryEnabled("Tubes"));
+ Assert.assertTrue(lastEventDisabled[0].isEmpty());
+ }
+
+ @Test
+ public void testComponentCategoryActions() {
+ Presenter prodPresenter = new Presenter(view, ConfigurationManager.getInstance());
+ prodPresenter.resetCategoryVisibility();
+
+ ComponentCategoryAction catAction =
+ ActionFactory.getInstance().createComponentCategoryAction(prodPresenter, "Tubes");
+ Assert.assertEquals(true, catAction.getValue(javax.swing.AbstractAction.SELECTED_KEY));
+
+ // Toggle off
+ catAction.putValue(javax.swing.AbstractAction.SELECTED_KEY, false);
+ catAction.actionPerformed(null);
+ Assert.assertFalse(prodPresenter.isCategoryEnabled("Tubes"));
+
+ // Reset via ResetCategoriesAction
+ ResetCategoriesAction resetAction =
+ ActionFactory.getInstance().createResetCategoriesAction(prodPresenter);
+ resetAction.actionPerformed(null);
+ Assert.assertTrue(prodPresenter.isCategoryEnabled("Tubes"));
+ Assert.assertEquals(true, catAction.getValue(javax.swing.AbstractAction.SELECTED_KEY));
+ }
+
+ @Test
+ public void testCustomTreeModelRefresh() {
+ presenter.resetCategoryVisibility();
+ CustomTreeModel treeModel = new CustomTreeModel(presenter);
+ Assert.assertTrue(presenter.isCategoryEnabled("Tubes"));
+
+ presenter.setCategoryEnabled("Tubes", false);
+ treeModel.refresh();
+ Assert.assertFalse(presenter.getVisibleComponentTypes().containsKey("Tubes"));
+
+ presenter.resetCategoryVisibility();
+ treeModel.refresh();
+ Assert.assertTrue(presenter.getVisibleComponentTypes().containsKey("Tubes"));
+ }
// @Test
// public void testLoadVariants() {
diff --git a/diylc/docs/plans/category-visibility.md b/diylc/docs/plans/category-visibility.md
new file mode 100644
index 00000000..2fbf91e5
--- /dev/null
+++ b/diylc/docs/plans/category-visibility.md
@@ -0,0 +1,288 @@
+# Component Category Visibility
+
+A `Config` submenu that lets the user hide whole component categories from the
+component tree and the tabbed component bar, with the filtering applied in the
+`Presenter` and announced to the views through a new event.
+
+## 1. Goal
+
+The library ships sixteen categories (`Boards`, `Connectivity`, `Controllers`,
+`Displays & Outputs`, `Electro-Mechanical`, `Guitar`, `Misc`,
+`Modules & Breakouts`, `Passive`, `Robotics`, `SMD`, `Schematic Symbols`,
+`Semiconductors`, `Sensors`, `Shapes`, `Tubes`). A user who only ever draws
+guitar wiring has to scroll past tabs and tree nodes they will never open. The
+feature is a per-user toggle list: every category visible by default, any subset
+hidden, the change visible immediately in whichever browser is active.
+
+New in `ConfigPlugin`:
+
+```
+Config
+ ...
+ Auto-Edit Mode
+ Component Categories >
+ Reset All
+ ---------------
+ [x] Boards
+ [x] Connectivity
+ ...
+ [x] Tubes
+ Continuous Creation
+ ...
+```
+
+Disabling a category changes **only what the two browsers list**. Nothing is
+hidden on the canvas, no existing project is altered, and no `.diy` file is
+affected, so the serialized format and the regression references are untouched.
+
+## 2. Four decisions taken up front
+
+**The filtered view is a second accessor, not a change to `getComponentTypes()`.**
+That method has six callers and only two of them are browsers:
+
+| Caller | Uses the map for | Needs every category |
+| --- | --- | --- |
+| `CustomTreeModel` | tree nodes | no - filter here |
+| `ComponentTabbedPane` | category tabs | no - filter here |
+| `CanvasPanel` | type cache, keyboard-shortcut lookup | yes |
+| `ExplorerPane` | list cell renderer | yes |
+| `NetlistImportDialog` | netlist type mapping | yes |
+| `ExportVariantsAction` | variant export tree | yes |
+
+Had the existing method started filtering, a hidden category would silently
+break the explorer's rendering of components already on the canvas and the
+netlist importer's type lookup. So `getComponentTypes()` keeps its contract and
+a new `getVisibleComponentTypes()` is added for the browsers.
+
+**Favorites, Recently Used and keyboard shortcuts stay reachable.** Only the
+per-category listings are filtered. A part the user explicitly pinned does not
+vanish because its category was collapsed, and `CustomTreeModel`'s favorites
+branch - which dereferences `typesByClass.get(fav.getName())` without a null
+check - keeps working because that map is still built from the complete type
+list.
+
+**One config key holds the disabled categories**, as a `List` written
+through `writeValue` / read through `readObject`, the same shape as
+`FAVORITES_KEY` and `RECENT_COMPONENTS_KEY`:
+
+```
+disabledComponentCategories = ["Robotics", "Tubes"]
+```
+
+Storing the *disabled* set rather than the enabled one means an empty value is
+the default state, and a category introduced in a future release shows up
+enabled for every existing user without a migration. "Reset All" is then simply
+writing an empty list. The alternative - one boolean key per category - would
+have let `ConfigAction` be reused verbatim, at the cost of config keys derived
+from display names and one listener per category inside the `Presenter`.
+
+**The submenu sits directly under `Config`**, a sibling of `Snap To`, `Theme`
+and `Toolbox`, rather than nested inside `Toolbox`.
+
+## 3. Core: the event
+
+`diylc-core/src/main/java/org/diylc/common/EventType.java` gains a constant at
+the end of the enum, documented like its neighbours:
+
+```java
+/**
+ * Called when the set of enabled component categories changes. The first parameter is a
+ * Set<String> of disabled category names. All categories not included are
+ * enabled.
+ */
+COMPONENT_CATEGORIES_CHANGED
+```
+
+The payload mirrors `LAYER_VISIBILITY_CHANGED`, which passes the set of hidden
+layers. Views are free to ignore it and re-read `getVisibleComponentTypes()`,
+which is what both of them will do.
+
+## 4. Core: `IPlugInPort`
+
+Next to `FAVORITES_KEY`:
+
+```java
+public static final String DISABLED_CATEGORIES_KEY = "disabledComponentCategories";
+```
+
+and four methods, the accessor documented immediately after
+`getComponentTypes()` so the difference between the two is impossible to miss:
+
+```java
+/**
+ * Returns the {@link ComponentType}s that should be offered to the user, i.e. the result of
+ * {@link #getComponentTypes()} without the categories the user disabled. Intended for component
+ * browsers; anything that needs to resolve an arbitrary component class must use
+ * {@link #getComponentTypes()} instead.
+ */
+Map> getVisibleComponentTypes();
+
+boolean isCategoryEnabled(String category);
+
+void setCategoryEnabled(String category, boolean enabled);
+
+void resetCategoryVisibility();
+```
+
+Keeping the set arithmetic behind these three mutators means the Swing action
+never parses the stored list and never touches the config key, so the meaning of
+"disabled" lives in exactly one place.
+
+## 5. Core: `Presenter`
+
+The disabled set is read from `configManager` on demand rather than cached in a
+field. None of these calls is on a hot path - they run when a browser rebuilds
+or a menu item is toggled - and a stateless read cannot go stale:
+
+```java
+@SuppressWarnings("unchecked")
+private Set getDisabledCategories() {
+ return new HashSet(
+ (List) configManager.readObject(DISABLED_CATEGORIES_KEY, new ArrayList()));
+}
+```
+
+`getVisibleComponentTypes()` copies the entries it keeps into a new map;
+`ComponentProcessor` hands back its own cached instance, which must not be
+mutated.
+
+`setCategoryEnabled` adds or removes one name and writes a sorted
+`ArrayList` back, so the config file stays stable across sessions.
+`resetCategoryVisibility()` writes an empty list.
+
+The event is fired from a config listener, registered in the constructor next to
+the existing `SHOW_NODE_NAME_TOOLTIPS_KEY` one, rather than from the setters:
+
+```java
+configManager.addConfigListener(IPlugInPort.DISABLED_CATEGORIES_KEY, new IConfigListener() {
+ @Override
+ public void valueChanged(String key, Object value) {
+ messageDispatcher.dispatchMessage(EventType.COMPONENT_CATEGORIES_CHANGED,
+ getDisabledCategories());
+ }
+});
+```
+
+Any writer then refreshes the views, not just the menu. The dispatcher is
+constructed with `new MessageDispatcher(true)` - synchronous - so the
+views are rebuilt on the same EDT call that toggled the checkbox, and no
+`invokeLater` is needed.
+
+## 6. Swing: the menu
+
+Two new action classes beside `ConfigAction` and `ComponentBrowserAction` in
+`org.diylc.swing.actions`:
+
+- **`ComponentCategoryAction`** - one per category. Modelled on `ConfigAction`:
+ `NAME` is the category, `IView.CHECK_BOX_MENU_ITEM` is `true`,
+ `SELECTED_KEY` is seeded from `plugInPort.isCategoryEnabled(category)`, and a
+ listener on `DISABLED_CATEGORIES_KEY` re-reads that flag so "Reset All"
+ re-checks every item in the submenu. `actionPerformed` calls
+ `setCategoryEnabled`.
+- **`ResetCategoriesAction`** - a plain item calling
+ `resetCategoryVisibility()`.
+
+`ActionFactory` gets `createComponentCategoryAction(plugInPort, category)` and
+`createResetCategoriesAction(plugInPort)` next to
+`createComponentBrowserAction`.
+
+`ConfigPlugin` gains `CATEGORIES_MENU = "Component Categories"` and, placed
+alphabetically between "Auto-Edit Mode" and "Continuous Creation" to match the
+ordering of the rest of the menu:
+
+```java
+swingUI.injectSubmenu(CATEGORIES_MENU, IconLoader.FolderPreferences.getIcon(), CONFIG_MENU);
+swingUI.injectMenuAction(
+ ActionFactory.getInstance().createResetCategoriesAction(plugInPort), CATEGORIES_MENU);
+swingUI.injectMenuAction(null, CATEGORIES_MENU);
+plugInPort.getComponentTypes().keySet().stream().sorted()
+ .forEach(category -> swingUI.injectMenuAction(
+ ActionFactory.getInstance().createComponentCategoryAction(plugInPort, category),
+ CATEGORIES_MENU));
+```
+
+The items are built from the **complete** keyset - a disabled category still
+needs a checkbox to turn it back on. `injectMenuAction(null, ...)` is the
+established way to add a separator, and `MainFrame.injectMenuAction` already
+pushes action names through `LangUtil.translate`, so category names are
+translated like every other menu label.
+
+## 7. Swing: the component tree reacts
+
+`CustomTreeModel` takes its categories from the filtered map while keeping the
+class lookup complete:
+
+```java
+this.componentTypes = plugInPort.getVisibleComponentTypes();
+...
+for (Map.Entry> e : plugInPort.getComponentTypes().entrySet())
+ for (ComponentType c : e.getValue())
+ typesByClass.put(c.getInstanceClass().getCanonicalName(), c);
+```
+
+and gains a `refresh()` that re-reads the visible map, rebuilds `categories`
+with `(Favorites)`, `(Recently Used)` and `(Building Blocks)` back on front, and
+calls the existing `updateVisibleCategories()`, which already clears the leaf
+cache and fires `treeStructureChanged`. The constructor's category setup moves
+into that method so there is one copy of it. `TreePanel` exposes it through its
+existing `getTreeModel()`.
+
+`ComponentTree` currently returns `null` from `getSubscribedEventTypes()` with a
+TODO and does nothing in `processMessage`. Both are filled in:
+
+```java
+@Override
+public EnumSet getSubscribedEventTypes() {
+ return EnumSet.of(EventType.COMPONENT_CATEGORIES_CHANGED);
+}
+
+@Override
+public void processMessage(EventType eventType, Object... params) {
+ if (eventType == EventType.COMPONENT_CATEGORIES_CHANGED) {
+ getTreePanel().getTreeModel().refresh();
+ }
+}
+```
+
+Search text survives the refresh because `updateVisibleCategories()` re-applies
+it; tree expansion state does not, which is acceptable for an explicit menu
+action.
+
+## 8. Swing: the tabbed toolbar reacts
+
+`ComponentTabbedPane` builds its category tabs in the constructor, after the
+three fixed tabs. That loop moves into `refreshCategoryTabs()`, which removes
+every tab from index 3 onwards and re-adds one per visible category. Each
+category's panel is cached in a `Map`, because
+`createComponentPanel` instantiates a `ComponentButtonFactory` button - with its
+icon and variant popup - for every type in the category; toggling a category
+must not pay that cost again for the fifteen categories that did not change.
+
+The selected tab is remembered by title and restored if it is still present. A
+`refreshing` guard keeps the existing `ChangeListener` from persisting
+intermediate `lastSelectedTab` indices while tabs are being removed.
+
+`ToolBox` fills in the same `null`/TODO pair as `ComponentTree`, delegating to
+`getComponentTabbedPane().refreshCategoryTabs()`.
+
+## 9. What is deliberately not touched
+
+`CanvasPanel`, `ExplorerPane`, `NetlistImportDialog` and `ExportVariantsAction`
+keep calling `getComponentTypes()` and keep seeing every category, so keyboard
+shortcuts, the project explorer's renderer, netlist import and variant export
+behave identically whatever is hidden.
+
+## 10. Verification
+
+```bash
+mvn -q -pl diylc-swing -am compile # core + library + swing
+mvn -q test # reactor, since core changed
+```
+
+Then a manual pass in the app: toggle a category with the tree active and with
+the tabbed toolbar active, confirm both update without reopening the menu,
+confirm "Reset All" re-checks every item, confirm a favorited part from a hidden
+category is still clickable, and confirm the setting survives a restart.
+
+The change touches neither rendering, serialization nor netlist output, so a
+regression-suite run is not called for.