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