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