JAL-2023 suppress unwanted debug output
[jalview.git] / src / jalview / gui / SplitFrame.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer (Version 2.9.0b2)
3  * Copyright (C) 2015 The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.gui;
22
23 import jalview.api.SplitContainerI;
24 import jalview.api.ViewStyleI;
25 import jalview.datamodel.AlignmentI;
26 import jalview.jbgui.GAlignFrame;
27 import jalview.jbgui.GSplitFrame;
28 import jalview.structure.StructureSelectionManager;
29 import jalview.util.Platform;
30 import jalview.viewmodel.AlignmentViewport;
31
32 import java.awt.Component;
33 import java.awt.Toolkit;
34 import java.awt.event.ActionEvent;
35 import java.awt.event.ActionListener;
36 import java.awt.event.KeyAdapter;
37 import java.awt.event.KeyEvent;
38 import java.awt.event.KeyListener;
39 import java.beans.PropertyVetoException;
40 import java.util.Map.Entry;
41
42 import javax.swing.AbstractAction;
43 import javax.swing.InputMap;
44 import javax.swing.JComponent;
45 import javax.swing.JMenuItem;
46 import javax.swing.KeyStroke;
47 import javax.swing.event.InternalFrameAdapter;
48 import javax.swing.event.InternalFrameEvent;
49
50 /**
51  * An internal frame on the desktop that hosts a horizontally split view of
52  * linked DNA and Protein alignments. Additional views can be created in linked
53  * pairs, expanded to separate split frames, or regathered into a single frame.
54  * <p>
55  * (Some) operations on each alignment are automatically mirrored on the other.
56  * These include mouseover (highlighting), sequence and column selection,
57  * sequence ordering and sorting, and grouping, colouring and sorting by tree.
58  * 
59  * @author gmcarstairs
60  *
61  */
62 public class SplitFrame extends GSplitFrame implements SplitContainerI
63 {
64   private static final long serialVersionUID = 1L;
65
66   public SplitFrame(GAlignFrame top, GAlignFrame bottom)
67   {
68     super(top, bottom);
69     init();
70   }
71
72   /**
73    * Initialise this frame.
74    */
75   protected void init()
76   {
77     getTopFrame().setSplitFrame(this);
78     getBottomFrame().setSplitFrame(this);
79     getTopFrame().setVisible(true);
80     getBottomFrame().setVisible(true);
81
82     ((AlignFrame) getTopFrame()).getViewport().setCodingComplement(
83             ((AlignFrame) getBottomFrame()).getViewport());
84
85     /*
86      * estimate width and height of SplitFrame; this.getInsets() doesn't seem to
87      * give the full additional size (a few pixels short)
88      */
89     // UIDefaults defaults = UIManager.getDefaults();
90     // Set<Object> keySet = defaults.keySet();
91     // for (Object key : keySet)
92     // {
93     // System.out.println(key.toString() + " = "
94     // + UIManager.get(key).toString());
95     // }
96     int widthFudge = Platform.isAMac() ? 28 : 28; // Windows tbc
97     int heightFudge = Platform.isAMac() ? 50 : 50; // tbc
98     int width = ((AlignFrame) getTopFrame()).getWidth() + widthFudge;
99     int height = ((AlignFrame) getTopFrame()).getHeight()
100             + ((AlignFrame) getBottomFrame()).getHeight() + DIVIDER_SIZE
101             + heightFudge;
102     height = fitHeightToDesktop(height);
103     setSize(width, height);
104
105     adjustLayout();
106
107     addCloseFrameListener();
108
109     addKeyListener();
110
111     addKeyBindings();
112
113     addCommandListeners();
114   }
115
116   /**
117    * Reduce the height if too large to fit in the Desktop. Also adjust the
118    * divider location in proportion.
119    * 
120    * @param height
121    *          in pixels
122    * @return original or reduced height
123    */
124   public int fitHeightToDesktop(int height)
125   {
126     // allow about 65 pixels for Desktop decorators on Windows
127
128     int newHeight = Math.min(height, Desktop.instance.getHeight() - 65);
129     if (newHeight != height)
130     {
131       int oldDividerLocation = getDividerLocation();
132       setDividerLocation(oldDividerLocation * newHeight / height);
133     }
134     return newHeight;
135   }
136
137   /**
138    * Set the top and bottom frames to listen to each others Commands (e.g. Edit,
139    * Order).
140    */
141   protected void addCommandListeners()
142   {
143     // TODO if CommandListener is only ever 1:1 for complementary views,
144     // may change broadcast pattern to direct messaging (more efficient)
145     final StructureSelectionManager ssm = StructureSelectionManager
146             .getStructureSelectionManager(Desktop.instance);
147     ssm.addCommandListener(((AlignFrame) getTopFrame()).getViewport());
148     ssm.addCommandListener(((AlignFrame) getBottomFrame()).getViewport());
149   }
150
151   /**
152    * Do any tweaking and twerking of the layout wanted.
153    */
154   public void adjustLayout()
155   {
156     /*
157      * Ensure sequence ids are the same width so sequences line up
158      */
159     int w1 = ((AlignFrame) getTopFrame()).getViewport().getIdWidth();
160     int w2 = ((AlignFrame) getBottomFrame()).getViewport().getIdWidth();
161     int w3 = Math.max(w1, w2);
162     if (w1 != w3)
163     {
164       ((AlignFrame) getTopFrame()).getViewport().setIdWidth(w3);
165     }
166     if (w2 != w3)
167     {
168       ((AlignFrame) getBottomFrame()).getViewport().setIdWidth(w3);
169     }
170
171     /*
172      * Scale protein to either 1 or 3 times character width of dna
173      */
174     final AlignViewport topViewport = ((AlignFrame) getTopFrame()).viewport;
175     final AlignViewport bottomViewport = ((AlignFrame) getBottomFrame()).viewport;
176     final AlignmentI topAlignment = topViewport.getAlignment();
177     final AlignmentI bottomAlignment = bottomViewport.getAlignment();
178     AlignmentViewport cdna = topAlignment.isNucleotide() ? topViewport
179             : (bottomAlignment.isNucleotide() ? bottomViewport : null);
180     AlignmentViewport protein = !topAlignment.isNucleotide() ? topViewport
181             : (!bottomAlignment.isNucleotide() ? bottomViewport : null);
182     if (protein != null && cdna != null)
183     {
184       ViewStyleI vs = protein.getViewStyle();
185       int scale = vs.isScaleProteinAsCdna() ? 3 : 1;
186       vs.setCharWidth(scale * cdna.getViewStyle().getCharWidth());
187       protein.setViewStyle(vs);
188     }
189   }
190
191   /**
192    * Add a listener to tidy up when the frame is closed.
193    */
194   protected void addCloseFrameListener()
195   {
196     addInternalFrameListener(new InternalFrameAdapter()
197     {
198       @Override
199       public void internalFrameClosed(InternalFrameEvent evt)
200       {
201         close();
202       };
203     });
204   }
205
206   /**
207    * Add a key listener that delegates to whichever split component the mouse is
208    * in (or does nothing if neither).
209    */
210   protected void addKeyListener()
211   {
212     addKeyListener(new KeyAdapter()
213     {
214
215       @Override
216       public void keyPressed(KeyEvent e)
217       {
218         AlignFrame af = (AlignFrame) getFrameAtMouse();
219
220         /*
221          * Intercept and override any keys here if wanted.
222          */
223         if (!overrideKey(e, af))
224         {
225           if (af != null)
226           {
227             for (KeyListener kl : af.getKeyListeners())
228             {
229               kl.keyPressed(e);
230             }
231           }
232         }
233       }
234
235       @Override
236       public void keyReleased(KeyEvent e)
237       {
238         Component c = getFrameAtMouse();
239         if (c != null)
240         {
241           for (KeyListener kl : c.getKeyListeners())
242           {
243             kl.keyReleased(e);
244           }
245         }
246       }
247
248     });
249   }
250
251   /**
252    * Returns true if the key event is overriden and actioned (or ignored) here,
253    * else returns false, indicating it should be delegated to the AlignFrame's
254    * usual handler.
255    * <p>
256    * We can't handle Cmd-Key combinations here, instead this is done by
257    * overriding key bindings.
258    * 
259    * @see addKeyOverrides
260    * @param e
261    * @param af
262    * @return
263    */
264   protected boolean overrideKey(KeyEvent e, AlignFrame af)
265   {
266     boolean actioned = false;
267     int keyCode = e.getKeyCode();
268     switch (keyCode)
269     {
270     case KeyEvent.VK_DOWN:
271       if (e.isAltDown() || !af.viewport.cursorMode)
272       {
273         /*
274          * Key down (or Alt-key-down in cursor mode) - move selected sequences
275          */
276         ((AlignFrame) getTopFrame()).moveSelectedSequences(false);
277         ((AlignFrame) getBottomFrame()).moveSelectedSequences(false);
278         actioned = true;
279         e.consume();
280       }
281       break;
282     case KeyEvent.VK_UP:
283       if (e.isAltDown() || !af.viewport.cursorMode)
284       {
285         /*
286          * Key up (or Alt-key-up in cursor mode) - move selected sequences
287          */
288         ((AlignFrame) getTopFrame()).moveSelectedSequences(true);
289         ((AlignFrame) getBottomFrame()).moveSelectedSequences(true);
290         actioned = true;
291         e.consume();
292       }
293     default:
294     }
295     return actioned;
296   }
297
298   /**
299    * Set key bindings (recommended for Swing over key accelerators).
300    */
301   private void addKeyBindings()
302   {
303     overrideDelegatedKeyBindings();
304
305     overrideImplementedKeyBindings();
306   }
307
308   /**
309    * Override key bindings with alternative action methods implemented in this
310    * class.
311    */
312   protected void overrideImplementedKeyBindings()
313   {
314     overrideFind();
315     overrideNewView();
316     overrideCloseView();
317     overrideExpandViews();
318     overrideGatherViews();
319   }
320
321   /**
322    * Replace Cmd-W close view action with our version.
323    */
324   protected void overrideCloseView()
325   {
326     AbstractAction action;
327     /*
328      * Ctrl-W / Cmd-W - close view or window
329      */
330     KeyStroke key_cmdW = KeyStroke.getKeyStroke(KeyEvent.VK_W, Toolkit
331             .getDefaultToolkit().getMenuShortcutKeyMask(), false);
332     action = new AbstractAction()
333     {
334       @Override
335       public void actionPerformed(ActionEvent e)
336       {
337         closeView_actionPerformed();
338       }
339     };
340     overrideKeyBinding(key_cmdW, action);
341   }
342
343   /**
344    * Replace Cmd-T new view action with our version.
345    */
346   protected void overrideNewView()
347   {
348     /*
349      * Ctrl-T / Cmd-T open new view
350      */
351     KeyStroke key_cmdT = KeyStroke.getKeyStroke(KeyEvent.VK_T, Toolkit
352             .getDefaultToolkit().getMenuShortcutKeyMask(), false);
353     AbstractAction action = new AbstractAction()
354     {
355       @Override
356       public void actionPerformed(ActionEvent e)
357       {
358         newView_actionPerformed();
359       }
360     };
361     overrideKeyBinding(key_cmdT, action);
362   }
363
364   /**
365    * For now, delegates key events to the corresponding key accelerator for the
366    * AlignFrame that the mouse is in. Hopefully can be simplified in future if
367    * AlignFrame is changed to use key bindings rather than accelerators.
368    */
369   protected void overrideDelegatedKeyBindings()
370   {
371     if (getTopFrame() instanceof AlignFrame)
372     {
373       /*
374        * Get all accelerator keys in the top frame (the bottom should be
375        * identical) and override each one.
376        */
377       for (Entry<KeyStroke, JMenuItem> acc : ((AlignFrame) getTopFrame())
378               .getAccelerators().entrySet())
379       {
380         overrideKeyBinding(acc);
381       }
382     }
383   }
384
385   /**
386    * Overrides an AlignFrame key accelerator with our version which delegates to
387    * the action listener in whichever frame has the mouse (and does nothing if
388    * neither has).
389    * 
390    * @param acc
391    */
392   private void overrideKeyBinding(Entry<KeyStroke, JMenuItem> acc)
393   {
394     final KeyStroke ks = acc.getKey();
395     InputMap inputMap = this.getInputMap(JComponent.WHEN_FOCUSED);
396     inputMap.put(ks, ks);
397     this.getActionMap().put(ks, new AbstractAction()
398     {
399       @Override
400       public void actionPerformed(ActionEvent e)
401       {
402         Component c = getFrameAtMouse();
403         if (c != null && c instanceof AlignFrame)
404         {
405           for (ActionListener a : ((AlignFrame) c).getAccelerators()
406                   .get(ks).getActionListeners())
407           {
408             a.actionPerformed(null);
409           }
410         }
411       }
412     });
413   }
414
415   /**
416    * Replace an accelerator key's action with the specified action.
417    * 
418    * @param ks
419    */
420   protected void overrideKeyBinding(KeyStroke ks, AbstractAction action)
421   {
422     this.getActionMap().put(ks, action);
423     overrideMenuItem(ks, action);
424   }
425
426   /**
427    * Create and link new views (with matching names) in both panes.
428    * <p>
429    * Note this is _not_ multiple tabs, each hosting a split pane view, rather it
430    * is a single split pane with each split holding multiple tabs which are
431    * linked in pairs.
432    * <p>
433    * TODO implement instead with a tabbed holder in the SplitView, each tab
434    * holding a single JSplitPane. Would avoid a duplicated tab, at the cost of
435    * some additional coding.
436    */
437   protected void newView_actionPerformed()
438   {
439     AlignFrame topFrame = (AlignFrame) getTopFrame();
440     AlignFrame bottomFrame = (AlignFrame) getBottomFrame();
441     final boolean scaleProteinAsCdna = topFrame.viewport
442             .isScaleProteinAsCdna();
443
444     AlignmentPanel newTopPanel = topFrame.newView(null, true);
445     AlignmentPanel newBottomPanel = bottomFrame.newView(null, true);
446
447     /*
448      * This currently (for the first new view only) leaves the top pane on tab 0
449      * but the bottom on tab 1. This results from 'setInitialTabVisible' echoing
450      * from the bottom back to the first frame. Next line is a fudge to work
451      * around this. TODO find a better way.
452      */
453     if (topFrame.getTabIndex() != bottomFrame.getTabIndex())
454     {
455       topFrame.setDisplayedView(newTopPanel);
456     }
457
458     newBottomPanel.av.viewName = newTopPanel.av.viewName;
459     newTopPanel.av.setCodingComplement(newBottomPanel.av);
460
461     /*
462      * These lines can be removed once scaleProteinAsCdna is added to element
463      * Viewport in jalview.xsd, as Jalview2XML.copyAlignPanel will then take
464      * care of it
465      */
466     newTopPanel.av.setScaleProteinAsCdna(scaleProteinAsCdna);
467     newBottomPanel.av.setScaleProteinAsCdna(scaleProteinAsCdna);
468
469     /*
470      * Line up id labels etc
471      */
472     adjustLayout();
473
474     final StructureSelectionManager ssm = StructureSelectionManager
475             .getStructureSelectionManager(Desktop.instance);
476     ssm.addCommandListener(newTopPanel.av);
477     ssm.addCommandListener(newBottomPanel.av);
478   }
479
480   /**
481    * Close the currently selected view in both panes. If there is only one view,
482    * close this split frame.
483    */
484   protected void closeView_actionPerformed()
485   {
486     int viewCount = ((AlignFrame) getTopFrame()).getAlignPanels().size();
487     if (viewCount < 2)
488     {
489       close();
490       return;
491     }
492
493     AlignmentPanel topPanel = ((AlignFrame) getTopFrame()).alignPanel;
494     AlignmentPanel bottomPanel = ((AlignFrame) getBottomFrame()).alignPanel;
495
496     ((AlignFrame) getTopFrame()).closeView(topPanel);
497     ((AlignFrame) getBottomFrame()).closeView(bottomPanel);
498
499   }
500
501   /**
502    * Close child frames and this split frame.
503    */
504   public void close()
505   {
506     ((AlignFrame) getTopFrame()).closeMenuItem_actionPerformed(true);
507     ((AlignFrame) getBottomFrame()).closeMenuItem_actionPerformed(true);
508     try
509     {
510       this.setClosed(true);
511     } catch (PropertyVetoException e)
512     {
513       // ignore
514     }
515   }
516
517   /**
518    * Replace AlignFrame 'expand views' action with SplitFrame version.
519    */
520   protected void overrideExpandViews()
521   {
522     KeyStroke key_X = KeyStroke.getKeyStroke(KeyEvent.VK_X, 0, false);
523     AbstractAction action = new AbstractAction()
524     {
525       @Override
526       public void actionPerformed(ActionEvent e)
527       {
528         expandViews_actionPerformed();
529       }
530     };
531     overrideMenuItem(key_X, action);
532   }
533
534   /**
535    * Replace AlignFrame 'gather views' action with SplitFrame version.
536    */
537   protected void overrideGatherViews()
538   {
539     KeyStroke key_G = KeyStroke.getKeyStroke(KeyEvent.VK_G, 0, false);
540     AbstractAction action = new AbstractAction()
541     {
542       @Override
543       public void actionPerformed(ActionEvent e)
544       {
545         gatherViews_actionPerformed();
546       }
547     };
548     overrideMenuItem(key_G, action);
549   }
550
551   /**
552    * Override the menu action associated with the keystroke in the child frames,
553    * replacing it with the given action.
554    * 
555    * @param ks
556    * @param action
557    */
558   private void overrideMenuItem(KeyStroke ks, AbstractAction action)
559   {
560     overrideMenuItem(ks, action, getTopFrame());
561     overrideMenuItem(ks, action, getBottomFrame());
562   }
563
564   /**
565    * Override the menu action associated with the keystroke in one child frame,
566    * replacing it with the given action. Mwahahahaha.
567    * 
568    * @param key
569    * @param action
570    * @param comp
571    */
572   private void overrideMenuItem(KeyStroke key, final AbstractAction action,
573           JComponent comp)
574   {
575     if (comp instanceof AlignFrame)
576     {
577       JMenuItem mi = ((AlignFrame) comp).getAccelerators().get(key);
578       if (mi != null)
579       {
580         for (ActionListener al : mi.getActionListeners())
581         {
582           mi.removeActionListener(al);
583         }
584         mi.addActionListener(new ActionListener()
585         {
586           @Override
587           public void actionPerformed(ActionEvent e)
588           {
589             action.actionPerformed(e);
590           }
591         });
592       }
593     }
594   }
595
596   /**
597    * Expand any multiple views (which are always in pairs) into separate split
598    * frames.
599    */
600   protected void expandViews_actionPerformed()
601   {
602     Desktop.instance.explodeViews(this);
603   }
604
605   /**
606    * Gather any other SplitFrame views of this alignment back in as multiple
607    * (pairs of) views in this SplitFrame.
608    */
609   protected void gatherViews_actionPerformed()
610   {
611     Desktop.instance.gatherViews(this);
612   }
613
614   /**
615    * Returns the alignment in the complementary frame to the one given.
616    */
617   @Override
618   public AlignmentI getComplement(Object alignFrame)
619   {
620     if (alignFrame == this.getTopFrame())
621     {
622       return ((AlignFrame) getBottomFrame()).viewport.getAlignment();
623     }
624     else if (alignFrame == this.getBottomFrame())
625     {
626       return ((AlignFrame) getTopFrame()).viewport.getAlignment();
627     }
628     return null;
629   }
630
631   /**
632    * Returns the title of the complementary frame to the one given.
633    */
634   @Override
635   public String getComplementTitle(Object alignFrame)
636   {
637     if (alignFrame == this.getTopFrame())
638     {
639       return ((AlignFrame) getBottomFrame()).getTitle();
640     }
641     else if (alignFrame == this.getBottomFrame())
642     {
643       return ((AlignFrame) getTopFrame()).getTitle();
644     }
645     return null;
646   }
647
648   /**
649    * Set the 'other half' to hidden / revealed.
650    */
651   @Override
652   public void setComplementVisible(Object alignFrame, boolean show)
653   {
654     /*
655      * Hiding the AlignPanel suppresses unnecessary repaints
656      */
657     if (alignFrame == getTopFrame())
658     {
659       ((AlignFrame) getBottomFrame()).alignPanel.setVisible(show);
660     }
661     else if (alignFrame == getBottomFrame())
662     {
663       ((AlignFrame) getTopFrame()).alignPanel.setVisible(show);
664     }
665     super.setComplementVisible(alignFrame, show);
666   }
667
668   /**
669    * Replace Cmd-F Find action with our version. This is necessary because the
670    * 'default' Finder searches in the first AlignFrame it finds. We need it to
671    * search in the half of the SplitFrame that has the mouse.
672    */
673   protected void overrideFind()
674   {
675     /*
676      * Ctrl-F / Cmd-F open Finder dialog, 'focused' on the right alignment
677      */
678     KeyStroke key_cmdF = KeyStroke.getKeyStroke(KeyEvent.VK_F, Toolkit
679             .getDefaultToolkit().getMenuShortcutKeyMask(), false);
680     AbstractAction action = new AbstractAction()
681     {
682       @Override
683       public void actionPerformed(ActionEvent e)
684       {
685         Component c = getFrameAtMouse();
686         if (c != null && c instanceof AlignFrame)
687         {
688           AlignFrame af = (AlignFrame) c;
689           new Finder(af.viewport, af.alignPanel);
690         }
691       }
692     };
693     overrideKeyBinding(key_cmdF, action);
694   }
695 }