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