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