Added ability to hide component categories in the component tree and toolbar from the Config menu

This commit is contained in:
Bane Stojkovic
2026-10-06 20:19:45 +02:00
parent 51418d6252
commit 6a2270c7b5
16 changed files with 699 additions and 29 deletions
@@ -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
* <code>Set&lt;String&gt;</code> of disabled category names. All categories not included are
* enabled.
*/
COMPONENT_CATEGORIES_CHANGED;
}
@@ -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<String, List<ComponentType>> 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<String, List<ComponentType>> 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.
@@ -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<IPlugIn> plugInSupplier) {
@@ -408,12 +416,58 @@ public class Presenter implements IPlugInPort {
return projectFileManager.isModified();
}
@SuppressWarnings("unchecked")
private Set<String> getDisabledCategories() {
List<String> list = (List<String>) configManager.readObject(
IPlugInPort.DISABLED_CATEGORIES_KEY, new ArrayList<String>());
if (list == null) {
return new HashSet<String>();
}
return new HashSet<String>(list);
}
@SuppressWarnings("unchecked")
@Override
public Map<String, List<ComponentType>> getComponentTypes() {
return ComponentProcessor.getInstance().getComponentTypes();
}
@Override
public Map<String, List<ComponentType>> getVisibleComponentTypes() {
Set<String> disabled = getDisabledCategories();
Map<String, List<ComponentType>> all = getComponentTypes();
Map<String, List<ComponentType>> visible = new HashMap<String, List<ComponentType>>();
for (Map.Entry<String, List<ComponentType>> 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<String> disabled = getDisabledCategories();
if (enabled) {
disabled.remove(category);
} else {
disabled.add(category);
}
List<String> list = new ArrayList<String>(disabled);
Collections.sort(list);
configManager.writeValue(IPlugInPort.DISABLED_CATEGORIES_KEY, list);
}
@Override
public void resetCategoryVisibility() {
configManager.writeValue(IPlugInPort.DISABLED_CATEGORIES_KEY, new ArrayList<String>());
}
@SuppressWarnings({"unchecked"})
private boolean isComponentVisible(IDIYComponent<?> component) {
ComponentType componentType =
@@ -557,6 +557,10 @@
<changeType>NEW_FEATURE</changeType>
<description>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</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>Added ability to hide component categories in the component tree and toolbar from the Config menu</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>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</description>
@@ -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);
}
@@ -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 <http://www.gnu.org/licenses/>.
*/
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);
}
}
@@ -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 <http://www.gnu.org/licenses/>.
*/
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();
}
}
@@ -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);
@@ -89,6 +89,9 @@ class ComponentTabbedPane extends JTabbedPane {
private List<String> pendingRecentComponents = null;
private List<String> blocks;
private final Map<String, Component> categoryPanels = new HashMap<String, Component>();
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<String, List<ComponentType>> componentTypes = plugInPort.getComponentTypes();
List<String> categories = new ArrayList<String>(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<String, List<ComponentType>> visibleComponentTypes =
plugInPort.getVisibleComponentTypes();
List<String> categories = new ArrayList<String>(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<String, List<ComponentType>> componentTypes = plugInPort.getComponentTypes();
@@ -79,13 +79,13 @@ public class ToolBox implements IPlugIn {
@Override
public EnumSet<EventType> 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();
}
}
}
@@ -103,13 +103,13 @@ public class ComponentTree implements IPlugIn {
@Override
public EnumSet<EventType> 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();
}
}
}
@@ -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<String, List<TreeNode>>();
this.typesByClass = new HashMap<String, ComponentType>();
for (Map.Entry<String, List<ComponentType>> 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<String>(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<String, List<TreeNode>>();
this.typesByClass = new HashMap<String, ComponentType>();
for (Map.Entry<String, List<ComponentType>> e : componentTypes.entrySet())
for (ComponentType c : e.getValue())
typesByClass.put(c.getInstanceClass().getCanonicalName(), c);
this.initialize();
updateVisibleCategories();
}
@@ -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;
Binary file not shown.

After

Width:  |  Height:  |  Size: 863 B

@@ -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<String, List<ComponentType>> all = presenter.getComponentTypes();
Map<String, List<ComponentType>> 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<String>[] lastEventDisabled = new Set[1];
IPlugIn testPlugin = new IPlugIn() {
@Override
public void connect(IPlugInPort plugInPort) {}
@Override
public EnumSet<EventType> 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<String>) 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<String> stored = (List<String>) InMemoryConfigurationManager.getInstance()
.readObject(IPlugInPort.DISABLED_CATEGORIES_KEY, new ArrayList<String>());
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() {
+288
View File
@@ -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<String>` 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
* <code>Set&lt;String&gt;</code> 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<String, List<ComponentType>> 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<String> getDisabledCategories() {
return new HashSet<String>(
(List<String>) configManager.readObject(DISABLED_CATEGORIES_KEY, new ArrayList<String>()));
}
```
`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<String>` 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<EventType>(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<String, List<ComponentType>> 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<EventType> 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<String, Component>`, 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.