Files
diy-layout-creator/diylc/docs/plans/category-visibility.md
T
Bane Stojkovic a743de4024 Controllers (#1080)
* initial

* more components

* fixes

* Update TreePanel.java

* fix boost converter

* Improve wrapping of component names

* Update ULN2003Driver.java

* Update NRF24L01Transceiver.java

* imp

* updates

* improvements

* more components

* arduino

* uno

* icon

* cleanup

* mega

* mega

* improveent

* nano

* esp boards

* esp

* esp32

* esp32

* Update ESP8266NodeMCU.java

* d1

* Update WemosD1Mini.java

* pi

* consolidate esp32

* pi pico updates

* pi zero

* Update RaspberryPiZero.java

* pi pico

* cleanup

* pi improvements

* consolidate pins

* antenna consolidation

* improve solder pads

* teensy

* headers

* teensy fix, test cleanup

* s3

* Update ESP32DevKit.java

* usb standardization

* pico w

* button standardization

* arduino improvements

* Update AbstractMakerBoard.java

* Update RaspberryPiZero.java

* Update RaspberryPi.java

* pi 3

* pi 5 improvements

* Update RaspberryPiZero.java

* fix tests

* esp pinout fix

* Update ESP32DevKit.java

* pin name improvements

* Create tier-1-modern-boards.md

* new versions

* pinouts

* nano improvements

* sanitize node names

* label improvements

* minima fixes

* arduino uno labels

* nano labels

* Update ArduinoNano.java

* mega

* mega

* comment out future stuff

* release prep

* Update RaspberryPiPico.java

* Refactor abstract class

* labeling improvement

* label offset fixes

* Update Teensy.java

* consolidate tests

* micro variants

* Update RaspberryPi.java

* displays tweaks

* display improvements

* neopix

* displays

* improvements

* led matrix

* strip

* led bar graph

* jewel

* tft

* tft

* Update maker-peripherals.md

* nokia

* led bar

* labels

* tft

* cleanup

* oled

* cleanup

* display enhancement

* displays

* neopixel

* displays

* displays

* new tft

* displays

* icon

* variants

* 2-digit 0.56 inch 7 segment

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

* led bar

* Update variants.xml

* color selection for bar graph

* Added 1.8" sd

* tft fixes

* Update WS2812BRing.java

* neopixel improvements

* netlist and node name improvements

* oled tweak

* cleanup

* mockito fix

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-10-09 09:38:09 +02:00

11 KiB

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:

/**
 * 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:

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:

/**
 * 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:

@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:

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:

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:

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:

@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

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.