diff --git a/core/resources/l10n/messages.properties b/core/resources/l10n/messages.properties index 521279b03..cd4eb8f97 100644 --- a/core/resources/l10n/messages.properties +++ b/core/resources/l10n/messages.properties @@ -33,6 +33,8 @@ RocketActions.CopyAct.Copy = Copy RocketActions.CopyAct.ttip.Copy = Copy this component (and subcomponents) to the clipboard. RocketActions.PasteAct.Paste = Paste RocketActions.PasteAct.ttip.Paste = Paste the component or simulation on the clipboard to the design. +RocketActions.DuplicateAct.Duplicate = Duplicate +RocketActions.DuplicateAct.ttip.Duplicate = Duplicate this component (and subcomponents). RocketActions.EditAct.Edit = Edit RocketActions.EditAct.ttip.Edit = Edit the selected component. RocketActions.NewStageAct.Newstage = New stage diff --git a/core/resources/pix/icons/edit-copy.png b/core/resources/pix/icons/edit-copy.png index b7c938a99..28ac512bd 100644 Binary files a/core/resources/pix/icons/edit-copy.png and b/core/resources/pix/icons/edit-copy.png differ diff --git a/core/resources/pix/icons/edit-duplicate.png b/core/resources/pix/icons/edit-duplicate.png new file mode 100644 index 000000000..c02116357 Binary files /dev/null and b/core/resources/pix/icons/edit-duplicate.png differ diff --git a/core/resources/pix/icons/edit-edit.png b/core/resources/pix/icons/edit-edit.png new file mode 100644 index 000000000..44310b471 Binary files /dev/null and b/core/resources/pix/icons/edit-edit.png differ diff --git a/swing/src/net/sf/openrocket/gui/figure3d/photo/PhotoFrame.java b/swing/src/net/sf/openrocket/gui/figure3d/photo/PhotoFrame.java index c2452a36d..2030dba2b 100644 --- a/swing/src/net/sf/openrocket/gui/figure3d/photo/PhotoFrame.java +++ b/swing/src/net/sf/openrocket/gui/figure3d/photo/PhotoFrame.java @@ -60,7 +60,7 @@ import com.google.inject.Module; @SuppressWarnings("serial") public class PhotoFrame extends JFrame { private static final Logger log = LoggerFactory.getLogger(PhotoFrame.class); - private final int SHORTCUT_KEY = Toolkit.getDefaultToolkit().getMenuShortcutKeyMask(); + private final int SHORTCUT_KEY = Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx(); private final Translator trans = Application.getTranslator(); private final PhotoPanel photoPanel; diff --git a/swing/src/net/sf/openrocket/gui/main/BasicFrame.java b/swing/src/net/sf/openrocket/gui/main/BasicFrame.java index ac607b8bd..51dc03623 100644 --- a/swing/src/net/sf/openrocket/gui/main/BasicFrame.java +++ b/swing/src/net/sf/openrocket/gui/main/BasicFrame.java @@ -87,6 +87,8 @@ import net.sf.openrocket.utils.ComponentPresetEditor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import static java.awt.event.InputEvent.SHIFT_DOWN_MASK; + public class BasicFrame extends JFrame { private static final long serialVersionUID = 948877655223365313L; @@ -98,7 +100,10 @@ public class BasicFrame extends JFrame { private static final Translator trans = Application.getTranslator(); private static final Preferences prefs = Application.getPreferences(); - private static final int SHORTCUT_KEY = Toolkit.getDefaultToolkit().getMenuShortcutKeyMask(); + private static final int SHORTCUT_KEY = Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx(); + + private 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; @@ -124,6 +129,7 @@ public class BasicFrame extends JFrame { private JTabbedPane tabbedPane; private RocketPanel rocketpanel; private ComponentTree tree = null; + private final JPopupMenu popupMenu; private final DocumentSelectionModel selectionModel; private final TreeSelectionModel componentSelectionModel; @@ -163,6 +169,15 @@ public class BasicFrame extends JFrame { actions = new RocketActions(document, selectionModel, this); + // 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()); + log.debug("Constructing the BasicFrame UI"); // The main vertical split pane @@ -278,15 +293,17 @@ public class BasicFrame extends JFrame { // Double-click opens config dialog MouseListener ml = new MouseAdapter() { @Override - public void mousePressed(MouseEvent e) { + 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 ((e.getClickCount() == 2) && !ComponentConfigDialog.isDialogVisible()) { + if ((e.getButton() == MouseEvent.BUTTON1) && (e.getClickCount() == 2) && !ComponentConfigDialog.isDialogVisible()) { // Double-click RocketComponent c = (RocketComponent) selPath.getLastPathComponent(); ComponentConfigDialog.showDialog(BasicFrame.this, BasicFrame.this.document, c); + } else if ((e.getButton() == MouseEvent.BUTTON3) && (e.getClickCount() == 1)) { + doComponentTreePopup(e); } } } @@ -333,7 +350,7 @@ public class BasicFrame extends JFrame { button = new SelectColorButton(actions.getMoveDownAction()); panel.add(button, "sizegroup buttons, aligny 0%"); - button = new SelectColorButton(actions.getEditAction()); + button = new SelectColorButton(actions.getEditActionNoIcon()); panel.add(button, "sizegroup buttons"); button = new SelectColorButton(actions.getDeleteAction()); @@ -698,6 +715,9 @@ public class BasicFrame extends JFrame { item = new JMenuItem(actions.getPasteAction()); menu.add(item); + item = new JMenuItem(actions.getDuplicateAction()); + menu.add(item); + item = new JMenuItem(actions.getDeleteAction()); menu.add(item); @@ -856,7 +876,7 @@ public class BasicFrame extends JFrame { //// Debug log item = new JMenuItem(trans.get("main.menu.help.debugLog"), KeyEvent.VK_D); item.setIcon(Icons.HELP_DEBUG_LOG); - item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_D, SHORTCUT_KEY)); + item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_D, SHIFT_SHORTCUT_KEY)); item.getAccessibleContext().setAccessibleDescription(trans.get("main.menu.help.debugLog.desc")); item.addActionListener(new ActionListener() { @Override @@ -898,6 +918,10 @@ public class BasicFrame extends JFrame { this.setJMenuBar(menubar); } + protected void doComponentTreePopup(MouseEvent e) { + popupMenu.show(e.getComponent(), e.getX(), e.getY()); + } + private JMenu makeDebugMenu() { JMenu menu; JMenuItem item; diff --git a/swing/src/net/sf/openrocket/gui/main/RocketActions.java b/swing/src/net/sf/openrocket/gui/main/RocketActions.java index 77f19a41d..6094397aa 100644 --- a/swing/src/net/sf/openrocket/gui/main/RocketActions.java +++ b/swing/src/net/sf/openrocket/gui/main/RocketActions.java @@ -44,11 +44,14 @@ import net.sf.openrocket.util.Pair; public class RocketActions { public static final KeyStroke CUT_KEY_STROKE = KeyStroke.getKeyStroke(KeyEvent.VK_X, - Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()); + Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx()); public static final KeyStroke COPY_KEY_STROKE = KeyStroke.getKeyStroke(KeyEvent.VK_C, - Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()); + Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx()); public static final KeyStroke PASTE_KEY_STROKE = KeyStroke.getKeyStroke(KeyEvent.VK_V, - Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()); + Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx()); + + public static final KeyStroke DUPLICATE_KEY_STROKE = KeyStroke.getKeyStroke(KeyEvent.VK_D, + Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx()); private final OpenRocketDocument document; private final Rocket rocket; @@ -62,7 +65,9 @@ public class RocketActions { private final RocketAction cutAction; private final RocketAction copyAction; private final RocketAction pasteAction; + private final RocketAction duplicateAction; private final RocketAction editAction; + private final RocketAction editActionNoIcon; private final RocketAction newStageAction; private final RocketAction moveUpAction; private final RocketAction moveDownAction; @@ -83,7 +88,10 @@ public class RocketActions { this.cutAction = new CutAction(); this.copyAction = new CopyAction(); this.pasteAction = new PasteAction(); + this.duplicateAction = new DuplicateAction(); this.editAction = new EditAction(); + this.editActionNoIcon = new EditAction(); + this.editActionNoIcon.putValue(Action.SMALL_ICON, null); this.newStageAction = new NewStageAction(); this.moveUpAction = new MoveUpAction(); this.moveDownAction = new MoveDownAction(); @@ -115,7 +123,9 @@ public class RocketActions { cutAction.clipboardChanged(); copyAction.clipboardChanged(); pasteAction.clipboardChanged(); + duplicateAction.clipboardChanged(); editAction.clipboardChanged(); + editActionNoIcon.clipboardChanged(); newStageAction.clipboardChanged(); moveUpAction.clipboardChanged(); moveDownAction.clipboardChanged(); @@ -147,10 +157,18 @@ public class RocketActions { public Action getPasteAction() { return pasteAction; } + + public Action getDuplicateAction() { + return duplicateAction; + } public Action getEditAction() { return editAction; } + + public Action getEditActionNoIcon() { + return editActionNoIcon; + } public Action getNewStageAction() { return newStageAction; @@ -667,12 +685,12 @@ public class RocketActions { OpenRocketClipboard.setClipboard(copiedComponents); parentFrame.selectTab(BasicFrame.COMPONENT_TAB); - } else if (sims.length > 0) { - + } else if (sims != null && sims.length > 0) { Simulation[] simsCopy = new Simulation[sims.length]; for (int i=0; i < sims.length; i++) { simsCopy[i] = sims[i].copy(); } + OpenRocketClipboard.setClipboard(simsCopy); parentFrame.selectTab(BasicFrame.SIMULATION_TAB); } @@ -765,9 +783,81 @@ public class RocketActions { (OpenRocketClipboard.getClipboardSimulations() != null)); } } - - - + + + /** + * Action that duplicates the selected component. + */ + private class DuplicateAction extends RocketAction { + private static final long serialVersionUID = 1L; + + public DuplicateAction() { + //// Copy + this.putValue(NAME, trans.get("RocketActions.DuplicateAct.Duplicate")); + this.putValue(MNEMONIC_KEY, KeyEvent.VK_D); + this.putValue(ACCELERATOR_KEY, DUPLICATE_KEY_STROKE); + //// Copy this component (and subcomponents) to the clipboard. + this.putValue(SHORT_DESCRIPTION, trans.get("RocketActions.DuplicateAct.ttip.Duplicate")); + this.putValue(SMALL_ICON, Icons.EDIT_DUPLICATE); + clipboardChanged(); + } + + @Override + public void actionPerformed(ActionEvent e) { + List components = selectionModel.getSelectedComponents(); + if (components != null) { + components = new ArrayList<>(components); + fillInMissingSelections(components); + + components.sort(Comparator.comparing(c -> c.getParent() != null ? -c.getParent().getChildPosition(c) : 0)); + } + Simulation[] sims = selectionModel.getSelectedSimulations(); + + if (isCopyable(components)) { + List duplicateComponents = new LinkedList<>(copyComponentsMaintainParent(components)); + + List> positions = new LinkedList<>(); + for (RocketComponent component : duplicateComponents) { + positions.add(getPastePosition(component)); + } + + if (duplicateComponents.size() == 1) { + document.addUndoPosition("Duplicate " + duplicateComponents.get(0).getComponentName()); + } else { + document.addUndoPosition("Duplicate components"); + } + + for (int i = 0; i < duplicateComponents.size(); i++) { + positions.get(i).getU().addChild(duplicateComponents.get(i), positions.get(i).getV()); + } + + selectionModel.setSelectedComponents(duplicateComponents); + } else if (sims != null && sims.length > 0) { + ArrayList copySims = new ArrayList(); + + for (Simulation s: sims) { + Simulation copy = s.duplicateSimulation(rocket); + String name = copy.getName(); + if (name.matches(OpenRocketDocument.SIMULATION_NAME_PREFIX + "[0-9]+ *")) { + copy.setName(document.getNextSimulationName()); + } + document.addSimulation(copy); + copySims.add(copy); + } + // TODO: undo + selectionModel.setSelectedSimulations(copySims.toArray(new Simulation[0])); + + parentFrame.selectTab(BasicFrame.SIMULATION_TAB); + } + } + + @Override + public void clipboardChanged() { + this.setEnabled(isCopyable(selectionModel.getSelectedComponent()) || + isSimulationSelected()); + } + + } @@ -782,6 +872,7 @@ public class RocketActions { this.putValue(NAME, trans.get("RocketActions.EditAct.Edit")); //// Edit the selected component. this.putValue(SHORT_DESCRIPTION, trans.get("RocketActions.EditAct.ttip.Edit")); + this.putValue(SMALL_ICON, Icons.EDIT_EDIT); clipboardChanged(); } diff --git a/swing/src/net/sf/openrocket/gui/util/Icons.java b/swing/src/net/sf/openrocket/gui/util/Icons.java index be095f1c1..ebbfe4345 100644 --- a/swing/src/net/sf/openrocket/gui/util/Icons.java +++ b/swing/src/net/sf/openrocket/gui/util/Icons.java @@ -58,9 +58,11 @@ public class Icons { public static final Icon FILE_QUIT = loadImageIcon("pix/icons/application-exit.png", "Quit OpenRocket"); public static final Icon EDIT_UNDO = loadImageIcon("pix/icons/edit-undo.png", trans.get("Icons.Undo")); public static final Icon EDIT_REDO = loadImageIcon("pix/icons/edit-redo.png", trans.get("Icons.Redo")); + public static final Icon EDIT_EDIT = loadImageIcon("pix/icons/edit-edit.png", "Edit"); public static final Icon EDIT_CUT = loadImageIcon("pix/icons/edit-cut.png", "Cut"); public static final Icon EDIT_COPY = loadImageIcon("pix/icons/edit-copy.png", "Copy"); public static final Icon EDIT_PASTE = loadImageIcon("pix/icons/edit-paste.png", "Paste"); + public static final Icon EDIT_DUPLICATE = loadImageIcon("pix/icons/edit-duplicate.png", "Duplicate"); public static final Icon EDIT_DELETE = loadImageIcon("pix/icons/edit-delete.png", "Delete"); public static final Icon EDIT_SCALE = loadImageIcon("pix/icons/edit-scale.png", "Scale");