JAL-2435 slightly more sensible behaviour changing fonts in split frame
[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, Desktop.instance.getHeight()
135             - 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       // if (protein.isProteinFontAsCdna())
192       // {
193         // TODO handle different fonts
194         int scale = protein.isScaleProteinAsCdna() ? 3 : 1;
195         protein.setCharWidth(scale * cdna.getViewStyle().getCharWidth());
196       // }
197     }
198   }
199
200   /**
201    * Adjust the divider for a sensible split of the real estate (for example,
202    * when many transcripts are shown with a single protein). This should only be
203    * called after the split pane has been laid out (made visible) so it has a
204    * height.
205    */
206   protected void adjustDivider()
207   {
208     final AlignViewport topViewport = ((AlignFrame) getTopFrame()).viewport;
209     final AlignViewport bottomViewport = ((AlignFrame) getBottomFrame()).viewport;
210     final AlignmentI topAlignment = topViewport.getAlignment();
211     final AlignmentI bottomAlignment = bottomViewport.getAlignment();
212     boolean topAnnotations = topViewport.isShowAnnotation();
213     boolean bottomAnnotations = bottomViewport.isShowAnnotation();
214     // TODO need number of visible sequences here, not #sequences - how?
215     int topCount = topAlignment.getHeight();
216     int bottomCount = bottomAlignment.getHeight();
217     int topCharHeight = topViewport.getViewStyle().getCharHeight();
218     int bottomCharHeight = bottomViewport.getViewStyle().getCharHeight();
219
220     /*
221      * estimate ratio of (topFrameContent / bottomFrameContent)
222      */
223     int insets = Platform.isAMac() ? MAC_INSETS_HEIGHT
224             : WINDOWS_INSETS_HEIGHT;
225     // allow 3 'rows' for scale, scrollbar, status bar
226     int topHeight = insets + (3 + topCount) * topCharHeight
227             + (topAnnotations ? topViewport.calcPanelHeight() : 0);
228     int bottomHeight = insets + (3 + bottomCount) * bottomCharHeight
229             + (bottomAnnotations ? bottomViewport.calcPanelHeight() : 0);
230     double ratio = ((double) topHeight) / (topHeight + bottomHeight);
231
232     /*
233      * limit to 0.2 <= ratio <= 0.8 to avoid concealing all sequences
234      */
235     ratio = Math.min(ratio, 0.8d);
236     ratio = Math.max(ratio, 0.2d);
237     setRelativeDividerLocation(ratio);
238   }
239
240   /**
241    * Add a listener to tidy up when the frame is closed.
242    */
243   protected void addCloseFrameListener()
244   {
245     addInternalFrameListener(new InternalFrameAdapter()
246     {
247       @Override
248       public void internalFrameClosed(InternalFrameEvent evt)
249       {
250         close();
251       };
252     });
253   }
254
255   /**
256    * Add a key listener that delegates to whichever split component the mouse is
257    * in (or does nothing if neither).
258    */
259   protected void addKeyListener()
260   {
261     addKeyListener(new KeyAdapter()
262     {
263
264       @Override
265       public void keyPressed(KeyEvent e)
266       {
267         AlignFrame af = (AlignFrame) getFrameAtMouse();
268
269         /*
270          * Intercept and override any keys here if wanted.
271          */
272         if (!overrideKey(e, af))
273         {
274           if (af != null)
275           {
276             for (KeyListener kl : af.getKeyListeners())
277             {
278               kl.keyPressed(e);
279             }
280           }
281         }
282       }
283
284       @Override
285       public void keyReleased(KeyEvent e)
286       {
287         Component c = getFrameAtMouse();
288         if (c != null)
289         {
290           for (KeyListener kl : c.getKeyListeners())
291           {
292             kl.keyReleased(e);
293           }
294         }
295       }
296
297     });
298   }
299
300   /**
301    * Returns true if the key event is overriden and actioned (or ignored) here,
302    * else returns false, indicating it should be delegated to the AlignFrame's
303    * usual handler.
304    * <p>
305    * We can't handle Cmd-Key combinations here, instead this is done by
306    * overriding key bindings.
307    * 
308    * @see addKeyOverrides
309    * @param e
310    * @param af
311    * @return
312    */
313   protected boolean overrideKey(KeyEvent e, AlignFrame af)
314   {
315     boolean actioned = false;
316     int keyCode = e.getKeyCode();
317     switch (keyCode)
318     {
319     case KeyEvent.VK_DOWN:
320       if (e.isAltDown() || !af.viewport.cursorMode)
321       {
322         /*
323          * Key down (or Alt-key-down in cursor mode) - move selected sequences
324          */
325         ((AlignFrame) getTopFrame()).moveSelectedSequences(false);
326         ((AlignFrame) getBottomFrame()).moveSelectedSequences(false);
327         actioned = true;
328         e.consume();
329       }
330       break;
331     case KeyEvent.VK_UP:
332       if (e.isAltDown() || !af.viewport.cursorMode)
333       {
334         /*
335          * Key up (or Alt-key-up in cursor mode) - move selected sequences
336          */
337         ((AlignFrame) getTopFrame()).moveSelectedSequences(true);
338         ((AlignFrame) getBottomFrame()).moveSelectedSequences(true);
339         actioned = true;
340         e.consume();
341       }
342       break;
343     default:
344     }
345     return actioned;
346   }
347
348   /**
349    * Set key bindings (recommended for Swing over key accelerators).
350    */
351   private void addKeyBindings()
352   {
353     overrideDelegatedKeyBindings();
354
355     overrideImplementedKeyBindings();
356   }
357
358   /**
359    * Override key bindings with alternative action methods implemented in this
360    * class.
361    */
362   protected void overrideImplementedKeyBindings()
363   {
364     overrideFind();
365     overrideNewView();
366     overrideCloseView();
367     overrideExpandViews();
368     overrideGatherViews();
369   }
370
371   /**
372    * Replace Cmd-W close view action with our version.
373    */
374   protected void overrideCloseView()
375   {
376     AbstractAction action;
377     /*
378      * Ctrl-W / Cmd-W - close view or window
379      */
380     KeyStroke key_cmdW = KeyStroke.getKeyStroke(KeyEvent.VK_W, Toolkit
381             .getDefaultToolkit().getMenuShortcutKeyMask(), false);
382     action = new AbstractAction()
383     {
384       @Override
385       public void actionPerformed(ActionEvent e)
386       {
387         closeView_actionPerformed();
388       }
389     };
390     overrideKeyBinding(key_cmdW, action);
391   }
392
393   /**
394    * Replace Cmd-T new view action with our version.
395    */
396   protected void overrideNewView()
397   {
398     /*
399      * Ctrl-T / Cmd-T open new view
400      */
401     KeyStroke key_cmdT = KeyStroke.getKeyStroke(KeyEvent.VK_T, Toolkit
402             .getDefaultToolkit().getMenuShortcutKeyMask(), false);
403     AbstractAction action = new AbstractAction()
404     {
405       @Override
406       public void actionPerformed(ActionEvent e)
407       {
408         newView_actionPerformed();
409       }
410     };
411     overrideKeyBinding(key_cmdT, action);
412   }
413
414   /**
415    * For now, delegates key events to the corresponding key accelerator for the
416    * AlignFrame that the mouse is in. Hopefully can be simplified in future if
417    * AlignFrame is changed to use key bindings rather than accelerators.
418    */
419   protected void overrideDelegatedKeyBindings()
420   {
421     if (getTopFrame() instanceof AlignFrame)
422     {
423       /*
424        * Get all accelerator keys in the top frame (the bottom should be
425        * identical) and override each one.
426        */
427       for (Entry<KeyStroke, JMenuItem> acc : ((AlignFrame) getTopFrame())
428               .getAccelerators().entrySet())
429       {
430         overrideKeyBinding(acc);
431       }
432     }
433   }
434
435   /**
436    * Overrides an AlignFrame key accelerator with our version which delegates to
437    * the action listener in whichever frame has the mouse (and does nothing if
438    * neither has).
439    * 
440    * @param acc
441    */
442   private void overrideKeyBinding(Entry<KeyStroke, JMenuItem> acc)
443   {
444     final KeyStroke ks = acc.getKey();
445     InputMap inputMap = this.getInputMap(JComponent.WHEN_FOCUSED);
446     inputMap.put(ks, ks);
447     this.getActionMap().put(ks, new AbstractAction()
448     {
449       @Override
450       public void actionPerformed(ActionEvent e)
451       {
452         Component c = getFrameAtMouse();
453         if (c != null && c instanceof AlignFrame)
454         {
455           for (ActionListener a : ((AlignFrame) c).getAccelerators()
456                   .get(ks).getActionListeners())
457           {
458             a.actionPerformed(null);
459           }
460         }
461       }
462     });
463   }
464
465   /**
466    * Replace an accelerator key's action with the specified action.
467    * 
468    * @param ks
469    */
470   protected void overrideKeyBinding(KeyStroke ks, AbstractAction action)
471   {
472     this.getActionMap().put(ks, action);
473     overrideMenuItem(ks, action);
474   }
475
476   /**
477    * Create and link new views (with matching names) in both panes.
478    * <p>
479    * Note this is _not_ multiple tabs, each hosting a split pane view, rather it
480    * is a single split pane with each split holding multiple tabs which are
481    * linked in pairs.
482    * <p>
483    * TODO implement instead with a tabbed holder in the SplitView, each tab
484    * holding a single JSplitPane. Would avoid a duplicated tab, at the cost of
485    * some additional coding.
486    */
487   protected void newView_actionPerformed()
488   {
489     AlignFrame topFrame = (AlignFrame) getTopFrame();
490     AlignFrame bottomFrame = (AlignFrame) getBottomFrame();
491     final boolean scaleProteinAsCdna = topFrame.viewport
492             .isScaleProteinAsCdna();
493
494     AlignmentPanel newTopPanel = topFrame.newView(null, true);
495     AlignmentPanel newBottomPanel = bottomFrame.newView(null, true);
496
497     /*
498      * This currently (for the first new view only) leaves the top pane on tab 0
499      * but the bottom on tab 1. This results from 'setInitialTabVisible' echoing
500      * from the bottom back to the first frame. Next line is a fudge to work
501      * around this. TODO find a better way.
502      */
503     if (topFrame.getTabIndex() != bottomFrame.getTabIndex())
504     {
505       topFrame.setDisplayedView(newTopPanel);
506     }
507
508     newBottomPanel.av.viewName = newTopPanel.av.viewName;
509     newTopPanel.av.setCodingComplement(newBottomPanel.av);
510
511     /*
512      * These lines can be removed once scaleProteinAsCdna is added to element
513      * Viewport in jalview.xsd, as Jalview2XML.copyAlignPanel will then take
514      * care of it
515      */
516     newTopPanel.av.setScaleProteinAsCdna(scaleProteinAsCdna);
517     newBottomPanel.av.setScaleProteinAsCdna(scaleProteinAsCdna);
518
519     /*
520      * Line up id labels etc
521      */
522     adjustLayout();
523
524     final StructureSelectionManager ssm = StructureSelectionManager
525             .getStructureSelectionManager(Desktop.instance);
526     ssm.addCommandListener(newTopPanel.av);
527     ssm.addCommandListener(newBottomPanel.av);
528   }
529
530   /**
531    * Close the currently selected view in both panes. If there is only one view,
532    * close this split frame.
533    */
534   protected void closeView_actionPerformed()
535   {
536     int viewCount = ((AlignFrame) getTopFrame()).getAlignPanels().size();
537     if (viewCount < 2)
538     {
539       close();
540       return;
541     }
542
543     AlignmentPanel topPanel = ((AlignFrame) getTopFrame()).alignPanel;
544     AlignmentPanel bottomPanel = ((AlignFrame) getBottomFrame()).alignPanel;
545
546     ((AlignFrame) getTopFrame()).closeView(topPanel);
547     ((AlignFrame) getBottomFrame()).closeView(bottomPanel);
548
549   }
550
551   /**
552    * Close child frames and this split frame.
553    */
554   public void close()
555   {
556     ((AlignFrame) getTopFrame()).closeMenuItem_actionPerformed(true);
557     ((AlignFrame) getBottomFrame()).closeMenuItem_actionPerformed(true);
558     try
559     {
560       this.setClosed(true);
561     } catch (PropertyVetoException e)
562     {
563       // ignore
564     }
565   }
566
567   /**
568    * Replace AlignFrame 'expand views' action with SplitFrame version.
569    */
570   protected void overrideExpandViews()
571   {
572     KeyStroke key_X = KeyStroke.getKeyStroke(KeyEvent.VK_X, 0, false);
573     AbstractAction action = new AbstractAction()
574     {
575       @Override
576       public void actionPerformed(ActionEvent e)
577       {
578         expandViews_actionPerformed();
579       }
580     };
581     overrideMenuItem(key_X, action);
582   }
583
584   /**
585    * Replace AlignFrame 'gather views' action with SplitFrame version.
586    */
587   protected void overrideGatherViews()
588   {
589     KeyStroke key_G = KeyStroke.getKeyStroke(KeyEvent.VK_G, 0, false);
590     AbstractAction action = new AbstractAction()
591     {
592       @Override
593       public void actionPerformed(ActionEvent e)
594       {
595         gatherViews_actionPerformed();
596       }
597     };
598     overrideMenuItem(key_G, action);
599   }
600
601   /**
602    * Override the menu action associated with the keystroke in the child frames,
603    * replacing it with the given action.
604    * 
605    * @param ks
606    * @param action
607    */
608   private void overrideMenuItem(KeyStroke ks, AbstractAction action)
609   {
610     overrideMenuItem(ks, action, getTopFrame());
611     overrideMenuItem(ks, action, getBottomFrame());
612   }
613
614   /**
615    * Override the menu action associated with the keystroke in one child frame,
616    * replacing it with the given action. Mwahahahaha.
617    * 
618    * @param key
619    * @param action
620    * @param comp
621    */
622   private void overrideMenuItem(KeyStroke key, final AbstractAction action,
623           JComponent comp)
624   {
625     if (comp instanceof AlignFrame)
626     {
627       JMenuItem mi = ((AlignFrame) comp).getAccelerators().get(key);
628       if (mi != null)
629       {
630         for (ActionListener al : mi.getActionListeners())
631         {
632           mi.removeActionListener(al);
633         }
634         mi.addActionListener(new ActionListener()
635         {
636           @Override
637           public void actionPerformed(ActionEvent e)
638           {
639             action.actionPerformed(e);
640           }
641         });
642       }
643     }
644   }
645
646   /**
647    * Expand any multiple views (which are always in pairs) into separate split
648    * frames.
649    */
650   protected void expandViews_actionPerformed()
651   {
652     Desktop.instance.explodeViews(this);
653   }
654
655   /**
656    * Gather any other SplitFrame views of this alignment back in as multiple
657    * (pairs of) views in this SplitFrame.
658    */
659   protected void gatherViews_actionPerformed()
660   {
661     Desktop.instance.gatherViews(this);
662   }
663
664   /**
665    * Returns the alignment in the complementary frame to the one given.
666    */
667   @Override
668   public AlignmentI getComplement(Object alignFrame)
669   {
670     if (alignFrame == this.getTopFrame())
671     {
672       return ((AlignFrame) getBottomFrame()).viewport.getAlignment();
673     }
674     else if (alignFrame == this.getBottomFrame())
675     {
676       return ((AlignFrame) getTopFrame()).viewport.getAlignment();
677     }
678     return null;
679   }
680
681   /**
682    * Returns the title of the complementary frame to the one given.
683    */
684   @Override
685   public String getComplementTitle(Object alignFrame)
686   {
687     if (alignFrame == this.getTopFrame())
688     {
689       return ((AlignFrame) getBottomFrame()).getTitle();
690     }
691     else if (alignFrame == this.getBottomFrame())
692     {
693       return ((AlignFrame) getTopFrame()).getTitle();
694     }
695     return null;
696   }
697
698   /**
699    * Set the 'other half' to hidden / revealed.
700    */
701   @Override
702   public void setComplementVisible(Object alignFrame, boolean show)
703   {
704     /*
705      * Hiding the AlignPanel suppresses unnecessary repaints
706      */
707     if (alignFrame == getTopFrame())
708     {
709       ((AlignFrame) getBottomFrame()).alignPanel.setVisible(show);
710     }
711     else if (alignFrame == getBottomFrame())
712     {
713       ((AlignFrame) getTopFrame()).alignPanel.setVisible(show);
714     }
715     super.setComplementVisible(alignFrame, show);
716   }
717
718   /**
719    * return the AlignFrames held by this container
720    * 
721    * @return { Top alignFrame (Usually CDS), Bottom AlignFrame (Usually
722    *         Protein)}
723    */
724   public List<AlignFrame> getAlignFrames()
725   {
726     return Arrays.asList(new AlignFrame[] { (AlignFrame) getTopFrame(),
727         (AlignFrame) getBottomFrame() });
728   }
729
730   /**
731    * Replace Cmd-F Find action with our version. This is necessary because the
732    * 'default' Finder searches in the first AlignFrame it finds. We need it to
733    * search in the half of the SplitFrame that has the mouse.
734    */
735   protected void overrideFind()
736   {
737     /*
738      * Ctrl-F / Cmd-F open Finder dialog, 'focused' on the right alignment
739      */
740     KeyStroke key_cmdF = KeyStroke.getKeyStroke(KeyEvent.VK_F, Toolkit
741             .getDefaultToolkit().getMenuShortcutKeyMask(), false);
742     AbstractAction action = new AbstractAction()
743     {
744       @Override
745       public void actionPerformed(ActionEvent e)
746       {
747         Component c = getFrameAtMouse();
748         if (c != null && c instanceof AlignFrame)
749         {
750           AlignFrame af = (AlignFrame) c;
751           new Finder(af.viewport, af.alignPanel);
752         }
753       }
754     };
755     overrideKeyBinding(key_cmdF, action);
756   }
757 }