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