cab4d271050b086306b1be067d3dcef37fccfee3
[jalview.git] / src / jalview / gui / SplitFrame.java
1 package jalview.gui;
2
3 import java.awt.Component;
4 import java.awt.Toolkit;
5 import java.awt.event.ActionEvent;
6 import java.awt.event.ActionListener;
7 import java.awt.event.KeyAdapter;
8 import java.awt.event.KeyEvent;
9 import java.awt.event.KeyListener;
10 import java.beans.PropertyVetoException;
11 import java.util.Map.Entry;
12
13 import javax.swing.AbstractAction;
14 import javax.swing.InputMap;
15 import javax.swing.JComponent;
16 import javax.swing.JMenuItem;
17 import javax.swing.KeyStroke;
18 import javax.swing.event.InternalFrameAdapter;
19 import javax.swing.event.InternalFrameEvent;
20
21 import jalview.api.SplitContainerI;
22 import jalview.api.ViewStyleI;
23 import jalview.datamodel.AlignmentI;
24 import jalview.jbgui.GAlignFrame;
25 import jalview.jbgui.GSplitFrame;
26 import jalview.structure.StructureSelectionManager;
27 import jalview.viewmodel.AlignmentViewport;
28
29 /**
30  * An internal frame on the desktop that hosts a horizontally split view of
31  * linked DNA and Protein alignments. Additional views can be created in linked
32  * pairs, expanded to separate split frames, or regathered into a single frame.
33  * <p>
34  * (Some) operations on each alignment are automatically mirrored on the other.
35  * These include mouseover (highlighting), sequence and column selection,
36  * sequence ordering and sorting, and grouping, colouring and sorting by tree.
37  * 
38  * @author gmcarstairs
39  *
40  */
41 public class SplitFrame extends GSplitFrame implements SplitContainerI
42 {
43   private static final long serialVersionUID = 1L;
44
45   public SplitFrame(GAlignFrame top, GAlignFrame bottom)
46   {
47     super(top, bottom);
48     init();
49   }
50
51   /**
52    * Initialise this frame.
53    */
54   protected void init()
55   {
56     getTopFrame().setSplitFrame(this);
57     getBottomFrame().setSplitFrame(this);
58     getTopFrame().setVisible(true);
59     getBottomFrame().setVisible(true);
60
61     ((AlignFrame) getTopFrame()).getViewport().setCodingComplement(
62             ((AlignFrame) getBottomFrame()).getViewport());
63
64     int width = ((AlignFrame) getTopFrame()).getWidth();
65     // about 50 pixels for the SplitFrame's title bar etc
66     int height = ((AlignFrame) getTopFrame()).getHeight()
67             + ((AlignFrame) getBottomFrame()).getHeight() + 50;
68     height = Math.min(height, Desktop.instance.getHeight() - 20);
69     // setSize(AlignFrame.DEFAULT_WIDTH, Desktop.instance.getHeight() - 20);
70     setSize(width, height);
71
72     adjustLayout();
73
74     addCloseFrameListener();
75     
76     addKeyListener();
77
78     addKeyBindings();
79
80     addCommandListeners();
81   }
82
83   /**
84    * Set the top and bottom frames to listen to each others Commands (e.g. Edit,
85    * Order).
86    */
87   protected void addCommandListeners()
88   {
89     // TODO if CommandListener is only ever 1:1 for complementary views,
90     // may change broadcast pattern to direct messaging (more efficient)
91     final StructureSelectionManager ssm = StructureSelectionManager
92             .getStructureSelectionManager(Desktop.instance);
93     ssm.addCommandListener(((AlignFrame) getTopFrame()).getViewport());
94     ssm.addCommandListener(((AlignFrame) getBottomFrame()).getViewport());
95   }
96
97   /**
98    * Do any tweaking and twerking of the layout wanted.
99    */
100   public void adjustLayout()
101   {
102     /*
103      * Ensure sequence ids are the same width so sequences line up
104      */
105     int w1 = ((AlignFrame) getTopFrame()).getViewport().getIdWidth();
106     int w2 = ((AlignFrame) getBottomFrame()).getViewport().getIdWidth();
107     int w3 = Math.max(w1, w2);
108     if (w1 != w3)
109     {
110       ((AlignFrame) getTopFrame()).getViewport().setIdWidth(w3);
111     }
112     if (w2 != w3)
113     {
114       ((AlignFrame) getBottomFrame()).getViewport().setIdWidth(w3);
115     }
116
117     /*
118      * Scale protein to either 1 or 3 times character width of dna
119      */
120     final AlignViewport topViewport = ((AlignFrame) getTopFrame()).viewport;
121     final AlignViewport bottomViewport = ((AlignFrame) getBottomFrame()).viewport;
122     final AlignmentI topAlignment = topViewport.getAlignment();
123     final AlignmentI bottomAlignment = bottomViewport.getAlignment();
124     AlignmentViewport cdna = topAlignment.isNucleotide() ? topViewport
125             : (bottomAlignment.isNucleotide() ? bottomViewport : null);
126     AlignmentViewport protein = !topAlignment.isNucleotide() ? topViewport
127             : (!bottomAlignment.isNucleotide() ? bottomViewport : null);
128     if (protein != null && cdna != null)
129     {
130       ViewStyleI vs = protein.getViewStyle();
131       int scale = vs.isScaleProteinAsCdna() ? 3 : 1;
132       vs.setCharWidth(scale * cdna.getViewStyle().getCharWidth());
133       protein.setViewStyle(vs);
134     }
135   }
136
137   /**
138    * Add a listener to tidy up when the frame is closed.
139    */
140   protected void addCloseFrameListener()
141   {
142     addInternalFrameListener(new InternalFrameAdapter()
143     {
144       @Override
145       public void internalFrameClosed(InternalFrameEvent evt)
146       {
147         if (getTopFrame() instanceof AlignFrame)
148         {
149           ((AlignFrame) getTopFrame())
150                   .closeMenuItem_actionPerformed(true);
151         }
152         if (getBottomFrame() instanceof AlignFrame)
153         {
154           ((AlignFrame) getBottomFrame())
155                   .closeMenuItem_actionPerformed(true);
156         }
157       };
158     });
159   }
160
161   /**
162    * Add a key listener that delegates to whichever split component the mouse is
163    * in (or does nothing if neither).
164    */
165   protected void addKeyListener()
166   {
167     addKeyListener(new KeyAdapter() {
168
169       @Override
170       public void keyPressed(KeyEvent e)
171       {
172         AlignFrame af = (AlignFrame) getFrameAtMouse();
173
174         /*
175          * Intercept and override any keys here if wanted.
176          */
177         if (!overrideKey(e, af))
178         {
179           if (af != null)
180           {
181             for (KeyListener kl : af.getKeyListeners())
182             {
183               kl.keyPressed(e);
184             }
185           }
186         }
187       }
188
189       @Override
190       public void keyReleased(KeyEvent e)
191       {
192         Component c = getFrameAtMouse();
193         if (c != null)
194         {
195           for (KeyListener kl : c.getKeyListeners())
196           {
197             kl.keyReleased(e);
198           }
199         }
200       }
201       
202     });
203   }
204
205   /**
206    * Returns true if the key event is overriden and actioned (or ignored) here,
207    * else returns false, indicating it should be delegated to the AlignFrame's
208    * usual handler.
209    * <p>
210    * We can't handle Cmd-Key combinations here, instead this is done by
211    * overriding key bindings.
212    * 
213    * @see addKeyOverrides
214    * @param e
215    * @param af
216    * @return
217    */
218   protected boolean overrideKey(KeyEvent e, AlignFrame af)
219   {
220     boolean actioned = false;
221     int keyCode = e.getKeyCode();
222     switch (keyCode)
223     {
224     case KeyEvent.VK_DOWN:
225       if (e.isAltDown() || !af.viewport.cursorMode)
226       {
227         /*
228          * Key down (or Alt-key-down in cursor mode) - move selected sequences
229          */
230         ((AlignFrame) getTopFrame()).moveSelectedSequences(false);
231         ((AlignFrame) getBottomFrame()).moveSelectedSequences(false);
232         actioned = true;
233         e.consume();
234       }
235       break;
236     case KeyEvent.VK_UP:
237       if (e.isAltDown() || !af.viewport.cursorMode)
238       {
239         /*
240          * Key up (or Alt-key-up in cursor mode) - move selected sequences
241          */
242         ((AlignFrame) getTopFrame()).moveSelectedSequences(true);
243         ((AlignFrame) getBottomFrame()).moveSelectedSequences(true);
244         actioned = true;
245         e.consume();
246       }
247     default:
248     }
249     return actioned;
250   }
251
252   /**
253    * Set key bindings (recommended for Swing over key accelerators).
254    */
255   private void addKeyBindings()
256   {
257     overrideDelegatedKeyBindings();
258
259     overrideImplementedKeyBindings();
260   }
261
262   /**
263    * Override key bindings with alternative action methods implemented in this
264    * class.
265    */
266   protected void overrideImplementedKeyBindings()
267   {
268     overrideFind();
269     overrideNewView();
270     overrideCloseView();
271     overrideExpandViews();
272     overrideGatherViews();
273   }
274
275   /**
276    * Replace Cmd-W close view action with our version.
277    */
278   protected void overrideCloseView()
279   {
280     AbstractAction action;
281     /*
282      * Ctrl-W / Cmd-W - close view or window
283      */
284     KeyStroke key_cmdW = KeyStroke.getKeyStroke(KeyEvent.VK_W, Toolkit
285             .getDefaultToolkit().getMenuShortcutKeyMask(), false);
286     action = new AbstractAction()
287     {
288       @Override
289       public void actionPerformed(ActionEvent e)
290       {
291         closeView_actionPerformed();
292       }
293     };
294     overrideKeyBinding(key_cmdW, action);
295   }
296
297   /**
298    * Replace Cmd-T new view action with our version.
299    */
300   protected void overrideNewView()
301   {
302     /*
303      * Ctrl-T / Cmd-T open new view
304      */
305     KeyStroke key_cmdT = KeyStroke.getKeyStroke(KeyEvent.VK_T, Toolkit
306             .getDefaultToolkit().getMenuShortcutKeyMask(), false);
307     AbstractAction action = new AbstractAction()
308     {
309       @Override
310       public void actionPerformed(ActionEvent e)
311       {
312         newView_actionPerformed();
313       }
314     };
315     overrideKeyBinding(key_cmdT, action);
316   }
317
318   /**
319    * For now, delegates key events to the corresponding key accelerator for the
320    * AlignFrame that the mouse is in. Hopefully can be simplified in future if
321    * AlignFrame is changed to use key bindings rather than accelerators.
322    */
323   protected void overrideDelegatedKeyBindings()
324   {
325     if (getTopFrame() instanceof AlignFrame)
326     {
327       /*
328        * Get all accelerator keys in the top frame (the bottom should be
329        * identical) and override each one.
330        */
331       for (Entry<KeyStroke, JMenuItem> acc : ((AlignFrame) getTopFrame())
332               .getAccelerators().entrySet())
333       {
334         overrideKeyBinding(acc);
335       }
336     }
337   }
338
339   /**
340    * Overrides an AlignFrame key accelerator with our version which delegates to
341    * the action listener in whichever frame has the mouse (and does nothing if
342    * neither has).
343    * 
344    * @param acc
345    */
346   private void overrideKeyBinding(Entry<KeyStroke, JMenuItem> acc)
347   {
348     final KeyStroke ks = acc.getKey();
349     InputMap inputMap = this.getInputMap(JComponent.WHEN_FOCUSED);
350     inputMap.put(ks, ks);
351     this.getActionMap().put(ks, new AbstractAction()
352     {
353       @Override
354       public void actionPerformed(ActionEvent e)
355       {
356         Component c = getFrameAtMouse();
357         if (c != null && c instanceof AlignFrame)
358         {
359           for (ActionListener a : ((AlignFrame) c).getAccelerators()
360                   .get(ks).getActionListeners())
361           {
362             a.actionPerformed(null);
363           }
364         }
365       }
366     });
367   }
368
369   /**
370    * Replace an accelerator key's action with the specified action.
371    * 
372    * @param ks
373    */
374   protected void overrideKeyBinding(KeyStroke ks, AbstractAction action)
375   {
376     this.getActionMap().put(ks, action);
377     overrideMenuItem(ks, action);
378   }
379
380   /**
381    * Create and link new views (with matching names) in both panes.
382    * <p>
383    * Note this is _not_ multiple tabs, each hosting a split pane view, rather it
384    * is a single split pane with each split holding multiple tabs which are
385    * linked in pairs.
386    * <p>
387    * TODO implement instead with a tabbed holder in the SplitView, each tab
388    * holding a single JSplitPane. Would avoid a duplicated tab, at the cost of
389    * some additional coding.
390    */
391   protected void newView_actionPerformed()
392   {
393     AlignFrame topFrame = (AlignFrame) getTopFrame();
394     AlignFrame bottomFrame = (AlignFrame) getBottomFrame();
395     final boolean scaleProteinAsCdna = topFrame.viewport
396             .isScaleProteinAsCdna();
397
398     AlignmentPanel newTopPanel = topFrame.newView(null, true);
399     AlignmentPanel newBottomPanel = bottomFrame.newView(null, true);
400
401     /*
402      * This currently (for the first new view only) leaves the top pane on tab 0
403      * but the bottom on tab 1. This results from 'setInitialTabVisible' echoing
404      * from the bottom back to the first frame. Next line is a fudge to work
405      * around this. TODO find a better way.
406      */
407     if (topFrame.getTabIndex() != bottomFrame.getTabIndex())
408     {
409       topFrame.setDisplayedView(newTopPanel);
410     }
411
412     newBottomPanel.av.viewName = newTopPanel.av.viewName;
413     newTopPanel.av.setCodingComplement(newBottomPanel.av);
414
415     /*
416      * These lines can be removed once scaleProteinAsCdna is added to element
417      * Viewport in jalview.xsd, as Jalview2XML.copyAlignPanel will then take
418      * care of it
419      */
420     newTopPanel.av.setScaleProteinAsCdna(scaleProteinAsCdna);
421     newBottomPanel.av.setScaleProteinAsCdna(scaleProteinAsCdna);
422
423     /*
424      * Line up id labels etc
425      */
426     adjustLayout();
427
428     final StructureSelectionManager ssm = StructureSelectionManager
429             .getStructureSelectionManager(Desktop.instance);
430     ssm.addCommandListener(newTopPanel.av);
431     ssm.addCommandListener(newBottomPanel.av);
432   }
433
434   /**
435    * Close the currently selected view in both panes. If there is only one view,
436    * close this split frame.
437    */
438   protected void closeView_actionPerformed()
439   {
440     int viewCount = ((AlignFrame) getTopFrame()).getAlignPanels().size();
441     if (viewCount < 2)
442     {
443       close();
444       return;
445     }
446
447     AlignmentPanel topPanel = ((AlignFrame) getTopFrame()).alignPanel;
448     AlignmentPanel bottomPanel = ((AlignFrame) getBottomFrame()).alignPanel;
449
450     ((AlignFrame) getTopFrame()).closeView(topPanel);
451     ((AlignFrame) getBottomFrame()).closeView(bottomPanel);
452
453   }
454
455   /**
456    * Close child frames and this split frame.
457    */
458   public void close()
459   {
460     ((AlignFrame) getTopFrame()).closeMenuItem_actionPerformed(true);
461     ((AlignFrame) getBottomFrame()).closeMenuItem_actionPerformed(true);
462     try
463     {
464       this.setClosed(true);
465     } catch (PropertyVetoException e)
466     {
467       // ignore
468     }
469   }
470
471   /**
472    * Replace AlignFrame 'expand views' action with SplitFrame version.
473    */
474   protected void overrideExpandViews()
475   {
476     KeyStroke key_X = KeyStroke.getKeyStroke(KeyEvent.VK_X, 0, false);
477     AbstractAction action = new AbstractAction()
478     {
479       @Override
480       public void actionPerformed(ActionEvent e)
481       {
482         expandViews_actionPerformed();
483       }
484     };
485     overrideMenuItem(key_X, action);
486   }
487
488   /**
489    * Replace AlignFrame 'gather views' action with SplitFrame version.
490    */
491   protected void overrideGatherViews()
492   {
493     KeyStroke key_G = KeyStroke.getKeyStroke(KeyEvent.VK_G, 0, false);
494     AbstractAction action = new AbstractAction()
495     {
496       @Override
497       public void actionPerformed(ActionEvent e)
498       {
499         gatherViews_actionPerformed();
500       }
501     };
502     overrideMenuItem(key_G, action);
503   }
504
505   /**
506    * Override the menu action associated with the keystroke in the child frames,
507    * replacing it with the given action.
508    * 
509    * @param ks
510    * @param action
511    */
512   private void overrideMenuItem(KeyStroke ks, AbstractAction action)
513   {
514     overrideMenuItem(ks, action, getTopFrame());
515     overrideMenuItem(ks, action, getBottomFrame());
516   }
517
518   /**
519    * Override the menu action associated with the keystroke in one child frame,
520    * replacing it with the given action. Mwahahahaha.
521    * 
522    * @param key
523    * @param action
524    * @param comp
525    */
526   private void overrideMenuItem(KeyStroke key, final AbstractAction action,
527           JComponent comp)
528   {
529     if (comp instanceof AlignFrame)
530     {
531       JMenuItem mi = ((AlignFrame) comp).getAccelerators().get(key);
532       if (mi != null)
533       {
534         for (ActionListener al : mi.getActionListeners())
535         {
536           mi.removeActionListener(al);
537         }
538         mi.addActionListener(new ActionListener()
539         {
540           @Override
541           public void actionPerformed(ActionEvent e)
542           {
543             action.actionPerformed(e);
544           }
545         });
546       }
547     }
548   }
549
550   /**
551    * Expand any multiple views (which are always in pairs) into separate split
552    * frames.
553    */
554   protected void expandViews_actionPerformed()
555   {
556     Desktop.instance.explodeViews(this);
557   }
558
559   /**
560    * Gather any other SplitFrame views of this alignment back in as multiple
561    * (pairs of) views in this SplitFrame.
562    */
563   protected void gatherViews_actionPerformed()
564   {
565     Desktop.instance.gatherViews(this);
566   }
567
568   /**
569    * Returns the alignment in the complementary frame to the one given.
570    */
571   @Override
572   public AlignmentI getComplement(Object alignFrame)
573   {
574     if (alignFrame == this.getTopFrame())
575     {
576       return ((AlignFrame) getBottomFrame()).viewport.getAlignment();
577     }
578     else if (alignFrame == this.getBottomFrame())
579     {
580       return ((AlignFrame) getTopFrame()).viewport.getAlignment();
581     }
582     return null;
583   }
584
585   /**
586    * Returns the title of the complementary frame to the one given.
587    */
588   @Override
589   public String getComplementTitle(Object alignFrame)
590   {
591     if (alignFrame == this.getTopFrame())
592     {
593       return ((AlignFrame) getBottomFrame()).getTitle();
594     }
595     else if (alignFrame == this.getBottomFrame())
596     {
597       return ((AlignFrame) getTopFrame()).getTitle();
598     }
599     return null;
600   }
601
602   /**
603    * Set the 'other half' to hidden / revealed.
604    */
605   @Override
606   public void setComplementVisible(Object alignFrame, boolean show)
607   {
608     /*
609      * Hiding the AlignPanel suppresses unnecessary repaints
610      */
611     if (alignFrame == getTopFrame())
612     {
613       ((AlignFrame) getBottomFrame()).alignPanel.setVisible(show);
614     }
615     else if (alignFrame == getBottomFrame())
616     {
617       ((AlignFrame) getTopFrame()).alignPanel.setVisible(show);
618     }
619     super.setComplementVisible(alignFrame, show);
620   }
621
622   /**
623    * Replace Cmd-F Find action with our version. This is necessary because the
624    * 'default' Finder searches in the first AlignFrame it finds. We need it to
625    * search in the half of the SplitFrame that has the mouse.
626    */
627   protected void overrideFind()
628   {
629     /*
630      * Ctrl-F / Cmd-F open Finder dialog, 'focused' on the right alignment
631      */
632     KeyStroke key_cmdF = KeyStroke.getKeyStroke(KeyEvent.VK_F, Toolkit
633             .getDefaultToolkit().getMenuShortcutKeyMask(), false);
634     AbstractAction action = new AbstractAction()
635     {
636       @Override
637       public void actionPerformed(ActionEvent e)
638       {
639         Component c = getFrameAtMouse();
640         if (c != null && c instanceof AlignFrame)
641         {
642           AlignFrame af = (AlignFrame) c;
643           new Finder(af.viewport, af.alignPanel);
644         }
645       }
646     };
647     overrideKeyBinding(key_cmdF, action);
648   }
649 }
650