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