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