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