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