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