JAL-1645 source formatting and organise imports
[jalview.git] / src / jalview / gui / SplitFrame.java
1 package jalview.gui;
2
3 import jalview.api.SplitContainerI;
4 import jalview.api.ViewStyleI;
5 import jalview.datamodel.AlignmentI;
6 import jalview.jbgui.GAlignFrame;
7 import jalview.jbgui.GSplitFrame;
8 import jalview.structure.StructureSelectionManager;
9 import jalview.viewmodel.AlignmentViewport;
10
11 import java.awt.Component;
12 import java.awt.Toolkit;
13 import java.awt.event.ActionEvent;
14 import java.awt.event.ActionListener;
15 import java.awt.event.KeyAdapter;
16 import java.awt.event.KeyEvent;
17 import java.awt.event.KeyListener;
18 import java.beans.PropertyVetoException;
19 import java.util.Map.Entry;
20
21 import javax.swing.AbstractAction;
22 import javax.swing.InputMap;
23 import javax.swing.JComponent;
24 import javax.swing.JMenuItem;
25 import javax.swing.KeyStroke;
26 import javax.swing.event.InternalFrameAdapter;
27 import javax.swing.event.InternalFrameEvent;
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     // about 65 pixels for Desktop decorators on Windows
69     height = Math.min(height, Desktop.instance.getHeight() - 65);
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         close();
148       };
149     });
150   }
151
152   /**
153    * Add a key listener that delegates to whichever split component the mouse is
154    * in (or does nothing if neither).
155    */
156   protected void addKeyListener()
157   {
158     addKeyListener(new KeyAdapter()
159     {
160
161       @Override
162       public void keyPressed(KeyEvent e)
163       {
164         AlignFrame af = (AlignFrame) getFrameAtMouse();
165
166         /*
167          * Intercept and override any keys here if wanted.
168          */
169         if (!overrideKey(e, af))
170         {
171           if (af != null)
172           {
173             for (KeyListener kl : af.getKeyListeners())
174             {
175               kl.keyPressed(e);
176             }
177           }
178         }
179       }
180
181       @Override
182       public void keyReleased(KeyEvent e)
183       {
184         Component c = getFrameAtMouse();
185         if (c != null)
186         {
187           for (KeyListener kl : c.getKeyListeners())
188           {
189             kl.keyReleased(e);
190           }
191         }
192       }
193
194     });
195   }
196
197   /**
198    * Returns true if the key event is overriden and actioned (or ignored) here,
199    * else returns false, indicating it should be delegated to the AlignFrame's
200    * usual handler.
201    * <p>
202    * We can't handle Cmd-Key combinations here, instead this is done by
203    * overriding key bindings.
204    * 
205    * @see addKeyOverrides
206    * @param e
207    * @param af
208    * @return
209    */
210   protected boolean overrideKey(KeyEvent e, AlignFrame af)
211   {
212     boolean actioned = false;
213     int keyCode = e.getKeyCode();
214     switch (keyCode)
215     {
216     case KeyEvent.VK_DOWN:
217       if (e.isAltDown() || !af.viewport.cursorMode)
218       {
219         /*
220          * Key down (or Alt-key-down in cursor mode) - move selected sequences
221          */
222         ((AlignFrame) getTopFrame()).moveSelectedSequences(false);
223         ((AlignFrame) getBottomFrame()).moveSelectedSequences(false);
224         actioned = true;
225         e.consume();
226       }
227       break;
228     case KeyEvent.VK_UP:
229       if (e.isAltDown() || !af.viewport.cursorMode)
230       {
231         /*
232          * Key up (or Alt-key-up in cursor mode) - move selected sequences
233          */
234         ((AlignFrame) getTopFrame()).moveSelectedSequences(true);
235         ((AlignFrame) getBottomFrame()).moveSelectedSequences(true);
236         actioned = true;
237         e.consume();
238       }
239     default:
240     }
241     return actioned;
242   }
243
244   /**
245    * Set key bindings (recommended for Swing over key accelerators).
246    */
247   private void addKeyBindings()
248   {
249     overrideDelegatedKeyBindings();
250
251     overrideImplementedKeyBindings();
252   }
253
254   /**
255    * Override key bindings with alternative action methods implemented in this
256    * class.
257    */
258   protected void overrideImplementedKeyBindings()
259   {
260     overrideFind();
261     overrideNewView();
262     overrideCloseView();
263     overrideExpandViews();
264     overrideGatherViews();
265   }
266
267   /**
268    * Replace Cmd-W close view action with our version.
269    */
270   protected void overrideCloseView()
271   {
272     AbstractAction action;
273     /*
274      * Ctrl-W / Cmd-W - close view or window
275      */
276     KeyStroke key_cmdW = KeyStroke.getKeyStroke(KeyEvent.VK_W, Toolkit
277             .getDefaultToolkit().getMenuShortcutKeyMask(), false);
278     action = new AbstractAction()
279     {
280       @Override
281       public void actionPerformed(ActionEvent e)
282       {
283         closeView_actionPerformed();
284       }
285     };
286     overrideKeyBinding(key_cmdW, action);
287   }
288
289   /**
290    * Replace Cmd-T new view action with our version.
291    */
292   protected void overrideNewView()
293   {
294     /*
295      * Ctrl-T / Cmd-T open new view
296      */
297     KeyStroke key_cmdT = KeyStroke.getKeyStroke(KeyEvent.VK_T, Toolkit
298             .getDefaultToolkit().getMenuShortcutKeyMask(), false);
299     AbstractAction action = new AbstractAction()
300     {
301       @Override
302       public void actionPerformed(ActionEvent e)
303       {
304         newView_actionPerformed();
305       }
306     };
307     overrideKeyBinding(key_cmdT, action);
308   }
309
310   /**
311    * For now, delegates key events to the corresponding key accelerator for the
312    * AlignFrame that the mouse is in. Hopefully can be simplified in future if
313    * AlignFrame is changed to use key bindings rather than accelerators.
314    */
315   protected void overrideDelegatedKeyBindings()
316   {
317     if (getTopFrame() instanceof AlignFrame)
318     {
319       /*
320        * Get all accelerator keys in the top frame (the bottom should be
321        * identical) and override each one.
322        */
323       for (Entry<KeyStroke, JMenuItem> acc : ((AlignFrame) getTopFrame())
324               .getAccelerators().entrySet())
325       {
326         overrideKeyBinding(acc);
327       }
328     }
329   }
330
331   /**
332    * Overrides an AlignFrame key accelerator with our version which delegates to
333    * the action listener in whichever frame has the mouse (and does nothing if
334    * neither has).
335    * 
336    * @param acc
337    */
338   private void overrideKeyBinding(Entry<KeyStroke, JMenuItem> acc)
339   {
340     final KeyStroke ks = acc.getKey();
341     InputMap inputMap = this.getInputMap(JComponent.WHEN_FOCUSED);
342     inputMap.put(ks, ks);
343     this.getActionMap().put(ks, new AbstractAction()
344     {
345       @Override
346       public void actionPerformed(ActionEvent e)
347       {
348         Component c = getFrameAtMouse();
349         if (c != null && c instanceof AlignFrame)
350         {
351           for (ActionListener a : ((AlignFrame) c).getAccelerators()
352                   .get(ks).getActionListeners())
353           {
354             a.actionPerformed(null);
355           }
356         }
357       }
358     });
359   }
360
361   /**
362    * Replace an accelerator key's action with the specified action.
363    * 
364    * @param ks
365    */
366   protected void overrideKeyBinding(KeyStroke ks, AbstractAction action)
367   {
368     this.getActionMap().put(ks, action);
369     overrideMenuItem(ks, action);
370   }
371
372   /**
373    * Create and link new views (with matching names) in both panes.
374    * <p>
375    * Note this is _not_ multiple tabs, each hosting a split pane view, rather it
376    * is a single split pane with each split holding multiple tabs which are
377    * linked in pairs.
378    * <p>
379    * TODO implement instead with a tabbed holder in the SplitView, each tab
380    * holding a single JSplitPane. Would avoid a duplicated tab, at the cost of
381    * some additional coding.
382    */
383   protected void newView_actionPerformed()
384   {
385     AlignFrame topFrame = (AlignFrame) getTopFrame();
386     AlignFrame bottomFrame = (AlignFrame) getBottomFrame();
387     final boolean scaleProteinAsCdna = topFrame.viewport
388             .isScaleProteinAsCdna();
389
390     AlignmentPanel newTopPanel = topFrame.newView(null, true);
391     AlignmentPanel newBottomPanel = bottomFrame.newView(null, true);
392
393     /*
394      * This currently (for the first new view only) leaves the top pane on tab 0
395      * but the bottom on tab 1. This results from 'setInitialTabVisible' echoing
396      * from the bottom back to the first frame. Next line is a fudge to work
397      * around this. TODO find a better way.
398      */
399     if (topFrame.getTabIndex() != bottomFrame.getTabIndex())
400     {
401       topFrame.setDisplayedView(newTopPanel);
402     }
403
404     newBottomPanel.av.viewName = newTopPanel.av.viewName;
405     newTopPanel.av.setCodingComplement(newBottomPanel.av);
406
407     /*
408      * These lines can be removed once scaleProteinAsCdna is added to element
409      * Viewport in jalview.xsd, as Jalview2XML.copyAlignPanel will then take
410      * care of it
411      */
412     newTopPanel.av.setScaleProteinAsCdna(scaleProteinAsCdna);
413     newBottomPanel.av.setScaleProteinAsCdna(scaleProteinAsCdna);
414
415     /*
416      * Line up id labels etc
417      */
418     adjustLayout();
419
420     final StructureSelectionManager ssm = StructureSelectionManager
421             .getStructureSelectionManager(Desktop.instance);
422     ssm.addCommandListener(newTopPanel.av);
423     ssm.addCommandListener(newBottomPanel.av);
424   }
425
426   /**
427    * Close the currently selected view in both panes. If there is only one view,
428    * close this split frame.
429    */
430   protected void closeView_actionPerformed()
431   {
432     int viewCount = ((AlignFrame) getTopFrame()).getAlignPanels().size();
433     if (viewCount < 2)
434     {
435       close();
436       return;
437     }
438
439     AlignmentPanel topPanel = ((AlignFrame) getTopFrame()).alignPanel;
440     AlignmentPanel bottomPanel = ((AlignFrame) getBottomFrame()).alignPanel;
441
442     ((AlignFrame) getTopFrame()).closeView(topPanel);
443     ((AlignFrame) getBottomFrame()).closeView(bottomPanel);
444
445   }
446
447   /**
448    * Close child frames and this split frame.
449    */
450   public void close()
451   {
452     ((AlignFrame) getTopFrame()).closeMenuItem_actionPerformed(true);
453     ((AlignFrame) getBottomFrame()).closeMenuItem_actionPerformed(true);
454     try
455     {
456       this.setClosed(true);
457     } catch (PropertyVetoException e)
458     {
459       // ignore
460     }
461   }
462
463   /**
464    * Replace AlignFrame 'expand views' action with SplitFrame version.
465    */
466   protected void overrideExpandViews()
467   {
468     KeyStroke key_X = KeyStroke.getKeyStroke(KeyEvent.VK_X, 0, false);
469     AbstractAction action = new AbstractAction()
470     {
471       @Override
472       public void actionPerformed(ActionEvent e)
473       {
474         expandViews_actionPerformed();
475       }
476     };
477     overrideMenuItem(key_X, action);
478   }
479
480   /**
481    * Replace AlignFrame 'gather views' action with SplitFrame version.
482    */
483   protected void overrideGatherViews()
484   {
485     KeyStroke key_G = KeyStroke.getKeyStroke(KeyEvent.VK_G, 0, false);
486     AbstractAction action = new AbstractAction()
487     {
488       @Override
489       public void actionPerformed(ActionEvent e)
490       {
491         gatherViews_actionPerformed();
492       }
493     };
494     overrideMenuItem(key_G, action);
495   }
496
497   /**
498    * Override the menu action associated with the keystroke in the child frames,
499    * replacing it with the given action.
500    * 
501    * @param ks
502    * @param action
503    */
504   private void overrideMenuItem(KeyStroke ks, AbstractAction action)
505   {
506     overrideMenuItem(ks, action, getTopFrame());
507     overrideMenuItem(ks, action, getBottomFrame());
508   }
509
510   /**
511    * Override the menu action associated with the keystroke in one child frame,
512    * replacing it with the given action. Mwahahahaha.
513    * 
514    * @param key
515    * @param action
516    * @param comp
517    */
518   private void overrideMenuItem(KeyStroke key, final AbstractAction action,
519           JComponent comp)
520   {
521     if (comp instanceof AlignFrame)
522     {
523       JMenuItem mi = ((AlignFrame) comp).getAccelerators().get(key);
524       if (mi != null)
525       {
526         for (ActionListener al : mi.getActionListeners())
527         {
528           mi.removeActionListener(al);
529         }
530         mi.addActionListener(new ActionListener()
531         {
532           @Override
533           public void actionPerformed(ActionEvent e)
534           {
535             action.actionPerformed(e);
536           }
537         });
538       }
539     }
540   }
541
542   /**
543    * Expand any multiple views (which are always in pairs) into separate split
544    * frames.
545    */
546   protected void expandViews_actionPerformed()
547   {
548     Desktop.instance.explodeViews(this);
549   }
550
551   /**
552    * Gather any other SplitFrame views of this alignment back in as multiple
553    * (pairs of) views in this SplitFrame.
554    */
555   protected void gatherViews_actionPerformed()
556   {
557     Desktop.instance.gatherViews(this);
558   }
559
560   /**
561    * Returns the alignment in the complementary frame to the one given.
562    */
563   @Override
564   public AlignmentI getComplement(Object alignFrame)
565   {
566     if (alignFrame == this.getTopFrame())
567     {
568       return ((AlignFrame) getBottomFrame()).viewport.getAlignment();
569     }
570     else if (alignFrame == this.getBottomFrame())
571     {
572       return ((AlignFrame) getTopFrame()).viewport.getAlignment();
573     }
574     return null;
575   }
576
577   /**
578    * Returns the title of the complementary frame to the one given.
579    */
580   @Override
581   public String getComplementTitle(Object alignFrame)
582   {
583     if (alignFrame == this.getTopFrame())
584     {
585       return ((AlignFrame) getBottomFrame()).getTitle();
586     }
587     else if (alignFrame == this.getBottomFrame())
588     {
589       return ((AlignFrame) getTopFrame()).getTitle();
590     }
591     return null;
592   }
593
594   /**
595    * Set the 'other half' to hidden / revealed.
596    */
597   @Override
598   public void setComplementVisible(Object alignFrame, boolean show)
599   {
600     /*
601      * Hiding the AlignPanel suppresses unnecessary repaints
602      */
603     if (alignFrame == getTopFrame())
604     {
605       ((AlignFrame) getBottomFrame()).alignPanel.setVisible(show);
606     }
607     else if (alignFrame == getBottomFrame())
608     {
609       ((AlignFrame) getTopFrame()).alignPanel.setVisible(show);
610     }
611     super.setComplementVisible(alignFrame, show);
612   }
613
614   /**
615    * Replace Cmd-F Find action with our version. This is necessary because the
616    * 'default' Finder searches in the first AlignFrame it finds. We need it to
617    * search in the half of the SplitFrame that has the mouse.
618    */
619   protected void overrideFind()
620   {
621     /*
622      * Ctrl-F / Cmd-F open Finder dialog, 'focused' on the right alignment
623      */
624     KeyStroke key_cmdF = KeyStroke.getKeyStroke(KeyEvent.VK_F, Toolkit
625             .getDefaultToolkit().getMenuShortcutKeyMask(), false);
626     AbstractAction action = new AbstractAction()
627     {
628       @Override
629       public void actionPerformed(ActionEvent e)
630       {
631         Component c = getFrameAtMouse();
632         if (c != null && c instanceof AlignFrame)
633         {
634           AlignFrame af = (AlignFrame) c;
635           new Finder(af.viewport, af.alignPanel);
636         }
637       }
638     };
639     overrideKeyBinding(key_cmdF, action);
640   }
641 }