X-Git-Url: http://source.jalview.org/gitweb/?a=blobdiff_plain;f=src%2Fjalview%2Fgui%2FSplitFrame.java;h=5c4e4d24085a2652912ad8ee0732504e3ffe7669;hb=466c5639624028f92e214121672dc409ad10514b;hp=2288d72bd03d70cc090ea4bca560d9cfa35b03d8;hpb=10ff37d2cb03f342ddbed679951d3e2fef0a404b;p=jalview.git diff --git a/src/jalview/gui/SplitFrame.java b/src/jalview/gui/SplitFrame.java index 2288d72..5c4e4d2 100644 --- a/src/jalview/gui/SplitFrame.java +++ b/src/jalview/gui/SplitFrame.java @@ -1,30 +1,48 @@ package jalview.gui; -import jalview.jbgui.GSplitFrame; - import java.awt.Component; -import java.awt.MouseInfo; -import java.awt.Point; -import java.awt.Rectangle; import java.awt.Toolkit; import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; +import java.beans.PropertyVetoException; import java.util.Map.Entry; import javax.swing.AbstractAction; +import javax.swing.InputMap; import javax.swing.JComponent; import javax.swing.JMenuItem; import javax.swing.KeyStroke; import javax.swing.event.InternalFrameAdapter; import javax.swing.event.InternalFrameEvent; -public class SplitFrame extends GSplitFrame +import jalview.api.SplitContainerI; +import jalview.api.ViewStyleI; +import jalview.datamodel.AlignmentI; +import jalview.jbgui.GAlignFrame; +import jalview.jbgui.GSplitFrame; +import jalview.structure.StructureSelectionManager; +import jalview.viewmodel.AlignmentViewport; + +/** + * An internal frame on the desktop that hosts a horizontally split view of + * linked DNA and Protein alignments. Additional views can be created in linked + * pairs, expanded to separate split frames, or regathered into a single frame. + *

