aa658ce2df592e2b588f65a0b477a1bac57e325d
[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     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     // about 65 pixels for Desktop decorators on Windows
69     height = Math.min(height, Desktop.instance.getHeight() - 65);
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         close();
148       };
149     });
150   }
151
152   /**
153    * Add a key listener that delegates to whichever split component the mouse is
154    * in (or does nothing if neither).
155    */
156   protected void addKeyListener()
157   {
158     addKeyListener(new KeyAdapter() {
159
160       @Override
161       public void keyPressed(KeyEvent e)
162       {
163         AlignFrame af = (AlignFrame) getFrameAtMouse();
164
165         /*
166          * Intercept and override any keys here if wanted.
167          */
168         if (!overrideKey(e, af))
169         {
170           if (af != null)
171           {
172             for (KeyListener kl : af.getKeyListeners())
173             {
174               kl.keyPressed(e);
175             }
176           }
177         }
178       }
179
180       @Override
181       public void keyReleased(KeyEvent e)
182       {
183         Component c = getFrameAtMouse();
184         if (c != null)
185         {
186           for (KeyListener kl : c.getKeyListeners())
187           {
188             kl.keyReleased(e);
189           }
190         }
191       }
192       
193     });
194   }
195
196   /**
197    * Returns true if the key event is overriden and actioned (or ignored) here,
198    * else returns false, indicating it should be delegated to the AlignFrame's
199    * usual handler.
200    * <p>
201    * We can't handle Cmd-Key combinations here, instead this is done by
202    * overriding key bindings.
203    * 
204    * @see addKeyOverrides
205    * @param e
206    * @param af
207    * @return
208    */
209   protected boolean overrideKey(KeyEvent e, AlignFrame af)
210   {
211     boolean actioned = false;
212     int keyCode = e.getKeyCode();
213     switch (keyCode)
214     {
215     case KeyEvent.VK_DOWN:
216       if (e.isAltDown() || !af.viewport.cursorMode)
217       {
218         /*
219          * Key down (or Alt-key-down in cursor mode) - move selected sequences
220          */
221         ((AlignFrame) getTopFrame()).moveSelectedSequences(false);
222         ((AlignFrame) getBottomFrame()).moveSelectedSequences(false);
223         actioned = true;
224         e.consume();
225       }
226       break;
227     case KeyEvent.VK_UP:
228       if (e.isAltDown() || !af.viewport.cursorMode)
229       {
230         /*
231          * Key up (or Alt-key-up in cursor mode) - move selected sequences
232          */
233         ((AlignFrame) getTopFrame()).moveSelectedSequences(true);
234         ((AlignFrame) getBottomFrame()).moveSelectedSequences(true);
235         actioned = true;
236         e.consume();
237       }
238     default:
239     }
240     return actioned;
241   }
242
243   /**
244    * Set key bindings (recommended for Swing over key accelerators).
245    */
246   private void addKeyBindings()
247   {
248     overrideDelegatedKeyBindings();
249
250     overrideImplementedKeyBindings();
251   }
252
253   /**
254    * Override key bindings with alternative action methods implemented in this
255    * class.
256    */
257   protected void overrideImplementedKeyBindings()
258   {
259     overrideFind();
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     final boolean scaleProteinAsCdna = topFrame.viewport
387             .isScaleProteinAsCdna();
388
389     AlignmentPanel newTopPanel = topFrame.newView(null, true);
390     AlignmentPanel newBottomPanel = bottomFrame.newView(null, true);
391
392     /*
393      * This currently (for the first new view only) leaves the top pane on tab 0
394      * but the bottom on tab 1. This results from 'setInitialTabVisible' echoing
395      * from the bottom back to the first frame. Next line is a fudge to work
396      * around this. TODO find a better way.
397      */
398     if (topFrame.getTabIndex() != bottomFrame.getTabIndex())
399     {
400       topFrame.setDisplayedView(newTopPanel);
401     }
402
403     newBottomPanel.av.viewName = newTopPanel.av.viewName;
404     newTopPanel.av.setCodingComplement(newBottomPanel.av);
405
406     /*
407      * These lines can be removed once scaleProteinAsCdna is added to element
408      * Viewport in jalview.xsd, as Jalview2XML.copyAlignPanel will then take
409      * care of it
410      */
411     newTopPanel.av.setScaleProteinAsCdna(scaleProteinAsCdna);
412     newBottomPanel.av.setScaleProteinAsCdna(scaleProteinAsCdna);
413
414     /*
415      * Line up id labels etc
416      */
417     adjustLayout();
418
419     final StructureSelectionManager ssm = StructureSelectionManager
420             .getStructureSelectionManager(Desktop.instance);
421     ssm.addCommandListener(newTopPanel.av);
422     ssm.addCommandListener(newBottomPanel.av);
423   }
424
425   /**
426    * Close the currently selected view in both panes. If there is only one view,
427    * close this split frame.
428    */
429   protected void closeView_actionPerformed()
430   {
431     int viewCount = ((AlignFrame) getTopFrame()).getAlignPanels().size();
432     if (viewCount < 2)
433     {
434       close();
435       return;
436     }
437
438     AlignmentPanel topPanel = ((AlignFrame) getTopFrame()).alignPanel;
439     AlignmentPanel bottomPanel = ((AlignFrame) getBottomFrame()).alignPanel;
440
441     ((AlignFrame) getTopFrame()).closeView(topPanel);
442     ((AlignFrame) getBottomFrame()).closeView(bottomPanel);
443
444   }
445
446   /**
447    * Close child frames and this split frame.
448    */
449   public void close()
450   {
451     ((AlignFrame) getTopFrame()).closeMenuItem_actionPerformed(true);
452     ((AlignFrame) getBottomFrame()).closeMenuItem_actionPerformed(true);
453     try
454     {
455       this.setClosed(true);
456     } catch (PropertyVetoException e)
457     {
458       // ignore
459     }
460   }
461
462   /**
463    * Replace AlignFrame 'expand views' action with SplitFrame version.
464    */
465   protected void overrideExpandViews()
466   {
467     KeyStroke key_X = KeyStroke.getKeyStroke(KeyEvent.VK_X, 0, false);
468     AbstractAction action = new AbstractAction()
469     {
470       @Override
471       public void actionPerformed(ActionEvent e)
472       {
473         expandViews_actionPerformed();
474       }
475     };
476     overrideMenuItem(key_X, action);
477   }
478
479   /**
480    * Replace AlignFrame 'gather views' action with SplitFrame version.
481    */
482   protected void overrideGatherViews()
483   {
484     KeyStroke key_G = KeyStroke.getKeyStroke(KeyEvent.VK_G, 0, false);
485     AbstractAction action = new AbstractAction()
486     {
487       @Override
488       public void actionPerformed(ActionEvent e)
489       {
490         gatherViews_actionPerformed();
491       }
492     };
493     overrideMenuItem(key_G, action);
494   }
495
496   /**
497    * Override the menu action associated with the keystroke in the child frames,
498    * replacing it with the given action.
499    * 
500    * @param ks
501    * @param action
502    */
503   private void overrideMenuItem(KeyStroke ks, AbstractAction action)
504   {
505     overrideMenuItem(ks, action, getTopFrame());
506     overrideMenuItem(ks, action, getBottomFrame());
507   }
508
509   /**
510    * Override the menu action associated with the keystroke in one child frame,
511    * replacing it with the given action. Mwahahahaha.
512    * 
513    * @param key
514    * @param action
515    * @param comp
516    */
517   private void overrideMenuItem(KeyStroke key, final AbstractAction action,
518           JComponent comp)
519   {
520     if (comp instanceof AlignFrame)
521     {
522       JMenuItem mi = ((AlignFrame) comp).getAccelerators().get(key);
523       if (mi != null)
524       {
525         for (ActionListener al : mi.getActionListeners())
526         {
527           mi.removeActionListener(al);
528         }
529         mi.addActionListener(new ActionListener()
530         {
531           @Override
532           public void actionPerformed(ActionEvent e)
533           {
534             action.actionPerformed(e);
535           }
536         });
537       }
538     }
539   }
540
541   /**
542    * Expand any multiple views (which are always in pairs) into separate split
543    * frames.
544    */
545   protected void expandViews_actionPerformed()
546   {
547     Desktop.instance.explodeViews(this);
548   }
549
550   /**
551    * Gather any other SplitFrame views of this alignment back in as multiple
552    * (pairs of) views in this SplitFrame.
553    */
554   protected void gatherViews_actionPerformed()
555   {
556     Desktop.instance.gatherViews(this);
557   }
558
559   /**
560    * Returns the alignment in the complementary frame to the one given.
561    */
562   @Override
563   public AlignmentI getComplement(Object alignFrame)
564   {
565     if (alignFrame == this.getTopFrame())
566     {
567       return ((AlignFrame) getBottomFrame()).viewport.getAlignment();
568     }
569     else if (alignFrame == this.getBottomFrame())
570     {
571       return ((AlignFrame) getTopFrame()).viewport.getAlignment();
572     }
573     return null;
574   }
575
576   /**
577    * Returns the title of the complementary frame to the one given.
578    */
579   @Override
580   public String getComplementTitle(Object alignFrame)
581   {
582     if (alignFrame == this.getTopFrame())
583     {
584       return ((AlignFrame) getBottomFrame()).getTitle();
585     }
586     else if (alignFrame == this.getBottomFrame())
587     {
588       return ((AlignFrame) getTopFrame()).getTitle();
589     }
590     return null;
591   }
592
593   /**
594    * Set the 'other half' to hidden / revealed.
595    */
596   @Override
597   public void setComplementVisible(Object alignFrame, boolean show)
598   {
599     /*
600      * Hiding the AlignPanel suppresses unnecessary repaints
601      */
602     if (alignFrame == getTopFrame())
603     {
604       ((AlignFrame) getBottomFrame()).alignPanel.setVisible(show);
605     }
606     else if (alignFrame == getBottomFrame())
607     {
608       ((AlignFrame) getTopFrame()).alignPanel.setVisible(show);
609     }
610     super.setComplementVisible(alignFrame, show);
611   }
612
613   /**
614    * Replace Cmd-F Find action with our version. This is necessary because the
615    * 'default' Finder searches in the first AlignFrame it finds. We need it to
616    * search in the half of the SplitFrame that has the mouse.
617    */
618   protected void overrideFind()
619   {
620     /*
621      * Ctrl-F / Cmd-F open Finder dialog, 'focused' on the right alignment
622      */
623     KeyStroke key_cmdF = KeyStroke.getKeyStroke(KeyEvent.VK_F, Toolkit
624             .getDefaultToolkit().getMenuShortcutKeyMask(), false);
625     AbstractAction action = new AbstractAction()
626     {
627       @Override
628       public void actionPerformed(ActionEvent e)
629       {
630         Component c = getFrameAtMouse();
631         if (c != null && c instanceof AlignFrame)
632         {
633           AlignFrame af = (AlignFrame) c;
634           new Finder(af.viewport, af.alignPanel);
635         }
636       }
637     };
638     overrideKeyBinding(key_cmdF, action);
639   }
640 }
641