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