+ * (Some) operations on each alignment are automatically mirrored on the other. + * These include mouseover (highlighting), sequence and column selection, + * sequence ordering and sorting, and grouping, colouring and sorting by tree. + * + * @author gmcarstairs + * + */ +public class SplitFrame extends GSplitFrame implements SplitContainerI { private static final long serialVersionUID = 1L; - public SplitFrame(JComponent top, JComponent bottom) + public SplitFrame(GAlignFrame top, GAlignFrame bottom) { super(top, bottom); init(); @@ -35,7 +53,23 @@ public class SplitFrame extends GSplitFrame */ protected void init() { - setSize(AlignFrame.DEFAULT_WIDTH, Desktop.instance.getHeight() - 20); + getTopFrame().setSplitFrame(this); + getBottomFrame().setSplitFrame(this); + getTopFrame().setVisible(true); + getBottomFrame().setVisible(true); + + ((AlignFrame) getTopFrame()).getViewport().setCodingComplement( + ((AlignFrame) getBottomFrame()).getViewport()); + + int width = ((AlignFrame) getTopFrame()).getWidth(); + // about 50 pixels for the SplitFrame's title bar etc + int height = ((AlignFrame) getTopFrame()).getHeight() + + ((AlignFrame) getBottomFrame()).getHeight() + 50; + height = Math.min(height, Desktop.instance.getHeight() - 20); + // setSize(AlignFrame.DEFAULT_WIDTH, Desktop.instance.getHeight() - 20); + setSize(width, height); + + adjustLayout(); addCloseFrameListener(); @@ -43,6 +77,65 @@ public class SplitFrame extends GSplitFrame addKeyBindings(); + addCommandListeners(); + } + + /** + * Set the top and bottom frames to listen to each others Commands (e.g. Edit, + * Order). + */ + protected void addCommandListeners() + { + // TODO if CommandListener is only ever 1:1 for complementary views, + // may change broadcast pattern to direct messaging (more efficient) + final StructureSelectionManager ssm = StructureSelectionManager + .getStructureSelectionManager(Desktop.instance); + ssm.addCommandListener(((AlignFrame) getTopFrame()).getViewport()); + ssm.addCommandListener(((AlignFrame) getBottomFrame()).getViewport()); + } + + /** + * Do any tweaking and twerking of the layout wanted. + */ + public void adjustLayout() + { + /* + * Ensure sequence ids are the same width for good alignment. + */ + int w1 = ((AlignFrame) getTopFrame()).getViewport().getIdWidth(); + int w2 = ((AlignFrame) getBottomFrame()).getViewport().getIdWidth(); + int w3 = Math.max(w1, w2); + if (w1 != w3) + { + ((AlignFrame) getTopFrame()).getViewport().setIdWidth(w3); + } + if (w2 != w3) + { + ((AlignFrame) getBottomFrame()).getViewport().setIdWidth(w3); + } + + /* + * Set the character width for protein to 3 times that for dna. + */ + boolean scaleThreeToOne = true; // TODO a new Preference option? + if (scaleThreeToOne) + { + final AlignViewport topViewport = ((AlignFrame) getTopFrame()).viewport; + final AlignViewport bottomViewport = ((AlignFrame) getBottomFrame()).viewport; + final AlignmentI topAlignment = topViewport.getAlignment(); + final AlignmentI bottomAlignment = bottomViewport.getAlignment(); + AlignmentViewport cdna = topAlignment.isNucleotide() ? topViewport + : (bottomAlignment.isNucleotide() ? bottomViewport : null); + AlignmentViewport protein = !topAlignment.isNucleotide() ? topViewport + : (!bottomAlignment.isNucleotide() ? bottomViewport : null); + if (protein != null && cdna != null) + { + ViewStyleI vs = cdna.getViewStyle(); + ViewStyleI vs2 = protein.getViewStyle(); + vs2.setCharWidth(3 * vs.getCharWidth()); + protein.setViewStyle(vs2); + } + } } /** @@ -55,14 +148,14 @@ public class SplitFrame extends GSplitFrame @Override public void internalFrameClosed(InternalFrameEvent evt) { - if (getTopComponent() instanceof AlignFrame) + if (getTopFrame() instanceof AlignFrame) { - ((AlignFrame) getTopComponent()) + ((AlignFrame) getTopFrame()) .closeMenuItem_actionPerformed(true); } - if (getBottomComponent() instanceof AlignFrame) + if (getBottomFrame() instanceof AlignFrame) { - ((AlignFrame) getBottomComponent()) + ((AlignFrame) getBottomFrame()) .closeMenuItem_actionPerformed(true); } }; @@ -75,18 +168,24 @@ public class SplitFrame extends GSplitFrame */ protected void addKeyListener() { - // TODO Key Bindings rather than KeyListener are recommended for Swing addKeyListener(new KeyAdapter() { @Override public void keyPressed(KeyEvent e) { - Component c = getComponentAtMouse(); - if (c != null) + AlignFrame af = (AlignFrame) getFrameAtMouse(); + + /* + * Intercept and override any keys here if wanted. + */ + if (!overrideKey(e, af)) { - for (KeyListener kl : c.getKeyListeners()) + if (af != null) { - kl.keyPressed(e); + for (KeyListener kl : af.getKeyListeners()) + { + kl.keyPressed(e); + } } } } @@ -94,7 +193,7 @@ public class SplitFrame extends GSplitFrame @Override public void keyReleased(KeyEvent e) { - Component c = getComponentAtMouse(); + Component c = getFrameAtMouse(); if (c != null) { for (KeyListener kl : c.getKeyListeners()) @@ -108,80 +207,433 @@ public class SplitFrame extends GSplitFrame } /** - * Returns the split pane component the mouse is in, or null if neither. + * Returns true if the key event is overriden and actioned (or ignored) here, + * else returns false, indicating it should be delegated to the AlignFrame's + * usual handler. + *

+ * We can't handle Cmd-Key combinations here, instead this is done by + * overriding key bindings. * + * @see addKeyOverrides + * @param e + * @param af * @return */ - protected Component getComponentAtMouse() + protected boolean overrideKey(KeyEvent e, AlignFrame af) { - Point loc = MouseInfo.getPointerInfo().getLocation(); - - if (isIn(loc, getTopComponent())) { - return getTopComponent(); - } - else if (isIn(loc, getBottomComponent())) + boolean actioned = false; + int keyCode = e.getKeyCode(); + switch (keyCode) { - return getBottomComponent(); + case KeyEvent.VK_DOWN: + if (e.isAltDown() || !af.viewport.cursorMode) + { + /* + * Key down (or Alt-key-down in cursor mode) - move selected sequences + */ + ((AlignFrame) getTopFrame()).moveSelectedSequences(false); + ((AlignFrame) getBottomFrame()).moveSelectedSequences(false); + actioned = true; + e.consume(); + } + break; + case KeyEvent.VK_UP: + if (e.isAltDown() || !af.viewport.cursorMode) + { + /* + * Key up (or Alt-key-up in cursor mode) - move selected sequences + */ + ((AlignFrame) getTopFrame()).moveSelectedSequences(true); + ((AlignFrame) getBottomFrame()).moveSelectedSequences(true); + actioned = true; + e.consume(); + } + default: } - return null; + return actioned; } - private boolean isIn(Point loc, JComponent comp) + /** + * Set key bindings (recommended for Swing over key accelerators). + */ + private void addKeyBindings() { - Point p = comp.getLocationOnScreen(); - Rectangle r = new Rectangle(p.x, p.y, comp.getWidth(), comp.getHeight()); - return r.contains(loc); + overrideDelegatedKeyBindings(); + + overrideImplementedKeyBindings(); } /** - * Set key bindings (recommended for Swing over key accelerators). + * Override key bindings with alternative action methods implemented in this + * class. */ - private void addKeyBindings() + protected void overrideImplementedKeyBindings() { + overrideFind(); + overrideNewView(); + overrideCloseView(); + overrideExpandViews(); + overrideGatherViews(); + } + + /** + * Replace Cmd-W close view action with our version. + */ + protected void overrideCloseView() + { + AbstractAction action; /* - * Bind Cmd-Alt/I (invert column selection) + * Ctrl-W / Cmd-W - close view or window */ - // If AlignFrame exposed a Map of Action objects by key, - // we could delegate all key bindings with a single lookup - this.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put( - KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_I, Toolkit - .getDefaultToolkit().getMenuShortcutKeyMask() - | java.awt.event.KeyEvent.ALT_MASK, false), - "INV_SEQ_SEL"); - this.getActionMap().put("INV_SEQ_SEL", new AbstractAction() + KeyStroke key_cmdW = KeyStroke.getKeyStroke(KeyEvent.VK_W, Toolkit + .getDefaultToolkit().getMenuShortcutKeyMask(), false); + action = new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { - Component c = getComponentAtMouse(); - if (c instanceof AlignFrame) + closeView_actionPerformed(); + } + }; + overrideKeyBinding(key_cmdW, action); + } + + /** + * Replace Cmd-T new view action with our version. + */ + protected void overrideNewView() + { + /* + * Ctrl-T / Cmd-T open new view + */ + KeyStroke key_cmdT = KeyStroke.getKeyStroke(KeyEvent.VK_T, Toolkit + .getDefaultToolkit().getMenuShortcutKeyMask(), false); + AbstractAction action = new AbstractAction() + { + @Override + public void actionPerformed(ActionEvent e) + { + newView_actionPerformed(); + } + }; + overrideKeyBinding(key_cmdT, action); + } + + /** + * For now, delegates key events to the corresponding key accelerator for the + * AlignFrame that the mouse is in. Hopefully can be simplified in future if + * AlignFrame is changed to use key bindings rather than accelerators. + */ + protected void overrideDelegatedKeyBindings() + { + if (getTopFrame() instanceof AlignFrame) + { + /* + * Get all accelerator keys in the top frame (the bottom should be + * identical) and override each one. + */ + for (Entry acc : ((AlignFrame) getTopFrame()) + .getAccelerators().entrySet()) + { + overrideKeyBinding(acc); + } + } + } + + /** + * Overrides an AlignFrame key accelerator with our version which delegates to + * the action listener in whichever frame has the mouse (and does nothing if + * neither has). + * + * @param acc + */ + private void overrideKeyBinding(Entry acc) + { + final KeyStroke ks = acc.getKey(); + InputMap inputMap = this.getInputMap(JComponent.WHEN_FOCUSED); + inputMap.put(ks, ks); + this.getActionMap().put(ks, new AbstractAction() + { + @Override + public void actionPerformed(ActionEvent e) + { + Component c = getFrameAtMouse(); + if (c != null && c instanceof AlignFrame) { - ((AlignFrame) c).invertColSel_actionPerformed(null); + for (ActionListener a : ((AlignFrame) c).getAccelerators() + .get(ks).getActionListeners()) + { + a.actionPerformed(null); + } } } }); - if (getTopComponent() instanceof AlignFrame) + } + + /** + * Replace an accelerator key's action with the specified action. + * + * @param ks + */ + protected void overrideKeyBinding(KeyStroke ks, AbstractAction action) + { + this.getActionMap().put(ks, action); + overrideMenuItem(ks, action); + } + + /** + * Create and link new views (with matching names) in both panes. + *

+ * Note this is _not_ multiple tabs, each hosting a split pane view, rather it + * is a single split pane with each split holding multiple tabs which are + * linked in pairs. + *

+ * TODO implement instead with a tabbed holder in the SplitView, each tab + * holding a single JSplitPane. Would avoid a duplicated tab, at the cost of + * some additional coding. + */ + protected void newView_actionPerformed() + { + AlignFrame topFrame = (AlignFrame) getTopFrame(); + AlignFrame bottomFrame = (AlignFrame) getBottomFrame(); + + AlignmentPanel newTopPanel = topFrame.newView(null, true); + AlignmentPanel newBottomPanel = bottomFrame.newView(null, true); + + /* + * This currently (for the first new view only) leaves the top pane on tab 0 + * but the bottom on tab 1. This results from 'setInitialTabVisible' echoing + * from the bottom back to the first frame. Next line is a fudge to work + * around this. TODO find a better way. + */ + if (topFrame.getTabIndex() != bottomFrame.getTabIndex()) { - for (Entry acc : ((AlignFrame) getTopComponent()) - .getAccelerators().entrySet()) + topFrame.setDisplayedView(newTopPanel); + } + + newBottomPanel.av.viewName = newTopPanel.av.viewName; + newTopPanel.av.setCodingComplement(newBottomPanel.av); + + final StructureSelectionManager ssm = StructureSelectionManager + .getStructureSelectionManager(Desktop.instance); + ssm.addCommandListener(newTopPanel.av); + ssm.addCommandListener(newBottomPanel.av); + } + + /** + * Close the currently selected view in both panes. If there is only one view, + * close this split frame. + */ + protected void closeView_actionPerformed() + { + int viewCount = ((AlignFrame) getTopFrame()).getAlignPanels().size(); + if (viewCount < 2) + { + close(); + return; + } + + AlignmentPanel topPanel = ((AlignFrame) getTopFrame()).alignPanel; + AlignmentPanel bottomPanel = ((AlignFrame) getBottomFrame()).alignPanel; + + ((AlignFrame) getTopFrame()).closeView(topPanel); + ((AlignFrame) getBottomFrame()).closeView(bottomPanel); + + } + + /** + * Close child frames and this split frame. + */ + public void close() + { + ((AlignFrame) getTopFrame()).closeMenuItem_actionPerformed(true); + ((AlignFrame) getBottomFrame()).closeMenuItem_actionPerformed(true); + try + { + this.setClosed(true); + } catch (PropertyVetoException e) + { + // ignore + } + } + + /** + * Replace AlignFrame 'expand views' action with SplitFrame version. + */ + protected void overrideExpandViews() + { + KeyStroke key_X = KeyStroke.getKeyStroke(KeyEvent.VK_X, 0, false); + AbstractAction action = new AbstractAction() + { + @Override + public void actionPerformed(ActionEvent e) { + expandViews_actionPerformed(); + } + }; + overrideMenuItem(key_X, action); + } - final KeyStroke ks = acc.getKey(); - this.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(ks, ks); - this.getActionMap().put(ks, new AbstractAction() + /** + * Replace AlignFrame 'gather views' action with SplitFrame version. + */ + protected void overrideGatherViews() + { + KeyStroke key_G = KeyStroke.getKeyStroke(KeyEvent.VK_G, 0, false); + AbstractAction action = new AbstractAction() + { + @Override + public void actionPerformed(ActionEvent e) + { + gatherViews_actionPerformed(); + } + }; + overrideMenuItem(key_G, action); + } + + /** + * Override the menu action associated with the keystroke in the child frames, + * replacing it with the given action. + * + * @param ks + * @param action + */ + private void overrideMenuItem(KeyStroke ks, AbstractAction action) + { + overrideMenuItem(ks, action, getTopFrame()); + overrideMenuItem(ks, action, getBottomFrame()); + } + + /** + * Override the menu action associated with the keystroke in one child frame, + * replacing it with the given action. Mwahahahaha. + * + * @param key + * @param action + * @param comp + */ + private void overrideMenuItem(KeyStroke key, final AbstractAction action, + JComponent comp) + { + if (comp instanceof AlignFrame) + { + JMenuItem mi = ((AlignFrame) comp).getAccelerators().get(key); + if (mi != null) + { + for (ActionListener al : mi.getActionListeners()) + { + mi.removeActionListener(al); + } + mi.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { - Component c = getComponentAtMouse(); - if (c instanceof AlignFrame) - { - ((AlignFrame) c).getAccelerators().get(ks) - .getActionListeners()[0].actionPerformed(null); - } + action.actionPerformed(e); } }); } } } + + /** + * Expand any multiple views (which are always in pairs) into separate split + * frames. + */ + protected void expandViews_actionPerformed() + { + Desktop.instance.explodeViews(this); + } + + /** + * Gather any other SplitFrame views of this alignment back in as multiple + * (pairs of) views in this SplitFrame. + */ + protected void gatherViews_actionPerformed() + { + Desktop.instance.gatherViews(this); + } + + /** + * Returns the alignment in the complementary frame to the one given. + */ + @Override + public AlignmentI getComplement(Object alignFrame) + { + if (alignFrame == this.getTopFrame()) + { + return ((AlignFrame) getBottomFrame()).viewport.getAlignment(); + } + else if (alignFrame == this.getBottomFrame()) + { + return ((AlignFrame) getTopFrame()).viewport.getAlignment(); + } + return null; + } + + /** + * Returns the title of the complementary frame to the one given. + */ + @Override + public String getComplementTitle(Object alignFrame) + { + if (alignFrame == this.getTopFrame()) + { + return ((AlignFrame) getBottomFrame()).getTitle(); + } + else if (alignFrame == this.getBottomFrame()) + { + return ((AlignFrame) getTopFrame()).getTitle(); + } + return null; + } + + /** + * Set the 'other half' to hidden / revealed. + */ + @Override + public void setComplementVisible(Object alignFrame, boolean show) + { + /* + * Hiding the AlignPanel suppresses unnecessary repaints + */ + if (alignFrame == getTopFrame()) + { + ((AlignFrame) getBottomFrame()).alignPanel.setVisible(show); + } + else if (alignFrame == getBottomFrame()) + { + ((AlignFrame) getTopFrame()).alignPanel.setVisible(show); + } + super.setComplementVisible(alignFrame, show); + } + + /** + * Replace Cmd-F Find action with our version. This is necessary because the + * 'default' Finder searches in the first AlignFrame it finds. We need it to + * search in the half of the SplitFrame that has the mouse. + */ + protected void overrideFind() + { + /* + * Ctrl-F / Cmd-F open Finder dialog, 'focused' on the right alignment + */ + KeyStroke key_cmdF = KeyStroke.getKeyStroke(KeyEvent.VK_F, Toolkit + .getDefaultToolkit().getMenuShortcutKeyMask(), false); + AbstractAction action = new AbstractAction() + { + @Override + public void actionPerformed(ActionEvent e) + { + Component c = getFrameAtMouse(); + if (c != null && c instanceof AlignFrame) + { + AlignFrame af = (AlignFrame) c; + new Finder(af.viewport, af.alignPanel); + } + } + }; + overrideKeyBinding(key_cmdF, action); + } } +