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