diff --git a/core/src/main/java/info/openrocket/core/rocketcomponent/RocketComponent.java b/core/src/main/java/info/openrocket/core/rocketcomponent/RocketComponent.java index b4e44cac7..f3b86d98e 100644 --- a/core/src/main/java/info/openrocket/core/rocketcomponent/RocketComponent.java +++ b/core/src/main/java/info/openrocket/core/rocketcomponent/RocketComponent.java @@ -146,6 +146,12 @@ public abstract class RocketComponent implements ChangeSource, Cloneable, Iterab // If true, component change events will not be fired private boolean bypassComponentChangeEvent = false; + + /** + * Controls the visibility of the component. If false, the component will not be rendered. + * Visibility does not affect component simulation. + */ + private boolean isVisible = true; /** @@ -2706,7 +2712,23 @@ public abstract class RocketComponent implements ChangeSource, Cloneable, Iterab } return false; } - + + /** + * Returns true if this component is visible. + * @return True if this component is visible. + */ + public boolean isVisible() { + return isVisible; + } + + /** + * Sets the component's visibility to the specified value. + * @param value Visibility value + */ + public void setVisible(boolean value) { + this.isVisible = value; + fireComponentChangeEvent(ComponentChangeEvent.GRAPHIC_CHANGE); + } /////////// Iterators ////////// diff --git a/swing/src/main/java/info/openrocket/swing/gui/figure3d/geometry/ComponentRenderer.java b/swing/src/main/java/info/openrocket/swing/gui/figure3d/geometry/ComponentRenderer.java index 6b51773d4..b04a76396 100644 --- a/swing/src/main/java/info/openrocket/swing/gui/figure3d/geometry/ComponentRenderer.java +++ b/swing/src/main/java/info/openrocket/swing/gui/figure3d/geometry/ComponentRenderer.java @@ -95,6 +95,9 @@ public class ComponentRenderer { if (glu == null) throw new IllegalStateException(this + " Not Initialized"); + if (!c.isVisible()) { + return; + } glu.gluQuadricNormals(q, GLU.GLU_SMOOTH); if (c instanceof BodyTube) { diff --git a/swing/src/main/java/info/openrocket/swing/gui/main/BasicFrame.java b/swing/src/main/java/info/openrocket/swing/gui/main/BasicFrame.java index 686277cee..a32340323 100644 --- a/swing/src/main/java/info/openrocket/swing/gui/main/BasicFrame.java +++ b/swing/src/main/java/info/openrocket/swing/gui/main/BasicFrame.java @@ -52,8 +52,6 @@ import net.miginfocom.swing.MigLayout; import info.openrocket.core.file.wavefrontobj.export.OBJExportOptions; import info.openrocket.core.file.wavefrontobj.export.OBJExporterFactory; -import info.openrocket.core.file.wavefrontobj.CoordTransform; -import info.openrocket.core.file.wavefrontobj.DefaultCoordTransform; import info.openrocket.core.logging.ErrorSet; import info.openrocket.core.logging.WarningSet; import info.openrocket.core.appearance.DecalImage; @@ -257,6 +255,7 @@ public class BasicFrame extends JFrame { popupMenu.addSeparator(); popupMenu.add(actions.getScaleAction()); + popupMenu.add(actions.getToggleVisibilityAction()); popupMenu.addSeparator(); popupMenu.add(actions.getExportOBJAction()); @@ -608,6 +607,10 @@ public class BasicFrame extends JFrame { item = new JMenuItem(actions.getScaleAction()); editMenu.add(item); + item = new JMenuItem(actions.getToggleVisibilityAction()); + item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_COMMA, SHORTCUT_KEY)); + editMenu.add(item); + //// Preferences item = new JMenuItem(trans.get("main.menu.edit.preferences")); diff --git a/swing/src/main/java/info/openrocket/swing/gui/main/RocketActions.java b/swing/src/main/java/info/openrocket/swing/gui/main/RocketActions.java index 3e17f4e31..f5eee4550 100644 --- a/swing/src/main/java/info/openrocket/swing/gui/main/RocketActions.java +++ b/swing/src/main/java/info/openrocket/swing/gui/main/RocketActions.java @@ -5,11 +5,7 @@ import java.awt.Toolkit; import java.awt.event.ActionEvent; import java.awt.event.KeyEvent; import java.io.Serial; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.LinkedList; -import java.util.List; +import java.util.*; import javax.swing.AbstractAction; import javax.swing.Action; @@ -18,8 +14,11 @@ import javax.swing.JOptionPane; import javax.swing.KeyStroke; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; + +import info.openrocket.core.rocketcomponent.*; import info.openrocket.swing.gui.configdialog.ComponentConfigDialog; import info.openrocket.swing.gui.dialogs.ScaleDialog; +import info.openrocket.swing.gui.util.GUIUtil; import info.openrocket.swing.gui.util.Icons; import org.slf4j.Logger; @@ -31,12 +30,6 @@ import info.openrocket.core.document.OpenRocketDocument; import info.openrocket.core.document.Simulation; import info.openrocket.core.l10n.Translator; import info.openrocket.core.logging.Markers; -import info.openrocket.core.rocketcomponent.ComponentChangeEvent; -import info.openrocket.core.rocketcomponent.ComponentChangeListener; -import info.openrocket.core.rocketcomponent.ParallelStage; -import info.openrocket.core.rocketcomponent.Rocket; -import info.openrocket.core.rocketcomponent.RocketComponent; -import info.openrocket.core.rocketcomponent.AxialStage; import info.openrocket.core.startup.Application; import info.openrocket.core.util.ORColor; import info.openrocket.core.util.Pair; @@ -61,6 +54,8 @@ public class RocketActions { public static final KeyStroke EDIT_KEY_STROKE = KeyStroke.getKeyStroke(KeyEvent.VK_E, Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx()); public static final KeyStroke DELETE_KEY_STROKE = KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0); + public static final KeyStroke VISIBILITY_KEY_STROKE = KeyStroke.getKeyStroke(KeyEvent.VK_COMMA, + Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx()); private final OpenRocketDocument document; private final Rocket rocket; @@ -81,6 +76,7 @@ public class RocketActions { private final RocketAction moveUpAction; private final RocketAction moveDownAction; private final RocketAction exportOBJAction; + private final RocketAction toggleVisibilityAction; private static final Translator trans = Application.getTranslator(); private static final Logger log = LoggerFactory.getLogger(RocketActions.class); @@ -106,6 +102,7 @@ public class RocketActions { this.moveUpAction = new MoveUpAction(); this.moveDownAction = new MoveDownAction(); this.exportOBJAction = new ExportOBJAction(); + this.toggleVisibilityAction = new ToggleVisibilityAction(); OpenRocketClipboard.addClipboardListener(new ClipboardListener() { @Override @@ -154,6 +151,7 @@ public class RocketActions { moveUpAction.clipboardChanged(); moveDownAction.clipboardChanged(); exportOBJAction.clipboardChanged(); + toggleVisibilityAction.clipboardChanged(); } @@ -207,6 +205,10 @@ public class RocketActions { return exportOBJAction; } + public Action getToggleVisibilityAction() { + return toggleVisibilityAction; + } + /** * Tie an action to a JButton, without using the icon or text of the action for the button. * @@ -1257,4 +1259,83 @@ public class RocketActions { } } + /** + * Action to toggle the visibility of the selected components. + */ + private class ToggleVisibilityAction extends RocketAction { + public ToggleVisibilityAction() { + this.putValue(NAME, trans.get("RocketActions.VisibilityAct.Hide")); + this.putValue(SHORT_DESCRIPTION, trans.get("RocketActions.VisibilityAct.ttip.Hide")); + this.putValue(SMALL_ICON, GUIUtil.getUITheme().getVisibilityHiddenIcon()); + this.putValue(MNEMONIC_KEY, KeyEvent.VK_COMMA); + this.putValue(ACCELERATOR_KEY, VISIBILITY_KEY_STROKE); + clipboardChanged(); + } + + @Override + public void clipboardChanged() { + var components = new ArrayList<>(selectionModel.getSelectedComponents()); + super.setEnabled(!components.isEmpty()); + + if (!components.isEmpty()) { + var firstComponent = components.get(0); + + if (components.size() > 1 || isRocketOrStage(firstComponent)) { + if (!firstComponent.isVisible()) { + this.putValue(NAME, trans.get("RocketActions.VisibilityAct.ShowAll")); + this.putValue(SHORT_DESCRIPTION, trans.get("RocketActions.VisibilityAct.ttip.ShowAll")); + } else { + this.putValue(NAME, trans.get("RocketActions.VisibilityAct.HideAll")); + this.putValue(SHORT_DESCRIPTION, trans.get("RocketActions.VisibilityAct.ttip.HideAll")); + } + } else { + if (!firstComponent.isVisible()) { + this.putValue(NAME, trans.get("RocketActions.VisibilityAct.Show")); + this.putValue(SHORT_DESCRIPTION, trans.get("RocketActions.VisibilityAct.ttip.Show")); + } else { + this.putValue(NAME, trans.get("RocketActions.VisibilityAct.Hide")); + this.putValue(SHORT_DESCRIPTION, trans.get("RocketActions.VisibilityAct.ttip.Hide")); + } + } + } + } + + @Override + public void actionPerformed(ActionEvent e) { + var components = new ArrayList<>(selectionModel.getSelectedComponents()); + + if (!components.isEmpty()) { + var visibility = !components.get(0).isVisible(); + + for (var component : components) { + if (isRocketOrStage(component)) { + getAllDescendants(components).forEach(c -> c.setVisible(visibility)); + continue; + } + component.setVisible(visibility); + } + } + } + + private boolean isRocketOrStage(RocketComponent component) { + return component instanceof AxialStage || component instanceof Rocket; + } + + private Set<RocketComponent> getAllDescendants(List<RocketComponent> components) { + var result = new LinkedHashSet<RocketComponent>(); + var queue = new ArrayDeque<>(components); + + while (!queue.isEmpty()) { + var node = queue.pop(); + result.add(node); + + for (var child : node.getChildren()) { + if (!result.contains(child)) { + queue.add(child); + } + } + } + return result; + } + } } diff --git a/swing/src/main/java/info/openrocket/swing/gui/main/componenttree/ComponentTreeRenderer.java b/swing/src/main/java/info/openrocket/swing/gui/main/componenttree/ComponentTreeRenderer.java index 0d4ea5ba6..e36086901 100644 --- a/swing/src/main/java/info/openrocket/swing/gui/main/componenttree/ComponentTreeRenderer.java +++ b/swing/src/main/java/info/openrocket/swing/gui/main/componenttree/ComponentTreeRenderer.java @@ -3,23 +3,16 @@ package info.openrocket.swing.gui.main.componenttree; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; -import java.awt.FlowLayout; -import java.awt.Font; import java.awt.Graphics; import java.util.LinkedList; import java.util.List; -import javax.swing.BorderFactory; -import javax.swing.BoxLayout; import javax.swing.Icon; -import javax.swing.ImageIcon; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JTree; -import javax.swing.SwingConstants; import javax.swing.UIManager; -import javax.swing.border.Border; import javax.swing.tree.DefaultTreeCellRenderer; import javax.swing.tree.TreePath; @@ -35,6 +28,7 @@ import info.openrocket.core.startup.Application; import info.openrocket.core.unit.UnitGroup; import info.openrocket.core.util.ArrayList; import info.openrocket.core.util.TextUtil; +import info.openrocket.swing.gui.util.Icons; @SuppressWarnings("serial") public class ComponentTreeRenderer extends DefaultTreeCellRenderer { @@ -45,6 +39,7 @@ public class ComponentTreeRenderer extends DefaultTreeCellRenderer { private static Color textSelectionForegroundColor; private static Color componentTreeBackgroundColor; private static Color componentTreeForegroundColor; + private static Color visibilityHiddenForegroundColor; private static Icon massOverrideSubcomponentIcon; private static Icon massOverrideIcon; private static Icon CGOverrideSubcomponentIcon; @@ -92,6 +87,11 @@ public class ComponentTreeRenderer extends DefaultTreeCellRenderer { RocketComponent c = (RocketComponent) value; applyToolTipText(components, c, panel); + // Set the cell text color if component is hidden + if (!c.isVisible() && !sel) { + label.setForeground(visibilityHiddenForegroundColor); + } + // Set the tree icon final Icon treeIcon; if (c.getClass().isAssignableFrom(MassComponent.class)) { @@ -103,10 +103,11 @@ public class ComponentTreeRenderer extends DefaultTreeCellRenderer { panel.add(new JLabel(treeIcon), BorderLayout.WEST); - // Add mass/CG/CD overridden icons + // Add mass/CG/CD overridden and component hidden icons if (c.isMassOverridden() || c.getMassOverriddenBy() != null || c.isCGOverridden() || c.getCGOverriddenBy() != null || - c.isCDOverridden() || c.getCDOverriddenBy() != null) { + c.isCDOverridden() || c.getCDOverriddenBy() != null || + !c.isVisible()) { List<Icon> icons = new LinkedList<>(); if (c.getMassOverriddenBy() != null) { icons.add(massOverrideSubcomponentIcon); @@ -123,6 +124,9 @@ public class ComponentTreeRenderer extends DefaultTreeCellRenderer { } else if (c.isCDOverridden()) { icons.add(CDOverrideIcon); } + if (!c.isVisible()) { + icons.add(Icons.COMPONENT_HIDDEN); + } Icon combinedIcon = combineIcons(3, icons.toArray(new Icon[0])); JLabel overrideIconsLabel = new JLabel(combinedIcon); @@ -144,6 +148,7 @@ public class ComponentTreeRenderer extends DefaultTreeCellRenderer { CGOverrideIcon = GUIUtil.getUITheme().getCGOverrideIcon(); CDOverrideSubcomponentIcon = GUIUtil.getUITheme().getCDOverrideSubcomponentIcon(); CDOverrideIcon = GUIUtil.getUITheme().getCDOverrideIcon(); + visibilityHiddenForegroundColor = GUIUtil.getUITheme().getvisibilityHiddenForegroundColor(); } private void applyToolTipText(List<RocketComponent> components, RocketComponent c, JComponent comp) { diff --git a/swing/src/main/java/info/openrocket/swing/gui/scalefigure/RocketFigure.java b/swing/src/main/java/info/openrocket/swing/gui/scalefigure/RocketFigure.java index fdcfb9dfa..62bfac6b9 100644 --- a/swing/src/main/java/info/openrocket/swing/gui/scalefigure/RocketFigure.java +++ b/swing/src/main/java/info/openrocket/swing/gui/scalefigure/RocketFigure.java @@ -265,6 +265,10 @@ public class RocketFigure extends AbstractScaleFigure { while (!figureShapesCopy.isEmpty()) { RocketComponentShapes rcs = figureShapesCopy.poll(); RocketComponent c = rcs.getComponent(); + + if (!c.isVisible()) { + continue; + } boolean selected = false; // Check if component is in the selection