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