mirror of
https://github.com/bancika/diy-layout-creator
synced 2026-11-07 12:24:58 +10:00
Added ability to hide component categories in the component tree and toolbar from the Config menu
This commit is contained in:
@@ -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<String></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);
|
||||
|
||||
+54
-8
@@ -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() {
|
||||
|
||||
@@ -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<String></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.
|
||||
Reference in New Issue
Block a user