diff --git a/swing/src/net/sf/openrocket/gui/main/BasicFrame.java b/swing/src/net/sf/openrocket/gui/main/BasicFrame.java index 7e6c4178e..a0ab3b412 100644 --- a/swing/src/net/sf/openrocket/gui/main/BasicFrame.java +++ b/swing/src/net/sf/openrocket/gui/main/BasicFrame.java @@ -4,9 +4,7 @@ import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; -import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; -import java.awt.event.MouseListener; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.io.File; @@ -19,16 +17,12 @@ import java.net.URL; import java.net.URLDecoder; import java.util.ArrayList; import java.util.Arrays; -import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.concurrent.ExecutionException; import javax.swing.*; import javax.swing.border.BevelBorder; -import javax.swing.border.TitledBorder; import javax.swing.event.ChangeEvent; -import javax.swing.event.TreeSelectionEvent; -import javax.swing.event.TreeSelectionListener; import javax.swing.tree.DefaultTreeSelectionModel; import javax.swing.tree.TreePath; import javax.swing.tree.TreeSelectionModel; @@ -60,7 +54,6 @@ import net.sf.openrocket.gui.dialogs.preferences.PreferencesDialog; import net.sf.openrocket.gui.figure3d.photo.PhotoFrame; import net.sf.openrocket.gui.help.tours.GuidedTourSelectionDialog; import net.sf.openrocket.gui.main.componenttree.ComponentTree; -import net.sf.openrocket.gui.main.flightconfigpanel.FlightConfigurationPanel; import net.sf.openrocket.gui.scalefigure.RocketPanel; import net.sf.openrocket.gui.util.DummyFrameMenuOSX; import net.sf.openrocket.gui.util.FileHelper; @@ -69,13 +62,11 @@ import net.sf.openrocket.gui.util.Icons; import net.sf.openrocket.gui.util.OpenFileWorker; import net.sf.openrocket.gui.util.SaveFileWorker; import net.sf.openrocket.gui.util.SwingPreferences; -import net.sf.openrocket.gui.widgets.SelectColorButton; import net.sf.openrocket.l10n.Translator; import net.sf.openrocket.logging.Markers; import net.sf.openrocket.rocketcomponent.AxialStage; import net.sf.openrocket.rocketcomponent.ComponentChangeEvent; import net.sf.openrocket.rocketcomponent.ComponentChangeListener; -import net.sf.openrocket.rocketcomponent.PodSet; import net.sf.openrocket.rocketcomponent.Rocket; import net.sf.openrocket.rocketcomponent.RocketComponent; import net.sf.openrocket.startup.Application; @@ -108,9 +99,10 @@ public class BasicFrame extends JFrame { public static final int SHIFT_SHORTCUT_KEY = Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx() | SHIFT_DOWN_MASK; - public static final int COMPONENT_TAB = 0; - public static final int CONFIGURATION_TAB = 1; + public static final int DESIGN_TAB = 0; + public static final int FLIGHT_CONFIGURATION_TAB = 1; public static final int SIMULATION_TAB = 2; + private int previousTab = DESIGN_TAB; /** @@ -142,7 +134,9 @@ public class BasicFrame extends JFrame { /** Actions available for rocket modifications */ private final RocketActions actions; - private SimulationPanel simulationPanel; + private final DesignPanel designPanel; + private final FlightConfigurationPanel flightConfigurationPanel; + private final SimulationPanel simulationPanel; public static BasicFrame lastFrameInstance = null; // Latest BasicFrame that was created private static boolean quitCalled = false; // Keeps track whether the quit action has been called @@ -166,57 +160,64 @@ public class BasicFrame extends JFrame { componentSelectionModel = new DefaultTreeSelectionModel(); componentSelectionModel.setSelectionMode(TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION); - // Obtain the simulation selection model that will be used - simulationPanel = new SimulationPanel(document); - simulationSelectionModel = simulationPanel.getSimulationListSelectionModel(); - - // Combine into a DocumentSelectionModel - selectionModel = new DocumentSelectionModel(document); - selectionModel.attachComponentTreeSelectionModel(componentSelectionModel); - selectionModel.attachSimulationListSelectionModel(simulationSelectionModel); - - actions = new RocketActions(document, selectionModel, this, simulationPanel); - - // Populate the popup menu - popupMenu = new JPopupMenu(); - popupMenu.add(actions.getEditAction()); - popupMenu.add(actions.getCutAction()); - popupMenu.add(actions.getCopyAction()); - popupMenu.add(actions.getPasteAction()); - popupMenu.add(actions.getDuplicateAction()); - popupMenu.add(actions.getDeleteAction()); - popupMenu.addSeparator(); - popupMenu.add(actions.getScaleAction()); - + // ----- Create the different BasicFrame panels ----- log.debug("Constructing the BasicFrame UI"); - // The main vertical split pane - JSplitPane vertical = new JSplitPane(JSplitPane.VERTICAL_SPLIT, true); - vertical.setResizeWeight(0.5); - this.add(vertical); + //// Top segment, tabbed pane + simulationPanel = new SimulationPanel(document); + { + // Obtain the simulation selection model that will be used + simulationSelectionModel = simulationPanel.getSimulationListSelectionModel(); - // The top tabbed pane + // Combine into a DocumentSelectionModel + selectionModel = new DocumentSelectionModel(document); + selectionModel.attachComponentTreeSelectionModel(componentSelectionModel); + selectionModel.attachSimulationListSelectionModel(simulationSelectionModel); + + // Create RocketActions + actions = new RocketActions(document, selectionModel, this, simulationPanel); + } + { + // Create the component tree + tree = new ComponentTree(document); + tree.setSelectionModel(componentSelectionModel); + } + + designPanel = new DesignPanel(this, document, tree); + flightConfigurationPanel = new FlightConfigurationPanel(this, document); tabbedPane = new JTabbedPane(); - //// Rocket design - tabbedPane.addTab(trans.get("BasicFrame.tab.Rocketdesign"), null, designTab()); - //// Flight configurations - tabbedPane.addTab(trans.get("BasicFrame.tab.Flightconfig"), null, new FlightConfigurationPanel(this, document)); - //// Flight simulations + tabbedPane.addTab(trans.get("BasicFrame.tab.Rocketdesign"), null, designPanel); + tabbedPane.addTab(trans.get("BasicFrame.tab.Flightconfig"), null, flightConfigurationPanel); tabbedPane.addTab(trans.get("BasicFrame.tab.Flightsim"), null, simulationPanel); // Add change listener to catch when the tabs are changed. This is to run simulations // automatically when the simulation tab is selected. tabbedPane.addChangeListener(new BasicFrame_changeAdapter(this)); - vertical.setTopComponent(tabbedPane); - - // Bottom segment, rocket figure - + //// Bottom segment, rocket figure rocketpanel = new RocketPanel(document, this); - vertical.setBottomComponent(rocketpanel); - rocketpanel.setSelectionModel(tree.getSelectionModel()); + //// The main vertical split pane + JSplitPane vertical = new JSplitPane(JSplitPane.VERTICAL_SPLIT, true); + vertical.setResizeWeight(0.5); + vertical.setTopComponent(tabbedPane); + vertical.setBottomComponent(rocketpanel); + this.add(vertical); + + // Populate the popup menu + { + popupMenu = new JPopupMenu(); + popupMenu.add(actions.getEditAction()); + popupMenu.add(actions.getCutAction()); + popupMenu.add(actions.getCopyAction()); + popupMenu.add(actions.getPasteAction()); + popupMenu.add(actions.getDuplicateAction()); + popupMenu.add(actions.getDeleteAction()); + popupMenu.addSeparator(); + popupMenu.add(actions.getScaleAction()); + } + createMenu(); @@ -272,194 +273,6 @@ public class BasicFrame extends JFrame { } - /** - * Construct the "Rocket design" tab. This contains a horizontal split pane - * with the left component the design tree and the right component buttons - * for adding components. - */ - private JComponent designTab() { - JSplitPane horizontal = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, true); - horizontal.setResizeWeight(0.5); - - - // Upper-left segment, component tree - - JPanel panel = new JPanel(new MigLayout("fill, flowy", "[grow][grow 0]","[grow]")); - - tree = new ComponentTree(document); - tree.setSelectionModel(componentSelectionModel); - - // Remove JTree key events that interfere with menu accelerators - InputMap im = SwingUtilities.getUIInputMap(tree, JComponent.WHEN_FOCUSED); - im.put(KeyStroke.getKeyStroke(KeyEvent.VK_X, SHORTCUT_KEY), null); - im.put(KeyStroke.getKeyStroke(KeyEvent.VK_C, SHORTCUT_KEY), null); - im.put(KeyStroke.getKeyStroke(KeyEvent.VK_V, SHORTCUT_KEY), null); - im.put(KeyStroke.getKeyStroke(KeyEvent.VK_A, SHORTCUT_KEY), null); - im.put(KeyStroke.getKeyStroke(KeyEvent.VK_S, SHORTCUT_KEY), null); - im.put(KeyStroke.getKeyStroke(KeyEvent.VK_O, SHORTCUT_KEY), null); - im.put(KeyStroke.getKeyStroke(KeyEvent.VK_N, SHORTCUT_KEY), null); - - // Visually select all child components of a stage/rocket/podset when it is selected - componentSelectionModel.addTreeSelectionListener(new TreeSelectionListener() { - @Override - public void valueChanged(TreeSelectionEvent e) { - if (tree == null || tree.getSelectionPaths() == null || tree.getSelectionPaths().length == 0 - || rocketpanel == null) return; - - // Get all the components that need to be selected = currently selected components + children of stages/boosters/podsets - List children = new ArrayList<>(Arrays.asList(rocketpanel.getFigure().getSelection())); - for (TreePath p : tree.getSelectionPaths()) { - if (p != null) { - RocketComponent c = (RocketComponent) p.getLastPathComponent(); - if (c instanceof AxialStage || c instanceof Rocket || c instanceof PodSet) { - Iterator iter = c.iterator(false); - while (iter.hasNext()) { - RocketComponent child = iter.next(); - children.add(child); - } - } - } - } - - // Select all the child components - if (rocketpanel.getFigure() != null && rocketpanel.getFigure3d() != null) { - rocketpanel.getFigure().setSelection(children.toArray(new RocketComponent[0])); - rocketpanel.getFigure3d().setSelection(children.toArray(new RocketComponent[0])); - } - } - }); - - // Double-click opens config dialog - MouseListener ml = new MouseAdapter() { - @Override - public void mouseClicked(MouseEvent e) { - int selRow = tree.getRowForLocation(e.getX(), e.getY()); - TreePath selPath = tree.getPathForLocation(e.getX(), e.getY()); - if (selRow != -1) { - if (selPath == null) return; - - // Double-click - if ((e.getButton() == MouseEvent.BUTTON1) && (e.getClickCount() == 2) && !ComponentConfigDialog.isDialogVisible()) { - RocketComponent component = (RocketComponent) selPath.getLastPathComponent(); - - // Multi-component edit if shift/meta key is pressed - if ((e.isShiftDown() || e.isMetaDown()) && tree.getSelectionPaths() != null) { - // Add the other selected components as listeners to the last selected component - for (TreePath p : tree.getSelectionPaths()) { - if (p != null) { - if (p.getLastPathComponent() == component) continue; - RocketComponent c = (RocketComponent) p.getLastPathComponent(); - c.clearConfigListeners(); - component.addConfigListener(c); - } - } - - // Add the selection path to the tree selection - List paths = new LinkedList<>(Arrays.asList(tree.getSelectionPaths())); - paths.add(selPath); - tree.setSelectionPaths(paths.toArray(new TreePath[0])); - } - - ComponentConfigDialog.showDialog(BasicFrame.this, BasicFrame.this.document, component); - } - // Context menu - else if ((e.getButton() == MouseEvent.BUTTON3) && (e.getClickCount() == 1)) { - if (!tree.isPathSelected(selPath)) { - // Select new path - tree.setSelectionPath(selPath); - } - - doComponentTreePopup(e); - } - } else { // Clicked on blank space - tree.clearSelection(); - } - } - }; - tree.addMouseListener(ml); - - // Update dialog when selection is changed - componentSelectionModel.addTreeSelectionListener(new TreeSelectionListener() { - @Override - public void valueChanged(TreeSelectionEvent e) { - // Scroll tree to the selected item - TreePath[] paths = componentSelectionModel.getSelectionPaths(); - if (paths == null || paths.length == 0) - return; - - for (TreePath path : paths) { - tree.scrollPathToVisible(path); - } - - if (!ComponentConfigDialog.isDialogVisible()) - return; - else - ComponentConfigDialog.disposeDialog(); - - RocketComponent c = (RocketComponent) paths[0].getLastPathComponent(); - c.clearConfigListeners(); - for (int i = 1; i < paths.length; i++) { - RocketComponent listener = (RocketComponent) paths[i].getLastPathComponent(); - listener.clearConfigListeners(); - c.addConfigListener(listener); - } - ComponentConfigDialog.showDialog(BasicFrame.this, BasicFrame.this.document, c); - } - }); - - // Place tree inside scroll pane - JScrollPane scroll = new JScrollPane(tree); - panel.add(scroll, "spany, grow, wrap"); - - - // Buttons - JButton button = new SelectColorButton(actions.getMoveUpAction()); - panel.add(button, "sizegroup buttons, aligny 65%"); - - button = new SelectColorButton(actions.getMoveDownAction()); - panel.add(button, "sizegroup buttons, aligny 0%"); - - button = new SelectColorButton(); - RocketActions.tieActionToButtonNoIcon(button, actions.getEditAction()); - button.setMnemonic(0); - panel.add(button, "sizegroup buttons, gaptop 20%"); - - button = new SelectColorButton(); - RocketActions.tieActionToButtonNoIcon(button, actions.getDuplicateAction()); - button.setMnemonic(0); - panel.add(button, "sizegroup buttons"); - - button = new SelectColorButton(); - RocketActions.tieActionToButtonNoIcon(button, actions.getDeleteAction()); - button.setMnemonic(0); - panel.add(button, "sizegroup buttons"); - - horizontal.setLeftComponent(panel); - - - // Upper-right segment, component addition buttons - - panel = new JPanel(new MigLayout("fill, insets 0", "[0::]")); - - scroll = new JScrollPane(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, - ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); - scroll.setViewportView(new ComponentAddButtons(document, componentSelectionModel, - scroll.getViewport())); - scroll.setBorder(null); - scroll.setViewportBorder(null); - - TitledBorder border = BorderFactory.createTitledBorder(trans.get("BasicFrame.title.Addnewcomp")); - GUIUtil.changeFontStyle(border, Font.BOLD); - scroll.setBorder(border); - - panel.add(scroll, "grow"); - - horizontal.setRightComponent(panel); - - return horizontal; - } - - /** * Return the currently selected rocket component, or null if none selected. */ @@ -985,6 +798,10 @@ public class BasicFrame extends JFrame { this.setJMenuBar(menubar); } + public RocketActions getRocketActions() { + return actions; + } + public void doComponentTreePopup(MouseEvent e) { popupMenu.show(e.getComponent(), e.getX(), e.getY()); } @@ -1250,7 +1067,7 @@ public class BasicFrame extends JFrame { /** * Select the tab on the main pane. * - * @param tab one of {@link #COMPONENT_TAB} or {@link #SIMULATION_TAB}. + * @param tab one of {@link #DESIGN_TAB}, {@link #FLIGHT_CONFIGURATION_TAB} or {@link #SIMULATION_TAB}. */ public void selectTab(int tab) { tabbedPane.setSelectedIndex(tab); @@ -1934,8 +1751,21 @@ public class BasicFrame extends JFrame { public void stateChanged(ChangeEvent e) { JTabbedPane tabSource = (JTabbedPane) e.getSource(); int tab = tabSource.getSelectedIndex(); - if (tab == SIMULATION_TAB) { - simulationPanel.activating(); + if (previousTab == SIMULATION_TAB) { + simulationPanel.updatePreviousSelection(); + } + previousTab = tab; + switch (tab) { + case DESIGN_TAB: + designPanel.takeTheSpotlight(); + break; + case FLIGHT_CONFIGURATION_TAB: + flightConfigurationPanel.takeTheSpotlight(); + break; + case SIMULATION_TAB: + simulationPanel.takeTheSpotlight(); + simulationPanel.activating(); + break; } } diff --git a/swing/src/net/sf/openrocket/gui/main/DesignPanel.java b/swing/src/net/sf/openrocket/gui/main/DesignPanel.java new file mode 100644 index 000000000..72065c9ed --- /dev/null +++ b/swing/src/net/sf/openrocket/gui/main/DesignPanel.java @@ -0,0 +1,237 @@ +package net.sf.openrocket.gui.main; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.gui.configdialog.ComponentConfigDialog; +import net.sf.openrocket.gui.main.componenttree.ComponentTree; +import net.sf.openrocket.gui.scalefigure.RocketPanel; +import net.sf.openrocket.gui.util.GUIUtil; +import net.sf.openrocket.gui.widgets.SelectColorButton; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.rocketcomponent.AxialStage; +import net.sf.openrocket.rocketcomponent.PodSet; +import net.sf.openrocket.rocketcomponent.Rocket; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.startup.Application; + +import javax.swing.BorderFactory; +import javax.swing.InputMap; +import javax.swing.JButton; +import javax.swing.JComponent; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JSplitPane; +import javax.swing.KeyStroke; +import javax.swing.ScrollPaneConstants; +import javax.swing.SwingUtilities; +import javax.swing.border.TitledBorder; +import javax.swing.event.TreeSelectionEvent; +import javax.swing.event.TreeSelectionListener; +import javax.swing.tree.TreePath; +import java.awt.Component; +import java.awt.Font; +import java.awt.event.KeyEvent; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +import static net.sf.openrocket.gui.main.BasicFrame.SHORTCUT_KEY; + +/** + * Construct the "Rocket design" tab. This contains a horizontal split pane + * with the left component the design tree and the right component buttons + * for adding components. + */ +public class DesignPanel extends JSplitPane { + private static final Translator trans = Application.getTranslator(); + private final Component tree; + + public DesignPanel(final BasicFrame parent, final OpenRocketDocument document, final ComponentTree tree) { + super(JSplitPane.HORIZONTAL_SPLIT, true); + setResizeWeight(0.5); + this.tree = tree; + + // Upper-left segment, component tree + JPanel panel = new JPanel(new MigLayout("fill, flowy", "[grow][grow 0]","[grow]")); + + // Remove JTree key events that interfere with menu accelerators + InputMap im = SwingUtilities.getUIInputMap(tree, JComponent.WHEN_FOCUSED); + im.put(KeyStroke.getKeyStroke(KeyEvent.VK_X, SHORTCUT_KEY), null); + im.put(KeyStroke.getKeyStroke(KeyEvent.VK_C, SHORTCUT_KEY), null); + im.put(KeyStroke.getKeyStroke(KeyEvent.VK_V, SHORTCUT_KEY), null); + im.put(KeyStroke.getKeyStroke(KeyEvent.VK_A, SHORTCUT_KEY), null); + im.put(KeyStroke.getKeyStroke(KeyEvent.VK_S, SHORTCUT_KEY), null); + im.put(KeyStroke.getKeyStroke(KeyEvent.VK_O, SHORTCUT_KEY), null); + im.put(KeyStroke.getKeyStroke(KeyEvent.VK_N, SHORTCUT_KEY), null); + + // Visually select all child components of a stage/rocket/podset when it is selected + tree.getSelectionModel().addTreeSelectionListener(new TreeSelectionListener() { + @Override + public void valueChanged(TreeSelectionEvent e) { + if (tree == null || tree.getSelectionPaths() == null || tree.getSelectionPaths().length == 0 + || parent.getRocketPanel() == null) return; + + // Get all the components that need to be selected = currently selected components + children of stages/boosters/podsets + List children = new ArrayList<>(Arrays.asList(parent.getRocketPanel().getFigure().getSelection())); + for (TreePath p : tree.getSelectionPaths()) { + if (p != null) { + RocketComponent c = (RocketComponent) p.getLastPathComponent(); + if (c instanceof AxialStage || c instanceof Rocket || c instanceof PodSet) { + Iterator iter = c.iterator(false); + while (iter.hasNext()) { + RocketComponent child = iter.next(); + children.add(child); + } + } + } + } + + // Select all the child components + if (parent.getRocketPanel().getFigure() != null && parent.getRocketPanel().getFigure3d() != null) { + parent.getRocketPanel().getFigure().setSelection(children.toArray(new RocketComponent[0])); + parent.getRocketPanel().getFigure3d().setSelection(children.toArray(new RocketComponent[0])); + } + } + }); + + // Double-click opens config dialog + MouseListener ml = new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + int selRow = tree.getRowForLocation(e.getX(), e.getY()); + TreePath selPath = tree.getPathForLocation(e.getX(), e.getY()); + if (selRow != -1) { + if (selPath == null) return; + + // Double-click + if ((e.getButton() == MouseEvent.BUTTON1) && (e.getClickCount() == 2) && !ComponentConfigDialog.isDialogVisible()) { + RocketComponent component = (RocketComponent) selPath.getLastPathComponent(); + + // Multi-component edit if shift/meta key is pressed + if ((e.isShiftDown() || e.isMetaDown()) && tree.getSelectionPaths() != null) { + // Add the other selected components as listeners to the last selected component + for (TreePath p : tree.getSelectionPaths()) { + if (p != null) { + if (p.getLastPathComponent() == component) continue; + RocketComponent c = (RocketComponent) p.getLastPathComponent(); + c.clearConfigListeners(); + component.addConfigListener(c); + } + } + + // Add the selection path to the tree selection + List paths = new LinkedList<>(Arrays.asList(tree.getSelectionPaths())); + paths.add(selPath); + tree.setSelectionPaths(paths.toArray(new TreePath[0])); + } + + ComponentConfigDialog.showDialog(parent, document, component); + } + // Context menu + else if ((e.getButton() == MouseEvent.BUTTON3) && (e.getClickCount() == 1)) { + if (!tree.isPathSelected(selPath)) { + // Select new path + tree.setSelectionPath(selPath); + } + + parent.doComponentTreePopup(e); + } + } else { // Clicked on blank space + tree.clearSelection(); + } + } + }; + tree.addMouseListener(ml); + + // Update dialog when selection is changed + tree.getSelectionModel().addTreeSelectionListener(new TreeSelectionListener() { + @Override + public void valueChanged(TreeSelectionEvent e) { + // Scroll tree to the selected item + TreePath[] paths = tree.getSelectionModel().getSelectionPaths(); + if (paths == null || paths.length == 0) + return; + + for (TreePath path : paths) { + tree.scrollPathToVisible(path); + } + + if (!ComponentConfigDialog.isDialogVisible()) + return; + else + ComponentConfigDialog.disposeDialog(); + + RocketComponent c = (RocketComponent) paths[0].getLastPathComponent(); + c.clearConfigListeners(); + for (int i = 1; i < paths.length; i++) { + RocketComponent listener = (RocketComponent) paths[i].getLastPathComponent(); + listener.clearConfigListeners(); + c.addConfigListener(listener); + } + ComponentConfigDialog.showDialog(parent, document, c); + } + }); + + // Place tree inside scroll pane + JScrollPane scroll = new JScrollPane(tree); + panel.add(scroll, "spany, grow, wrap"); + + + // Buttons + JButton button = new SelectColorButton(parent.getRocketActions().getMoveUpAction()); + panel.add(button, "sizegroup buttons, aligny 65%"); + + button = new SelectColorButton(parent.getRocketActions().getMoveDownAction()); + panel.add(button, "sizegroup buttons, aligny 0%"); + + button = new SelectColorButton(parent.getRocketActions().getEditAction()); + button.setIcon(null); + button.setMnemonic(0); + panel.add(button, "sizegroup buttons, gaptop 20%"); + + button = new SelectColorButton(parent.getRocketActions().getDuplicateAction()); + button.setIcon(null); + button.setMnemonic(0); + panel.add(button, "sizegroup buttons"); + + button = new SelectColorButton(parent.getRocketActions().getDeleteAction()); + button.setIcon(null); + button.setMnemonic(0); + panel.add(button, "sizegroup buttons"); + + this.setLeftComponent(panel); + + + // Upper-right segment, component addition buttons + + panel = new JPanel(new MigLayout("fill, insets 0", "[0::]")); + + scroll = new JScrollPane(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, + ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); + scroll.setViewportView(new ComponentAddButtons(document, tree.getSelectionModel(), + scroll.getViewport())); + scroll.setBorder(null); + scroll.setViewportBorder(null); + + TitledBorder border = BorderFactory.createTitledBorder(trans.get("BasicFrame.title.Addnewcomp")); + GUIUtil.changeFontStyle(border, Font.BOLD); + scroll.setBorder(border); + + panel.add(scroll, "grow"); + + this.setRightComponent(panel); + } + + /** + * Focus on the component tree. + */ + public void takeTheSpotlight() { + tree.requestFocusInWindow(); + } + +} diff --git a/swing/src/net/sf/openrocket/gui/main/flightconfigpanel/FlightConfigurationPanel.java b/swing/src/net/sf/openrocket/gui/main/FlightConfigurationPanel.java similarity index 86% rename from swing/src/net/sf/openrocket/gui/main/flightconfigpanel/FlightConfigurationPanel.java rename to swing/src/net/sf/openrocket/gui/main/FlightConfigurationPanel.java index 2e37fb14d..b4fc279a0 100644 --- a/swing/src/net/sf/openrocket/gui/main/flightconfigpanel/FlightConfigurationPanel.java +++ b/swing/src/net/sf/openrocket/gui/main/FlightConfigurationPanel.java @@ -1,4 +1,4 @@ -package net.sf.openrocket.gui.main.flightconfigpanel; +package net.sf.openrocket.gui.main; import java.awt.event.ActionEvent; import java.awt.event.MouseEvent; @@ -20,7 +20,10 @@ import net.miginfocom.swing.MigLayout; import net.sf.openrocket.document.OpenRocketDocument; import net.sf.openrocket.document.Simulation; import net.sf.openrocket.gui.dialogs.flightconfiguration.RenameConfigDialog; -import net.sf.openrocket.gui.main.BasicFrame; +import net.sf.openrocket.gui.main.flightconfigpanel.FlightConfigurablePanel; +import net.sf.openrocket.gui.main.flightconfigpanel.MotorConfigurationPanel; +import net.sf.openrocket.gui.main.flightconfigpanel.RecoveryConfigurationPanel; +import net.sf.openrocket.gui.main.flightconfigpanel.SeparationConfigurationPanel; import net.sf.openrocket.l10n.Translator; import net.sf.openrocket.rocketcomponent.ComponentChangeEvent; import net.sf.openrocket.rocketcomponent.FlightConfigurableComponent; @@ -111,20 +114,30 @@ public class FlightConfigurationPanel extends JPanel implements StateChangeListe this.add(duplicateConfButton, "wrap"); tabs.addChangeListener(new ChangeListener() { + private FlightConfigurablePanel previousPanel = motorConfigurationPanel; @Override public void stateChanged(ChangeEvent e) { // Trigger a selection of the motor/recovery/configuration item + FlightConfigurablePanel panel = null; switch (tabs.getSelectedIndex()) { case MOTOR_TAB_INDEX: - motorConfigurationPanel.updateButtonState(); + panel = motorConfigurationPanel; break; case RECOVERY_TAB_INDEX: - recoveryConfigurationPanel.updateButtonState(); + panel = recoveryConfigurationPanel; break; case SEPARATION_TAB_INDEX: - separationConfigurationPanel.updateButtonState(); + panel = separationConfigurationPanel; break; } + + // Update the panel selection, focus, and button state + if (panel == null) return; + synchronizePanelSelection(previousPanel, panel); + panel.updateButtonState(); + panel.takeTheSpotlight(); + panel.updateRocketViewSelection(); + previousPanel = panel; } }); @@ -230,6 +243,7 @@ public class FlightConfigurationPanel extends JPanel implements StateChangeListe } configurationChanged(ComponentChangeEvent.NONFUNCTIONAL_CHANGE); + takeTheSpotlight(); } public void doPopupConfig(MouseEvent e) { @@ -294,6 +308,19 @@ public class FlightConfigurationPanel extends JPanel implements StateChangeListe } + /** + * Synchronize the table row selection of a target panel with the selection in the source panel. + */ + private void synchronizePanelSelection(FlightConfigurablePanel source, FlightConfigurablePanel target) { + if (source == null || target == null) return; + List fids = source.getSelectedConfigurationIds(); + if (fids == null || fids.isEmpty()) { + target.clearSelection(); + } else { + target.setSelectedConfigurationIds(fids); + } + } + private List getSelectedConfigurationIds() { switch (tabs.getSelectedIndex()) { case MOTOR_TAB_INDEX: @@ -366,4 +393,24 @@ public class FlightConfigurationPanel extends JPanel implements StateChangeListe newOrDuplicateConfigAction(true); } } + + /** + * Focus on the table of the config panel that is currently opened. + */ + public void takeTheSpotlight() { + switch (tabs.getSelectedIndex()) { + case MOTOR_TAB_INDEX: + motorConfigurationPanel.takeTheSpotlight(); + motorConfigurationPanel.updateRocketViewSelection(); + break; + case RECOVERY_TAB_INDEX: + recoveryConfigurationPanel.takeTheSpotlight(); + recoveryConfigurationPanel.updateRocketViewSelection(); + break; + case SEPARATION_TAB_INDEX: + separationConfigurationPanel.takeTheSpotlight(); + separationConfigurationPanel.updateRocketViewSelection(); + break; + } + } } diff --git a/swing/src/net/sf/openrocket/gui/main/RocketActions.java b/swing/src/net/sf/openrocket/gui/main/RocketActions.java index 15ed1d597..070b18ee7 100644 --- a/swing/src/net/sf/openrocket/gui/main/RocketActions.java +++ b/swing/src/net/sf/openrocket/gui/main/RocketActions.java @@ -608,7 +608,7 @@ public class RocketActions { parentFrame.selectTab(BasicFrame.SIMULATION_TAB); } else { deleteComponentAction.actionPerformed(e); - parentFrame.selectTab(BasicFrame.COMPONENT_TAB); + parentFrame.selectTab(BasicFrame.DESIGN_TAB); } } @@ -662,7 +662,7 @@ public class RocketActions { OpenRocketClipboard.setClipboard(copiedComponents); delete(components); - parentFrame.selectTab(BasicFrame.COMPONENT_TAB); + parentFrame.selectTab(BasicFrame.DESIGN_TAB); } else if (isSimulationSelected()) { Simulation[] simsCopy = new Simulation[sims.length]; @@ -719,7 +719,7 @@ public class RocketActions { List copiedComponents = new LinkedList<>(copyComponentsMaintainParent(components)); OpenRocketClipboard.setClipboard(copiedComponents); - parentFrame.selectTab(BasicFrame.COMPONENT_TAB); + parentFrame.selectTab(BasicFrame.DESIGN_TAB); } else if (sims != null && sims.length > 0) { Simulation[] simsCopy = new Simulation[sims.length]; for (int i=0; i < sims.length; i++) { @@ -799,7 +799,7 @@ public class RocketActions { selectionModel.setSelectedComponents(successfullyPasted); - parentFrame.selectTab(BasicFrame.COMPONENT_TAB); + parentFrame.selectTab(BasicFrame.DESIGN_TAB); } else if (sims != null) { @@ -896,7 +896,7 @@ public class RocketActions { selectionModel.setSelectedComponents(duplicateComponents); - parentFrame.selectTab(BasicFrame.COMPONENT_TAB); + parentFrame.selectTab(BasicFrame.DESIGN_TAB); } else if (sims != null && sims.length > 0) { ArrayList copySims = new ArrayList(); diff --git a/swing/src/net/sf/openrocket/gui/main/SimulationPanel.java b/swing/src/net/sf/openrocket/gui/main/SimulationPanel.java index cd46a4880..d287d26fa 100644 --- a/swing/src/net/sf/openrocket/gui/main/SimulationPanel.java +++ b/swing/src/net/sf/openrocket/gui/main/SimulationPanel.java @@ -107,6 +107,8 @@ public class SimulationPanel extends JPanel { private final SimulationAction duplicateSimulationAction; private final SimulationAction deleteSimulationAction; + private int[] previousSelection = null; + public SimulationPanel(OpenRocketDocument doc) { super(new MigLayout("fill", "[grow][][][][][][grow]")); @@ -165,6 +167,7 @@ public class SimulationPanel extends JPanel { simulationTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); simulationTable.setDefaultRenderer(Object.class, new JLabelRenderer()); simulationTableModel.setColumnWidths(simulationTable.getColumnModel()); + simulationTable.setFillsViewportHeight(true); // Context menu pm = new JPopupMenu(); @@ -193,19 +196,33 @@ public class SimulationPanel extends JPanel { public void mouseClicked(MouseEvent e) { int selectedRow = simulationTable.getSelectedRow(); - if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() == 2) { - int selected = simulationTable.convertRowIndexToModel(selectedRow); + if (e.getButton() == MouseEvent.BUTTON1) { + // Clear the table selection when clicked outside the table rows. + if (e.getClickCount() == 1) { + int row = simulationTable.rowAtPoint(e.getPoint()); + int column = simulationTable.columnAtPoint(e.getPoint()); - int column = simulationTable.columnAtPoint(e.getPoint()); - if (column == 0) { - SimulationWarningDialog.showWarningDialog(SimulationPanel.this, document.getSimulations().get(selected)); - } else { - simulationTable.clearSelection(); - simulationTable.addRowSelectionInterval(selectedRow, selectedRow); - - openDialog(document.getSimulations().get(selected)); + if (row == -1 || column == -1) { + simulationTable.clearSelection(); + } } - } else if (e.getButton() == MouseEvent.BUTTON3 && e.getClickCount() == 1) { + // Edit the simulation or plot/export + else if (e.getClickCount() == 2) { + int selected = simulationTable.convertRowIndexToModel(selectedRow); + + int column = simulationTable.columnAtPoint(e.getPoint()); + if (column == 0) { + SimulationWarningDialog.showWarningDialog(SimulationPanel.this, document.getSimulations().get(selected)); + } else { + simulationTable.clearSelection(); + simulationTable.addRowSelectionInterval(selectedRow, selectedRow); + + openDialog(document.getSimulations().get(selected)); + } + } + } + // Show context menu + else if (e.getButton() == MouseEvent.BUTTON3 && e.getClickCount() == 1) { // Get the row that the right-click action happened on int r = simulationTable.rowAtPoint(e.getPoint()); @@ -263,6 +280,10 @@ public class SimulationPanel extends JPanel { updateButtonStates(); } + public void updatePreviousSelection() { + this.previousSelection = simulationTable.getSelectedRows(); + } + private void newSimulation() { Simulation sim = new Simulation(document.getRocket()); sim.setName(document.getNextSimulationName()); @@ -294,6 +315,7 @@ public class SimulationPanel extends JPanel { } fireMaintainSelection(); + takeTheSpotlight(); openDialog(true, sim); } @@ -343,6 +365,7 @@ public class SimulationPanel extends JPanel { document.removeSimulation(selection[i]); } simulationTableModel.fireTableDataChanged(); + takeTheSpotlight(); } private void runSimulation() { @@ -354,6 +377,7 @@ public class SimulationPanel extends JPanel { SimulationPanel.this), document, sims).setVisible(true); log.info("Running simulations took " + (System.currentTimeMillis() - t) + " ms"); fireMaintainSelection(); + takeTheSpotlight(); } public void editSimulation() { @@ -490,11 +514,13 @@ public class SimulationPanel extends JPanel { } d.setVisible(true); fireMaintainSelection(); + takeTheSpotlight(); } private void openDialog(final Simulation sim) { boolean plotMode = false; - if (sim.hasSimulationData() && (sim.getStatus() == Status.UPTODATE || sim.getStatus() == Status.EXTERNAL)) { + if (sim.hasSimulationData() && (sim.getStatus() == Status.UPTODATE || sim.getStatus() == Status.LOADED + || sim.getStatus() == Status.EXTERNAL)) { plotMode = true; } openDialog(plotMode, sim); @@ -987,6 +1013,24 @@ public class SimulationPanel extends JPanel { } } + /** + * Focus on the simulation table and maintain the previous row selection(s). + */ + public void takeTheSpotlight() { + simulationTable.requestFocusInWindow(); + if (simulationTable.getSelectedRows().length > 0) { + return; + } + if (previousSelection == null || previousSelection.length == 0) { + simulationTable.setRowSelectionInterval(0, 0); + } else { + simulationTable.clearSelection(); + for (int row : previousSelection) { + simulationTable.addRowSelectionInterval(row, row); + } + } + } + private static class NextRowAction extends AbstractAction { private final JTable table; private final boolean cycle; @@ -1048,6 +1092,5 @@ public class SimulationPanel extends JPanel { table.getSelectionModel().setSelectionInterval(nextRow, nextRow); table.getColumnModel().getSelectionModel().setSelectionInterval(0, 0); } - } } diff --git a/swing/src/net/sf/openrocket/gui/main/flightconfigpanel/FlightConfigurablePanel.java b/swing/src/net/sf/openrocket/gui/main/flightconfigpanel/FlightConfigurablePanel.java index 593cbdc8a..69dc1d532 100644 --- a/swing/src/net/sf/openrocket/gui/main/flightconfigpanel/FlightConfigurablePanel.java +++ b/swing/src/net/sf/openrocket/gui/main/flightconfigpanel/FlightConfigurablePanel.java @@ -1,333 +1,408 @@ -package net.sf.openrocket.gui.main.flightconfigpanel; - -import java.awt.Color; -import java.awt.Component; -import java.awt.Font; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -import javax.swing.JComponent; -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.JTable; -import javax.swing.ListSelectionModel; -import javax.swing.UIManager; -import javax.swing.border.Border; -import javax.swing.border.EmptyBorder; -import javax.swing.event.ListSelectionEvent; -import javax.swing.event.ListSelectionListener; -import javax.swing.table.AbstractTableModel; -import javax.swing.table.DefaultTableCellRenderer; - -import net.sf.openrocket.util.ArrayList; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import net.miginfocom.swing.MigLayout; -import net.sf.openrocket.formatting.RocketDescriptor; -import net.sf.openrocket.gui.util.GUIUtil; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.rocketcomponent.ComponentChangeEvent; -import net.sf.openrocket.rocketcomponent.ComponentChangeListener; -import net.sf.openrocket.rocketcomponent.FlightConfigurableComponent; -import net.sf.openrocket.rocketcomponent.FlightConfigurationId; -import net.sf.openrocket.rocketcomponent.Rocket; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.util.Pair; - - -@SuppressWarnings("serial") -public abstract class FlightConfigurablePanel extends JPanel implements ComponentChangeListener { - - protected static final Translator trans = Application.getTranslator(); - private static final Logger log = LoggerFactory.getLogger(FlightConfigurablePanel.class); - protected RocketDescriptor descriptor = Application.getInjector().getInstance(RocketDescriptor.class); - - protected final FlightConfigurationPanel flightConfigurationPanel; - protected final Rocket rocket; - protected final JTable table; - - public FlightConfigurablePanel(final FlightConfigurationPanel flightConfigurationPanel, Rocket rocket) { - super(new MigLayout("fill")); - this.flightConfigurationPanel = flightConfigurationPanel; - this.rocket = rocket; - table = doTableInitialization(); - - installTableListener(); - synchronizeConfigurationSelection(); - } - - /** - * Update the data in the table, with component change event type {cce} - * @param cce index of the ComponentChangeEvent to use (e.g. ComponentChangeEvent.NONFUNCTIONAL_CHANGE) - */ - public void fireTableDataChanged(int cce) { - int selectedRow = table.getSelectedRow(); - int selectedColumn = table.getSelectedColumn(); - this.rocket.fireComponentChangeEvent(cce); - ((AbstractTableModel)table.getModel()).fireTableDataChanged(); - restoreSelection(selectedRow,selectedColumn); - updateButtonState(); - } - - protected abstract void updateButtonState(); - - @Override - public void componentChanged(ComponentChangeEvent e) { - this.synchronizeConfigurationSelection(); - } - - /** - * Initialize the table using the specific implementation's initializeTable - * method and then select the row to match what the rocket's current selected - * configuration is. - * - * @return the JTable created - */ - private final JTable doTableInitialization() { - JTable table = this.initializeTable(); - FlightConfigurationId current = this.rocket.getSelectedConfiguration().getFlightConfigurationID(); - int col = (table.getColumnCount() > 1) ? table.getColumnCount() - 1 : 0; - for (int row = 0; row < table.getRowCount(); row++) { - FlightConfigurationId rowFCID = rocket.getId(row); - if (rowFCID.equals(current)) { - table.changeSelection(row, col, false, false); - break; - } - } - return table; - } - - protected final void synchronizeConfigurationSelection() { - FlightConfigurationId currentRocketFCID = rocket.getSelectedConfiguration().getFlightConfigurationID(); - FlightConfigurationId selectedFCID = getSelectedConfigurationId(); - - if ( currentRocketFCID == FlightConfigurationId.DEFAULT_VALUE_FCID ) { - // need to unselect - table.clearSelection(); - } else if ( !currentRocketFCID.equals(selectedFCID)){ - // Need to change selection - // We'll select the correct row, in the currently selected column. - int col = table.getSelectedColumn(); - if ( col < 0 ) { - col = (table.getColumnCount() > 1) ? table.getColumnCount() - 1 : 0; - } - - for( int rowNum = 0; rowNum < table.getRowCount(); rowNum++ ) { - FlightConfigurationId rowFCID = rocket.getId(rowNum ); - if ( rowFCID.equals(currentRocketFCID) ) { - table.changeSelection(rowNum, col, false, false); - break; - } - } - } - } - - protected void restoreSelection( int row, int col ) { - if ( row <= 0 || col <= 0 ) { - synchronizeConfigurationSelection(); - return; - } - if ( row >= table.getRowCount() || col >= table.getColumnCount() ) { - synchronizeConfigurationSelection(); - return; - } - table.changeSelection(row, col, true, false); - } - - protected void installTableListener() { - table.getSelectionModel().addListSelectionListener(new ListSelectionListener() { - - @Override - public void valueChanged(ListSelectionEvent e) { - if ( e.getValueIsAdjusting() ) { - return; - } - - /* Find the selected row and set it as the current selected configuration - * for the rocket. This will propagate the event to ensure that other - * pieces of the UI are updated and match the table selection. - */ - int firstrow = e.getFirstIndex(); - int lastrow = e.getLastIndex(); - ListSelectionModel model = (ListSelectionModel) e.getSource(); - for( int row = firstrow; row <= lastrow; row ++) { - if ( model.isSelectedIndex(row) ) { - FlightConfigurationId fcid = (FlightConfigurationId) table.getValueAt(row, table.convertColumnIndexToView(0)); - rocket.setSelectedConfiguration(fcid); - return; - } - } - } - - }); - } - - /** - * Override this method to create the embedded JTable and it's backing Model. - * - * @return - */ - protected abstract JTable initializeTable(); - - protected T getSelectedComponent() { - - int col = table.convertColumnIndexToModel(table.getSelectedColumn()); - int row = table.convertRowIndexToModel(table.getSelectedRow()); - if ( row < 0 || col < 0 ) { - return null; - } - Object tableValue = table.getModel().getValueAt(row, col); - if ( tableValue instanceof Pair ) { - @SuppressWarnings("unchecked") - Pair selectedComponent = (Pair) tableValue; - return selectedComponent.getV(); - } - return null; - } - - protected List getSelectedComponents() { - int[] cols = Arrays.stream(table.getSelectedColumns()).map(table::convertRowIndexToModel).toArray(); - int[] rows = Arrays.stream(table.getSelectedRows()).map(table::convertRowIndexToModel).toArray(); - if (Arrays.stream(cols).min().isEmpty() || Arrays.stream(rows).min().isEmpty() || - Arrays.stream(cols).min().getAsInt() < 0 || Arrays.stream(rows).min().getAsInt() < 0) { - return null; - } - List components = new ArrayList<>(); - for (int row : rows) { - for (int col : cols) { - Object tableValue = table.getModel().getValueAt(row, col); - if (tableValue instanceof Pair) { - @SuppressWarnings("unchecked") - Pair selectedComponent = (Pair) tableValue; - components.add(selectedComponent.getV()); - } - } - } - return components; - } - - protected FlightConfigurationId getSelectedConfigurationId() { - int col = table.convertColumnIndexToModel(table.getSelectedColumn()); - int row = table.convertRowIndexToModel(table.getSelectedRow()); - if ( row < 0 || col < 0 || row >= table.getRowCount() || col >= table.getColumnCount() ) { - return null; - } - Object tableValue = table.getModel().getValueAt(row, col); - if ( tableValue instanceof Pair ) { - @SuppressWarnings("unchecked") - Pair selectedComponent = (Pair) tableValue; - FlightConfigurationId fcid = selectedComponent.getU(); - return fcid; - } else if ( tableValue instanceof FlightConfigurationId ){ - return (FlightConfigurationId) tableValue; - } - return FlightConfigurationId.ERROR_FCID; - } - - protected List getSelectedConfigurationIds() { - int col = table.convertColumnIndexToModel(table.getSelectedColumn()); - int[] rows = Arrays.stream(table.getSelectedRows()).map(table::convertRowIndexToModel).toArray(); - if (Arrays.stream(rows).min().isEmpty() || Arrays.stream(rows).min().getAsInt() < 0 || col < 0 || - Arrays.stream(rows).max().getAsInt() >= table.getRowCount() || col >= table.getColumnCount() ) { - return null; - } - Object[] tableValues = Arrays.stream(rows).mapToObj(c -> table.getModel().getValueAt(c, col)).toArray(); - List Ids = new ArrayList<>(); - for (Object tableValue : tableValues) { - if (tableValue instanceof Pair) { - @SuppressWarnings("unchecked") - Pair selectedComponent = (Pair) tableValue; - FlightConfigurationId fcid = selectedComponent.getU(); - Ids.add(fcid); - } else if (tableValue instanceof FlightConfigurationId) { - Ids.add((FlightConfigurationId) tableValue); - } else { - Ids.add(FlightConfigurationId.ERROR_FCID); - } - } - - return Ids; - } - - protected abstract class FlightConfigurableCellRenderer extends DefaultTableCellRenderer { - - @Override - public Component getTableCellRendererComponent(JTable table, Object newValue, boolean isSelected, boolean hasFocus, int row, int column) { - JLabel label = (JLabel) super.getTableCellRendererComponent(table, newValue, isSelected, hasFocus, row, column); - Object oldValue = table.getModel().getValueAt(row, column); - - // this block is more for the benefit of the reader than the computer -- - // this assignment is technically redundant, but useful to point out that the new value here is often null, - // while the old value seems to always be valid. - if( null == newValue ){ - log.warn("Detected null newValue to render... (oldValue: "+oldValue+")"); - newValue = oldValue; - } - - column = table.convertColumnIndexToModel(column); - switch (column) { - case 0: { - label.setText(descriptor.format(rocket, (FlightConfigurationId) oldValue)); - regular(label); - setSelected(label, table, isSelected, hasFocus); - return label; - } - default: { - @SuppressWarnings("unchecked") - Pair v = (Pair) oldValue; - - if(v!=null){ - FlightConfigurationId fcid = v.getU(); - T component = v.getV(); - label = format(component, fcid, label ); - } - for (Component c : label.getComponents()) { - if (c instanceof JLabel) { - setSelected((JLabel)c, table, isSelected, hasFocus); - } - } - setSelected(label, table, isSelected, hasFocus); - return label; - } - } - } - - private final void setSelected( JComponent c, JTable table, boolean isSelected, boolean hasFocus ) { - c.setOpaque(true); - if ( isSelected) { - c.setBackground(table.getSelectionBackground()); - c.setForeground((Color)UIManager.get("Table.selectionForeground")); - } else { - c.setBackground(table.getBackground()); - c.setForeground(c.getForeground()); - } - Border b = null; - if ( hasFocus ) { - if (isSelected) { - b = UIManager.getBorder("Table.focusSelectedCellHighlightBorder"); - } else { - b = UIManager.getBorder("Table.focusCellHighligtBorder"); - } - } else { - b = new EmptyBorder(1,1,1,1); - } - c.setBorder(b); - } - - protected final void shaded(JLabel label) { - GUIUtil.changeFontStyle(label, Font.ITALIC); - label.setForeground(Color.GRAY); - } - - protected final void regular(JLabel label) { - GUIUtil.changeFontStyle(label, Font.PLAIN); - label.setForeground(Color.BLACK); - } - - protected abstract JLabel format( T component, FlightConfigurationId configId, JLabel label ); - - } - +package net.sf.openrocket.gui.main.flightconfigpanel; + +import java.awt.Color; +import java.awt.Component; +import java.awt.Font; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; + +import javax.swing.JComponent; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JTable; +import javax.swing.ListSelectionModel; +import javax.swing.UIManager; +import javax.swing.border.Border; +import javax.swing.border.EmptyBorder; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; +import javax.swing.table.AbstractTableModel; +import javax.swing.table.DefaultTableCellRenderer; + +import net.sf.openrocket.gui.main.FlightConfigurationPanel; +import net.sf.openrocket.util.ArrayList; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.formatting.RocketDescriptor; +import net.sf.openrocket.gui.util.GUIUtil; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.rocketcomponent.ComponentChangeEvent; +import net.sf.openrocket.rocketcomponent.ComponentChangeListener; +import net.sf.openrocket.rocketcomponent.FlightConfigurableComponent; +import net.sf.openrocket.rocketcomponent.FlightConfigurationId; +import net.sf.openrocket.rocketcomponent.Rocket; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.util.Pair; + + +@SuppressWarnings("serial") +public abstract class FlightConfigurablePanel extends JPanel implements ComponentChangeListener { + + protected static final Translator trans = Application.getTranslator(); + private static final Logger log = LoggerFactory.getLogger(FlightConfigurablePanel.class); + protected RocketDescriptor descriptor = Application.getInjector().getInstance(RocketDescriptor.class); + + protected final FlightConfigurationPanel flightConfigurationPanel; + protected final Rocket rocket; + protected final JTable table; + + public FlightConfigurablePanel(final FlightConfigurationPanel flightConfigurationPanel, Rocket rocket) { + super(new MigLayout("fill")); + this.flightConfigurationPanel = flightConfigurationPanel; + this.rocket = rocket; + table = doTableInitialization(); + + installTableListener(); + synchronizeConfigurationSelection(); + } + + /** + * Update the data in the table, with component change event type {cce} + * @param cce index of the ComponentChangeEvent to use (e.g. ComponentChangeEvent.NONFUNCTIONAL_CHANGE) + */ + public void fireTableDataChanged(int cce) { + int selectedRow = table.getSelectedRow(); + int selectedColumn = table.getSelectedColumn(); + this.rocket.fireComponentChangeEvent(cce); + ((AbstractTableModel)table.getModel()).fireTableDataChanged(); + restoreSelection(selectedRow,selectedColumn); + updateButtonState(); + } + + public abstract void updateButtonState(); + + @Override + public void componentChanged(ComponentChangeEvent e) { + this.synchronizeConfigurationSelection(); + } + + /** + * Initialize the table using the specific implementation's initializeTable + * method and then select the row to match what the rocket's current selected + * configuration is. + * + * @return the JTable created + */ + private final JTable doTableInitialization() { + JTable table = this.initializeTable(); + table.setFillsViewportHeight(true); + FlightConfigurationId current = this.rocket.getSelectedConfiguration().getFlightConfigurationID(); + int col = (table.getColumnCount() > 1) ? table.getColumnCount() - 1 : 0; + for (int row = 0; row < table.getRowCount(); row++) { + FlightConfigurationId rowFCID = rocket.getId(row); + if (rowFCID.equals(current)) { + table.changeSelection(row, col, false, false); + break; + } + } + return table; + } + + public final void synchronizeConfigurationSelection() { + FlightConfigurationId currentRocketFCID = rocket.getSelectedConfiguration().getFlightConfigurationID(); + FlightConfigurationId selectedFCID = getSelectedConfigurationId(); + + if ( currentRocketFCID == FlightConfigurationId.DEFAULT_VALUE_FCID ) { + // need to unselect + table.clearSelection(); + } else if ( !currentRocketFCID.equals(selectedFCID)){ + // Need to change selection + // We'll select the correct row, in the currently selected column. + int col = table.getSelectedColumn(); + if ( col < 0 ) { + col = (table.getColumnCount() > 1) ? table.getColumnCount() - 1 : 0; + } + + for( int rowNum = 0; rowNum < table.getRowCount(); rowNum++ ) { + FlightConfigurationId rowFCID = rocket.getId(rowNum ); + if ( rowFCID.equals(currentRocketFCID) ) { + table.changeSelection(rowNum, col, false, false); + break; + } + } + } + } + + protected void restoreSelection( int row, int col ) { + if ( row <= 0 || col <= 0 ) { + synchronizeConfigurationSelection(); + return; + } + if ( row >= table.getRowCount() || col >= table.getColumnCount() ) { + synchronizeConfigurationSelection(); + return; + } + table.changeSelection(row, col, true, false); + table.requestFocusInWindow(); + } + + protected void installTableListener() { + table.getSelectionModel().addListSelectionListener(new ListSelectionListener() { + + @Override + public void valueChanged(ListSelectionEvent e) { + if ( e.getValueIsAdjusting() ) { + return; + } + + // Don't update the flight configuration for multi-selections + if (table.getSelectionModel().getSelectedItemsCount() > 1) { + return; + } + + /* Find the selected row and set it as the current selected configuration + * for the rocket. This will propagate the event to ensure that other + * pieces of the UI are updated and match the table selection. + */ + int firstrow = e.getFirstIndex(); + int lastrow = e.getLastIndex(); + ListSelectionModel model = (ListSelectionModel) e.getSource(); + for( int row = firstrow; row <= lastrow; row ++) { + if ( model.isSelectedIndex(row) ) { + FlightConfigurationId fcid = (FlightConfigurationId) table.getValueAt(row, table.convertColumnIndexToView(0)); + rocket.setSelectedConfiguration(fcid); + return; + } + } + } + + }); + + // Clear the table selection when clicked outside the table rows. + table.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() == 1) { + int row = table.rowAtPoint(e.getPoint()); + int col = table.columnAtPoint(e.getPoint()); + if (row == -1 || col == -1) { + clearSelection(); + } + } + } + }); + + table.getColumnModel().getSelectionModel().addListSelectionListener(new ListSelectionListener() { + @Override + public void valueChanged(ListSelectionEvent e) { + updateRocketViewSelection(e); + } + }); + } + + /** + * Override this method to create the embedded JTable and it's backing Model. + * + * @return + */ + protected abstract JTable initializeTable(); + + public void clearSelection() { + table.clearSelection(); + } + + public void updateRocketViewSelection() { + ListSelectionEvent e = new ListSelectionEvent(this, 0, 0, false); + updateRocketViewSelection(e); + } + + /** + * Update the selection in the rocket design view, based on the currently selected motor, recovery device, or stage. + */ + public abstract void updateRocketViewSelection(ListSelectionEvent e); + + protected T getSelectedComponent() { + + int col = table.convertColumnIndexToModel(table.getSelectedColumn()); + int row = table.convertRowIndexToModel(table.getSelectedRow()); + if ( row < 0 || col < 0 ) { + return null; + } + Object tableValue = table.getModel().getValueAt(row, col); + if ( tableValue instanceof Pair ) { + @SuppressWarnings("unchecked") + Pair selectedComponent = (Pair) tableValue; + return selectedComponent.getV(); + } + return null; + } + + protected List getSelectedComponents() { + int[] cols = Arrays.stream(table.getSelectedColumns()).map(table::convertRowIndexToModel).toArray(); + int[] rows = Arrays.stream(table.getSelectedRows()).map(table::convertRowIndexToModel).toArray(); + if (Arrays.stream(cols).min().isEmpty() || Arrays.stream(rows).min().isEmpty() || + Arrays.stream(cols).min().getAsInt() < 0 || Arrays.stream(rows).min().getAsInt() < 0) { + return null; + } + List components = new ArrayList<>(); + for (int row : rows) { + for (int col : cols) { + Object tableValue = table.getModel().getValueAt(row, col); + if (tableValue instanceof Pair) { + @SuppressWarnings("unchecked") + Pair selectedComponent = (Pair) tableValue; + components.add(selectedComponent.getV()); + } + } + } + return components; + } + + protected FlightConfigurationId getSelectedConfigurationId() { + int col = table.convertColumnIndexToModel(table.getSelectedColumn()); + int row = table.convertRowIndexToModel(table.getSelectedRow()); + if ( row < 0 || col < 0 || row >= table.getRowCount() || col >= table.getColumnCount() ) { + return null; + } + Object tableValue = table.getModel().getValueAt(row, col); + if ( tableValue instanceof Pair ) { + @SuppressWarnings("unchecked") + Pair selectedComponent = (Pair) tableValue; + FlightConfigurationId fcid = selectedComponent.getU(); + return fcid; + } else if ( tableValue instanceof FlightConfigurationId ){ + return (FlightConfigurationId) tableValue; + } + return FlightConfigurationId.ERROR_FCID; + } + + public List getSelectedConfigurationIds() { + int col = table.convertColumnIndexToModel(table.getSelectedColumn()); + int[] rows = Arrays.stream(table.getSelectedRows()).map(table::convertRowIndexToModel).toArray(); + if (Arrays.stream(rows).min().isEmpty() || Arrays.stream(rows).min().getAsInt() < 0 || col < 0 || + Arrays.stream(rows).max().getAsInt() >= table.getRowCount() || col >= table.getColumnCount() ) { + return null; + } + Object[] tableValues = Arrays.stream(rows).mapToObj(c -> table.getModel().getValueAt(c, col)).toArray(); + List Ids = new ArrayList<>(); + for (Object tableValue : tableValues) { + if (tableValue instanceof Pair) { + @SuppressWarnings("unchecked") + Pair selectedComponent = (Pair) tableValue; + FlightConfigurationId fcid = selectedComponent.getU(); + Ids.add(fcid); + } else if (tableValue instanceof FlightConfigurationId) { + Ids.add((FlightConfigurationId) tableValue); + } else { + Ids.add(FlightConfigurationId.ERROR_FCID); + } + } + + return Ids; + } + + /** + * Select the rows of the table that correspond to the given FlightConfigurationIds. The second column of the table + * will be used for the selection. + * @param fids flight configuration ids to select + */ + public void setSelectedConfigurationIds(List fids) { + if (fids == null || fids.isEmpty() || table.getColumnCount() == 0) return; + + if (getSelectedConfigurationIds() != null && new HashSet<>(getSelectedConfigurationIds()).containsAll(fids)) return; + + table.clearSelection(); + for (FlightConfigurationId id : fids) { + if (id == FlightConfigurationId.DEFAULT_VALUE_FCID) continue; + for (int rowNum = 0; rowNum < table.getRowCount(); rowNum++) { + FlightConfigurationId rowFCID = rocket.getId(rowNum ); + if (rowFCID.equals(id)) { + table.changeSelection(rowNum, 1, true, false); + break; + } + } + } + } + + protected abstract class FlightConfigurableCellRenderer extends DefaultTableCellRenderer { + + @Override + public Component getTableCellRendererComponent(JTable table, Object newValue, boolean isSelected, boolean hasFocus, int row, int column) { + JLabel label = (JLabel) super.getTableCellRendererComponent(table, newValue, isSelected, hasFocus, row, column); + Object oldValue = table.getModel().getValueAt(row, column); + + // this block is more for the benefit of the reader than the computer -- + // this assignment is technically redundant, but useful to point out that the new value here is often null, + // while the old value seems to always be valid. + if( null == newValue ){ + log.warn("Detected null newValue to render... (oldValue: "+oldValue+")"); + newValue = oldValue; + } + + column = table.convertColumnIndexToModel(column); + switch (column) { + case 0: { + label.setText(descriptor.format(rocket, (FlightConfigurationId) oldValue)); + regular(label); + setSelected(label, table, isSelected, hasFocus); + return label; + } + default: { + @SuppressWarnings("unchecked") + Pair v = (Pair) oldValue; + + if(v!=null){ + FlightConfigurationId fcid = v.getU(); + T component = v.getV(); + label = format(component, fcid, label ); + } + for (Component c : label.getComponents()) { + if (c instanceof JLabel) { + setSelected((JLabel)c, table, isSelected, hasFocus); + } + } + setSelected(label, table, isSelected, hasFocus); + return label; + } + } + } + + private final void setSelected( JComponent c, JTable table, boolean isSelected, boolean hasFocus ) { + c.setOpaque(true); + if ( isSelected) { + c.setBackground(table.getSelectionBackground()); + c.setForeground((Color)UIManager.get("Table.selectionForeground")); + } else { + c.setBackground(table.getBackground()); + c.setForeground(c.getForeground()); + } + Border b = null; + if ( hasFocus ) { + if (isSelected) { + b = UIManager.getBorder("Table.focusSelectedCellHighlightBorder"); + } else { + b = UIManager.getBorder("Table.focusCellHighligtBorder"); + } + } else { + b = new EmptyBorder(1,1,1,1); + } + c.setBorder(b); + } + + protected final void shaded(JLabel label) { + GUIUtil.changeFontStyle(label, Font.ITALIC); + label.setForeground(Color.GRAY); + } + + protected final void regular(JLabel label) { + GUIUtil.changeFontStyle(label, Font.PLAIN); + label.setForeground(Color.BLACK); + } + + protected abstract JLabel format( T component, FlightConfigurationId configId, JLabel label ); + + } + + /** + * Focus on the table + */ + public void takeTheSpotlight() { + table.requestFocusInWindow(); + } + } \ No newline at end of file diff --git a/swing/src/net/sf/openrocket/gui/main/flightconfigpanel/MotorConfigurationPanel.java b/swing/src/net/sf/openrocket/gui/main/flightconfigpanel/MotorConfigurationPanel.java index f2e6a3f27..52c15a1f9 100644 --- a/swing/src/net/sf/openrocket/gui/main/flightconfigpanel/MotorConfigurationPanel.java +++ b/swing/src/net/sf/openrocket/gui/main/flightconfigpanel/MotorConfigurationPanel.java @@ -3,8 +3,6 @@ package net.sf.openrocket.gui.main.flightconfigpanel; import java.awt.CardLayout; import java.awt.Dimension; import java.awt.event.ActionEvent; -import java.awt.event.FocusEvent; -import java.awt.event.FocusListener; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; @@ -33,6 +31,7 @@ import net.miginfocom.swing.MigLayout; import net.sf.openrocket.gui.dialogs.flightconfiguration.IgnitionSelectionDialog; import net.sf.openrocket.gui.dialogs.flightconfiguration.MotorMountConfigurationPanel; import net.sf.openrocket.gui.dialogs.motor.MotorChooserDialog; +import net.sf.openrocket.gui.main.FlightConfigurationPanel; import net.sf.openrocket.gui.widgets.SelectColorButton; import net.sf.openrocket.motor.IgnitionEvent; import net.sf.openrocket.motor.Motor; @@ -65,7 +64,7 @@ public class MotorConfigurationPanel extends FlightConfigurablePanel private final JPopupMenu popupMenuFull; // popup menu containing all the options - MotorConfigurationPanel(final FlightConfigurationPanel flightConfigurationPanel, Rocket rocket) { + public MotorConfigurationPanel(final FlightConfigurationPanel flightConfigurationPanel, Rocket rocket) { super(flightConfigurationPanel, rocket); motorChooserDialog = new MotorChooserDialog(SwingUtilities.getWindowAncestor(flightConfigurationPanel)); @@ -250,35 +249,11 @@ public class MotorConfigurationPanel extends FlightConfigurablePanel return configurationTable; } - @Override - protected void installTableListener() { - super.installTableListener(); - - table.getColumnModel().getSelectionModel().addListSelectionListener(new ListSelectionListener() { - @Override - public void valueChanged(ListSelectionEvent e) { - updateComponentSelection(e); - } - }); - - table.addFocusListener(new FocusListener() { - @Override - public void focusGained(FocusEvent e) { - updateComponentSelection(new ListSelectionEvent(this, 0, 0, false)); - } - - @Override - public void focusLost(FocusEvent e) { - - } - }); - } - private void doPopupFull(MouseEvent e) { popupMenuFull.show(e.getComponent(), e.getX(), e.getY()); } - public void updateComponentSelection(ListSelectionEvent e) { + public void updateRocketViewSelection(ListSelectionEvent e) { if (e.getValueIsAdjusting()) { return; } @@ -294,7 +269,7 @@ public class MotorConfigurationPanel extends FlightConfigurablePanel flightConfigurationPanel.setSelectedComponents(components); } - protected void updateButtonState() { + public void updateButtonState() { if( configurationTableModel.getColumnCount() > 1 ) { showContent(); @@ -356,6 +331,8 @@ public class MotorConfigurationPanel extends FlightConfigurablePanel if (update) { fireTableDataChanged(ComponentChangeEvent.MOTOR_CHANGE); + } else { + table.requestFocusInWindow(); } } @@ -420,6 +397,8 @@ public class MotorConfigurationPanel extends FlightConfigurablePanel if (update) { fireTableDataChanged(ComponentChangeEvent.MOTOR_CHANGE); + } else { + table.requestFocusInWindow(); } } @@ -448,6 +427,8 @@ public class MotorConfigurationPanel extends FlightConfigurablePanel if (update) { fireTableDataChanged(ComponentChangeEvent.MOTOR_CHANGE); + } else { + table.requestFocusInWindow(); } } diff --git a/swing/src/net/sf/openrocket/gui/main/flightconfigpanel/RecoveryConfigurationPanel.java b/swing/src/net/sf/openrocket/gui/main/flightconfigpanel/RecoveryConfigurationPanel.java index 11c991e1f..0ecbea16d 100644 --- a/swing/src/net/sf/openrocket/gui/main/flightconfigpanel/RecoveryConfigurationPanel.java +++ b/swing/src/net/sf/openrocket/gui/main/flightconfigpanel/RecoveryConfigurationPanel.java @@ -1,8 +1,6 @@ package net.sf.openrocket.gui.main.flightconfigpanel; import java.awt.event.ActionEvent; -import java.awt.event.FocusEvent; -import java.awt.event.FocusListener; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; @@ -25,6 +23,7 @@ import javax.swing.event.ListSelectionListener; import net.sf.openrocket.formatting.RocketDescriptor; import net.sf.openrocket.gui.dialogs.flightconfiguration.DeploymentSelectionDialog; +import net.sf.openrocket.gui.main.FlightConfigurationPanel; import net.sf.openrocket.l10n.Translator; import net.sf.openrocket.rocketcomponent.*; import net.sf.openrocket.rocketcomponent.DeploymentConfiguration.DeployEvent; @@ -43,7 +42,7 @@ public class RecoveryConfigurationPanel extends FlightConfigurablePanel devices = getSelectedComponents(); List fcIds = getSelectedConfigurationIds(); @@ -235,6 +210,8 @@ public class RecoveryConfigurationPanel extends FlightConfigurablePanel stages = getSelectedComponents(); List fcIds = getSelectedConfigurationIds(); @@ -245,6 +220,8 @@ public class SeparationConfigurationPanel extends FlightConfigurablePanel