Clover icon

Coverage Report

  1. Project Clover database Mon Nov 11 2024 15:14:12 GMT
  2. Package jalview.gui

File SplitFrame.java

 

Coverage histogram

../../img/srcFileCovDistChart4.png
48% of files have more coverage

Code metrics

106
296
51
1
1,116
745
122
0.41
5.8
51
2.39

Classes

Class Line # Actions
SplitFrame 76 296 122
0.357615935.8%
 

Contributing tests

This file is covered by 2 tests. .

Source view

1    /*
2    * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3    * Copyright (C) $$Year-Rel$$ 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 java.awt.BorderLayout;
24    import java.awt.Component;
25    import java.awt.Dimension;
26    import java.awt.event.ActionEvent;
27    import java.awt.event.ActionListener;
28    import java.awt.event.KeyAdapter;
29    import java.awt.event.KeyEvent;
30    import java.awt.event.KeyListener;
31    import java.beans.PropertyVetoException;
32    import java.util.Arrays;
33    import java.util.List;
34    import java.util.Map.Entry;
35   
36    import javax.swing.AbstractAction;
37    import javax.swing.InputMap;
38    import javax.swing.JButton;
39    import javax.swing.JComponent;
40    import javax.swing.JDesktopPane;
41    import javax.swing.JInternalFrame;
42    import javax.swing.JLayeredPane;
43    import javax.swing.JMenuItem;
44    import javax.swing.JPanel;
45    import javax.swing.JTabbedPane;
46    import javax.swing.KeyStroke;
47    import javax.swing.event.ChangeEvent;
48    import javax.swing.event.ChangeListener;
49    import javax.swing.event.InternalFrameAdapter;
50    import javax.swing.event.InternalFrameEvent;
51   
52    import jalview.api.AlignViewControllerGuiI;
53    import jalview.api.FeatureSettingsControllerI;
54    import jalview.api.SplitContainerI;
55    import jalview.controller.FeatureSettingsControllerGuiI;
56    import jalview.datamodel.AlignmentI;
57    import jalview.jbgui.GAlignFrame;
58    import jalview.jbgui.GSplitFrame;
59    import jalview.structure.StructureSelectionManager;
60    import jalview.util.MessageManager;
61    import jalview.util.Platform;
62    import jalview.viewmodel.AlignmentViewport;
63   
64    /**
65    * An internal frame on the desktop that hosts a horizontally split view of
66    * linked DNA and Protein alignments. Additional views can be created in linked
67    * pairs, expanded to separate split frames, or regathered into a single frame.
68    * <p>
69    * (Some) operations on each alignment are automatically mirrored on the other.
70    * These include mouseover (highlighting), sequence and column selection,
71    * sequence ordering and sorting, and grouping, colouring and sorting by tree.
72    *
73    * @author gmcarstairs
74    *
75    */
 
76    public class SplitFrame extends GSplitFrame implements SplitContainerI
77    {
78    private static final int WINDOWS_INSETS_WIDTH = 28; // tbc
79   
80    private static final int MAC_INSETS_WIDTH = 28;
81   
82    private static final int WINDOWS_INSETS_HEIGHT = 50; // tbc
83   
84    private static final int MAC_INSETS_HEIGHT = 50;
85   
86    private static final int DESKTOP_DECORATORS_HEIGHT = 65;
87   
88    private static final long serialVersionUID = 1L;
89   
90    /**
91    * geometry for Feature Settings Holder
92    */
93    private static final int FS_MIN_WIDTH = 400;
94   
95    private static final int FS_MIN_HEIGHT = 400;
96   
 
97  3 toggle public SplitFrame(GAlignFrame top, GAlignFrame bottom)
98    {
99  3 super(top, bottom);
100  3 init();
101    }
102   
103    /**
104    * Initialise this frame.
105    */
 
106  3 toggle protected void init()
107    {
108  3 setFrameIcon(null);
109  3 getTopFrame().setSplitFrame(this);
110  3 getBottomFrame().setSplitFrame(this);
111  3 getTopFrame().setVisible(true);
112  3 getBottomFrame().setVisible(true);
113   
114  3 ((AlignFrame) getTopFrame()).getViewport().setCodingComplement(
115    ((AlignFrame) getBottomFrame()).getViewport());
116   
117    /*
118    * estimate width and height of SplitFrame; this.getInsets() doesn't seem to
119    * give the full additional size (a few pixels short)
120    */
121  3 int widthFudge = Platform.isAMacAndNotJS() ? MAC_INSETS_WIDTH
122    : WINDOWS_INSETS_WIDTH;
123  3 int heightFudge = Platform.isAMacAndNotJS() ? MAC_INSETS_HEIGHT
124    : WINDOWS_INSETS_HEIGHT;
125  3 int width = ((AlignFrame) getTopFrame()).getWidth() + widthFudge;
126  3 int height = ((AlignFrame) getTopFrame()).getHeight()
127    + ((AlignFrame) getBottomFrame()).getHeight() + DIVIDER_SIZE
128    + heightFudge;
129  3 height = fitHeightToDesktop(height);
130  3 setSize(width, height);
131   
132  3 adjustLayout();
133   
134  3 addCloseFrameListener();
135   
136  3 addKeyListener();
137   
138  3 addKeyBindings();
139   
140  3 addCommandListeners();
141    }
142   
143    /**
144    * Reduce the height if too large to fit in the Desktop. Also adjust the
145    * divider location in proportion.
146    *
147    * @param height
148    * in pixels
149    * @return original or reduced height
150    */
 
151  3 toggle public int fitHeightToDesktop(int height)
152    {
153    // allow about 65 pixels for Desktop decorators on Windows
154   
155  3 int newHeight = Math.min(height,
156    Desktop.instance.getHeight() - DESKTOP_DECORATORS_HEIGHT);
157  3 if (newHeight != height)
158    {
159  3 int oldDividerLocation = getDividerLocation();
160  3 setDividerLocation(oldDividerLocation * newHeight / height);
161    }
162  3 return newHeight;
163    }
164   
165    /**
166    * Set the top and bottom frames to listen to each others Commands (e.g. Edit,
167    * Order).
168    */
 
169  3 toggle protected void addCommandListeners()
170    {
171    // TODO if CommandListener is only ever 1:1 for complementary views,
172    // may change broadcast pattern to direct messaging (more efficient)
173  3 final StructureSelectionManager ssm = StructureSelectionManager
174    .getStructureSelectionManager(Desktop.instance);
175  3 ssm.addCommandListener(((AlignFrame) getTopFrame()).getViewport());
176  3 ssm.addCommandListener(((AlignFrame) getBottomFrame()).getViewport());
177    }
178   
179    /**
180    * Do any tweaking and twerking of the layout wanted.
181    */
 
182  3 toggle public void adjustLayout()
183    {
184  3 final AlignViewport topViewport = ((AlignFrame) getTopFrame()).viewport;
185  3 final AlignViewport bottomViewport = ((AlignFrame) getBottomFrame()).viewport;
186   
187    /*
188    * Ensure sequence ids are the same width so sequences line up
189    */
190  3 int w1 = topViewport.getIdWidth();
191  3 int w2 = bottomViewport.getIdWidth();
192  3 int w3 = Math.max(w1, w2);
193  3 topViewport.setIdWidth(w3);
194  3 bottomViewport.setIdWidth(w3);
195   
196    /*
197    * Scale protein to either 1 or 3 times character width of dna
198    */
199  3 final AlignmentI topAlignment = topViewport.getAlignment();
200  3 final AlignmentI bottomAlignment = bottomViewport.getAlignment();
201  3 AlignmentViewport cdna = topAlignment.isNucleotide() ? topViewport
202  0 : (bottomAlignment.isNucleotide() ? bottomViewport : null);
203  3 AlignmentViewport protein = !topAlignment.isNucleotide() ? topViewport
204  3 : (!bottomAlignment.isNucleotide() ? bottomViewport : null);
205  3 if (protein != null && cdna != null)
206    {
207  3 int scale = protein.isScaleProteinAsCdna() ? 3 : 1;
208  3 protein.setCharWidth(scale * cdna.getViewStyle().getCharWidth());
209    }
210    }
211   
212    /**
213    * Adjusts the divider for a sensible split of the real estate (for example,
214    * when many transcripts are shown with a single protein). This should only be
215    * called after the split pane has been laid out (made visible) so it has a
216    * height. The aim is to avoid unnecessary vertical scroll bars, while
217    * ensuring that at least 2 sequences are visible in each panel.
218    * <p>
219    * Once laid out, the user may choose to customise as they wish, so this
220    * method is not called again after the initial layout.
221    */
 
222  2 toggle protected void adjustInitialLayout()
223    {
224  2 AlignFrame topFrame = (AlignFrame) getTopFrame();
225  2 AlignFrame bottomFrame = (AlignFrame) getBottomFrame();
226   
227    /*
228    * recompute layout of top and bottom panels to reflect their
229    * actual (rather than requested) height
230    */
231  2 topFrame.alignPanel.adjustAnnotationHeight();
232  2 bottomFrame.alignPanel.adjustAnnotationHeight();
233   
234  2 final AlignViewport topViewport = topFrame.viewport;
235  2 final AlignViewport bottomViewport = bottomFrame.viewport;
236  2 final AlignmentI topAlignment = topViewport.getAlignment();
237  2 final AlignmentI bottomAlignment = bottomViewport.getAlignment();
238  2 boolean topAnnotations = topViewport.isShowAnnotation();
239  2 boolean bottomAnnotations = bottomViewport.isShowAnnotation();
240    // TODO need number of visible sequences here, not #sequences - how?
241  2 int topCount = topAlignment.getHeight();
242  2 int bottomCount = bottomAlignment.getHeight();
243  2 int topCharHeight = topViewport.getViewStyle().getCharHeight();
244  2 int bottomCharHeight = bottomViewport.getViewStyle().getCharHeight();
245   
246    /*
247    * calculate the minimum ratio that leaves at least the height
248    * of two sequences (after rounding) visible in the top panel
249    */
250  2 int topPanelHeight = topFrame.getHeight();
251  2 int bottomPanelHeight = bottomFrame.getHeight();
252  2 int topSequencesHeight = topFrame.alignPanel.getSeqPanel().seqCanvas
253    .getHeight();
254  2 int topPanelMinHeight = topPanelHeight
255    - Math.max(0, topSequencesHeight - 3 * topCharHeight);
256  2 double totalHeight = (double) topPanelHeight + bottomPanelHeight;
257  2 double minRatio = topPanelMinHeight / totalHeight;
258   
259    /*
260    * calculate the maximum ratio that leaves at least the height
261    * of two sequences (after rounding) visible in the bottom panel
262    */
263  2 int bottomSequencesHeight = bottomFrame.alignPanel
264    .getSeqPanel().seqCanvas.getHeight();
265  2 int bottomPanelMinHeight = bottomPanelHeight
266    - Math.max(0, bottomSequencesHeight - 3 * bottomCharHeight);
267  2 double maxRatio = (totalHeight - bottomPanelMinHeight) / totalHeight;
268   
269    /*
270    * estimate ratio of (topFrameContent / bottomFrameContent)
271    */
272  2 int insets = Platform.isAMacAndNotJS() ? MAC_INSETS_HEIGHT
273    : WINDOWS_INSETS_HEIGHT;
274    // allow 3 'rows' for scale, scrollbar, status bar
275  2 int topHeight = insets + (3 + topCount) * topCharHeight
276  2 + (topAnnotations ? topViewport.calcPanelHeight() : 0);
277  2 int bottomHeight = insets + (3 + bottomCount) * bottomCharHeight
278  2 + (bottomAnnotations ? bottomViewport.calcPanelHeight() : 0);
279  2 double ratio = ((double) topHeight)
280    / (double) (topHeight + bottomHeight);
281   
282    /*
283    * limit ratio to avoid concealing all sequences
284    */
285  2 ratio = Math.min(ratio, maxRatio);
286  2 ratio = Math.max(ratio, minRatio);
287  2 setRelativeDividerLocation(ratio);
288    }
289   
290    /**
291    * Add a listener to tidy up when the frame is closed.
292    */
 
293  3 toggle protected void addCloseFrameListener()
294    {
295  3 addInternalFrameListener(new InternalFrameAdapter()
296    {
 
297  3 toggle @Override
298    public void internalFrameClosed(InternalFrameEvent evt)
299    {
300  3 close();
301    };
302    });
303    }
304   
305    /**
306    * Add a key listener that delegates to whichever split component the mouse is
307    * in (or does nothing if neither).
308    */
 
309  3 toggle protected void addKeyListener()
310    {
311  3 addKeyListener(new KeyAdapter()
312    {
313   
 
314  0 toggle @Override
315    public void keyPressed(KeyEvent e)
316    {
317  0 AlignFrame af = (AlignFrame) getFrameAtMouse();
318   
319    /*
320    * Intercept and override any keys here if wanted.
321    */
322  0 if (!overrideKey(e, af))
323    {
324  0 if (af != null)
325    {
326  0 for (KeyListener kl : af.getKeyListeners())
327    {
328  0 kl.keyPressed(e);
329    }
330    }
331    }
332    }
333   
 
334  0 toggle @Override
335    public void keyReleased(KeyEvent e)
336    {
337  0 Component c = getFrameAtMouse();
338  0 if (c != null)
339    {
340  0 for (KeyListener kl : c.getKeyListeners())
341    {
342  0 kl.keyReleased(e);
343    }
344    }
345    }
346   
347    });
348    }
349   
350    /**
351    * Returns true if the key event is overriden and actioned (or ignored) here,
352    * else returns false, indicating it should be delegated to the AlignFrame's
353    * usual handler.
354    * <p>
355    * We can't handle Cmd-Key combinations here, instead this is done by
356    * overriding key bindings.
357    *
358    * @see addKeyOverrides
359    * @param e
360    * @param af
361    * @return
362    */
 
363  0 toggle protected boolean overrideKey(KeyEvent e, AlignFrame af)
364    {
365  0 boolean actioned = false;
366  0 int keyCode = e.getKeyCode();
367  0 switch (keyCode)
368    {
369  0 case KeyEvent.VK_DOWN:
370  0 if (e.isAltDown() || !af.viewport.cursorMode)
371    {
372    /*
373    * Key down (or Alt-key-down in cursor mode) - move selected sequences
374    */
375  0 ((AlignFrame) getTopFrame()).moveSelectedSequences(false);
376  0 ((AlignFrame) getBottomFrame()).moveSelectedSequences(false);
377  0 actioned = true;
378  0 e.consume();
379    }
380  0 break;
381  0 case KeyEvent.VK_UP:
382  0 if (e.isAltDown() || !af.viewport.cursorMode)
383    {
384    /*
385    * Key up (or Alt-key-up in cursor mode) - move selected sequences
386    */
387  0 ((AlignFrame) getTopFrame()).moveSelectedSequences(true);
388  0 ((AlignFrame) getBottomFrame()).moveSelectedSequences(true);
389  0 actioned = true;
390  0 e.consume();
391    }
392  0 break;
393  0 default:
394    }
395  0 return actioned;
396    }
397   
398    /**
399    * Set key bindings (recommended for Swing over key accelerators).
400    */
 
401  3 toggle private void addKeyBindings()
402    {
403  3 overrideDelegatedKeyBindings();
404   
405  3 overrideImplementedKeyBindings();
406    }
407   
408    /**
409    * Override key bindings with alternative action methods implemented in this
410    * class.
411    */
 
412  3 toggle protected void overrideImplementedKeyBindings()
413    {
414  3 overrideFind();
415  3 overrideNewView();
416  3 overrideCloseView();
417  3 overrideExpandViews();
418  3 overrideGatherViews();
419    }
420   
421    /**
422    * Replace Cmd-W close view action with our version.
423    */
 
424  3 toggle protected void overrideCloseView()
425    {
426  3 AbstractAction action;
427    /*
428    * Ctrl-W / Cmd-W - close view or window
429    */
430  3 KeyStroke key_cmdW = KeyStroke.getKeyStroke(KeyEvent.VK_W,
431    jalview.util.ShortcutKeyMaskExWrapper
432    .getMenuShortcutKeyMaskEx(),
433    false);
434  3 action = new AbstractAction()
435    {
 
436  0 toggle @Override
437    public void actionPerformed(ActionEvent e)
438    {
439  0 closeView_actionPerformed();
440    }
441    };
442  3 overrideKeyBinding(key_cmdW, action);
443    }
444   
445    /**
446    * Replace Cmd-T new view action with our version.
447    */
 
448  3 toggle protected void overrideNewView()
449    {
450    /*
451    * Ctrl-T / Cmd-T open new view
452    */
453  3 KeyStroke key_cmdT = KeyStroke.getKeyStroke(KeyEvent.VK_T,
454    jalview.util.ShortcutKeyMaskExWrapper
455    .getMenuShortcutKeyMaskEx(),
456    false);
457  3 AbstractAction action = new AbstractAction()
458    {
 
459  0 toggle @Override
460    public void actionPerformed(ActionEvent e)
461    {
462  0 newView_actionPerformed();
463    }
464    };
465  3 overrideKeyBinding(key_cmdT, action);
466    }
467   
468    /**
469    * For now, delegates key events to the corresponding key accelerator for the
470    * AlignFrame that the mouse is in. Hopefully can be simplified in future if
471    * AlignFrame is changed to use key bindings rather than accelerators.
472    */
 
473  3 toggle protected void overrideDelegatedKeyBindings()
474    {
475  3 if (getTopFrame() instanceof AlignFrame)
476    {
477    /*
478    * Get all accelerator keys in the top frame (the bottom should be
479    * identical) and override each one.
480    */
481  3 for (Entry<KeyStroke, JMenuItem> acc : ((AlignFrame) getTopFrame())
482    .getAccelerators().entrySet())
483    {
484  81 overrideKeyBinding(acc);
485    }
486    }
487    }
488   
489    /**
490    * Overrides an AlignFrame key accelerator with our version which delegates to
491    * the action listener in whichever frame has the mouse (and does nothing if
492    * neither has).
493    *
494    * @param acc
495    */
 
496  81 toggle private void overrideKeyBinding(Entry<KeyStroke, JMenuItem> acc)
497    {
498  81 final KeyStroke ks = acc.getKey();
499  81 InputMap inputMap = this.getInputMap(JComponent.WHEN_FOCUSED);
500  81 inputMap.put(ks, ks);
501  81 this.getActionMap().put(ks, new AbstractAction()
502    {
 
503  0 toggle @Override
504    public void actionPerformed(ActionEvent e)
505    {
506  0 Component c = getFrameAtMouse();
507  0 if (c != null && c instanceof AlignFrame)
508    {
509  0 for (ActionListener a : ((AlignFrame) c).getAccelerators().get(ks)
510    .getActionListeners())
511    {
512  0 a.actionPerformed(null);
513    }
514    }
515    }
516    });
517    }
518   
519    /**
520    * Replace an accelerator key's action with the specified action.
521    *
522    * @param ks
523    */
 
524  9 toggle protected void overrideKeyBinding(KeyStroke ks, AbstractAction action)
525    {
526  9 this.getActionMap().put(ks, action);
527  9 overrideMenuItem(ks, action);
528    }
529   
530    /**
531    * Create and link new views (with matching names) in both panes.
532    * <p>
533    * Note this is _not_ multiple tabs, each hosting a split pane view, rather it
534    * is a single split pane with each split holding multiple tabs which are
535    * linked in pairs.
536    * <p>
537    * TODO implement instead with a tabbed holder in the SplitView, each tab
538    * holding a single JSplitPane. Would avoid a duplicated tab, at the cost of
539    * some additional coding.
540    */
 
541  0 toggle protected void newView_actionPerformed()
542    {
543  0 AlignFrame topFrame = (AlignFrame) getTopFrame();
544  0 AlignFrame bottomFrame = (AlignFrame) getBottomFrame();
545  0 final boolean scaleProteinAsCdna = topFrame.viewport
546    .isScaleProteinAsCdna();
547   
548  0 AlignmentPanel newTopPanel = topFrame.newView(null, true);
549  0 AlignmentPanel newBottomPanel = bottomFrame.newView(null, true);
550   
551    /*
552    * This currently (for the first new view only) leaves the top pane on tab 0
553    * but the bottom on tab 1. This results from 'setInitialTabVisible' echoing
554    * from the bottom back to the first frame. Next line is a fudge to work
555    * around this. TODO find a better way.
556    */
557  0 if (topFrame.getTabIndex() != bottomFrame.getTabIndex())
558    {
559  0 topFrame.setDisplayedView(newTopPanel);
560    }
561   
562  0 newBottomPanel.av.setViewName(newTopPanel.av.getViewName());
563  0 newTopPanel.av.setCodingComplement(newBottomPanel.av);
564   
565    /*
566    * These lines can be removed once scaleProteinAsCdna is added to element
567    * Viewport in jalview.xsd, as Jalview2XML.copyAlignPanel will then take
568    * care of it
569    */
570  0 newTopPanel.av.setScaleProteinAsCdna(scaleProteinAsCdna);
571  0 newBottomPanel.av.setScaleProteinAsCdna(scaleProteinAsCdna);
572   
573    /*
574    * Line up id labels etc
575    */
576  0 adjustLayout();
577   
578  0 final StructureSelectionManager ssm = StructureSelectionManager
579    .getStructureSelectionManager(Desktop.instance);
580  0 ssm.addCommandListener(newTopPanel.av);
581  0 ssm.addCommandListener(newBottomPanel.av);
582    }
583   
584    /**
585    * Close the currently selected view in both panes. If there is only one view,
586    * close this split frame.
587    */
 
588  0 toggle protected void closeView_actionPerformed()
589    {
590  0 int viewCount = ((AlignFrame) getTopFrame()).getAlignPanels().size();
591  0 if (viewCount < 2)
592    {
593  0 close();
594  0 return;
595    }
596   
597  0 AlignmentPanel topPanel = ((AlignFrame) getTopFrame()).alignPanel;
598  0 AlignmentPanel bottomPanel = ((AlignFrame) getBottomFrame()).alignPanel;
599   
600  0 ((AlignFrame) getTopFrame()).closeView(topPanel);
601  0 ((AlignFrame) getBottomFrame()).closeView(bottomPanel);
602   
603    }
604   
605    /**
606    * Close child frames and this split frame.
607    */
 
608  3 toggle public void close()
609    {
610  3 ((AlignFrame) getTopFrame()).closeMenuItem_actionPerformed(true);
611  3 ((AlignFrame) getBottomFrame()).closeMenuItem_actionPerformed(true);
612  3 try
613    {
614  3 this.setClosed(true);
615    } catch (PropertyVetoException e)
616    {
617    // ignore
618    }
619    }
620   
621    /**
622    * Replace AlignFrame 'expand views' action with SplitFrame version.
623    */
 
624  3 toggle protected void overrideExpandViews()
625    {
626  3 KeyStroke key_X = KeyStroke.getKeyStroke(KeyEvent.VK_X, 0, false);
627  3 AbstractAction action = new AbstractAction()
628    {
 
629  0 toggle @Override
630    public void actionPerformed(ActionEvent e)
631    {
632  0 expandViews_actionPerformed();
633    }
634    };
635  3 overrideMenuItem(key_X, action);
636    }
637   
638    /**
639    * Replace AlignFrame 'gather views' action with SplitFrame version.
640    */
 
641  3 toggle protected void overrideGatherViews()
642    {
643  3 KeyStroke key_G = KeyStroke.getKeyStroke(KeyEvent.VK_G, 0, false);
644  3 AbstractAction action = new AbstractAction()
645    {
 
646  0 toggle @Override
647    public void actionPerformed(ActionEvent e)
648    {
649  0 gatherViews_actionPerformed();
650    }
651    };
652  3 overrideMenuItem(key_G, action);
653    }
654   
655    /**
656    * Override the menu action associated with the keystroke in the child frames,
657    * replacing it with the given action.
658    *
659    * @param ks
660    * @param action
661    */
 
662  15 toggle private void overrideMenuItem(KeyStroke ks, AbstractAction action)
663    {
664  15 overrideMenuItem(ks, action, getTopFrame());
665  15 overrideMenuItem(ks, action, getBottomFrame());
666    }
667   
668    /**
669    * Override the menu action associated with the keystroke in one child frame,
670    * replacing it with the given action. Mwahahahaha.
671    *
672    * @param key
673    * @param action
674    * @param comp
675    */
 
676  30 toggle private void overrideMenuItem(KeyStroke key, final AbstractAction action,
677    JComponent comp)
678    {
679  30 if (comp instanceof AlignFrame)
680    {
681  30 JMenuItem mi = ((AlignFrame) comp).getAccelerators().get(key);
682  30 if (mi != null)
683    {
684  30 for (ActionListener al : mi.getActionListeners())
685    {
686  30 mi.removeActionListener(al);
687    }
688  30 mi.addActionListener(new ActionListener()
689    {
 
690  0 toggle @Override
691    public void actionPerformed(ActionEvent e)
692    {
693  0 action.actionPerformed(e);
694    }
695    });
696    }
697    }
698    }
699   
700    /**
701    * Expand any multiple views (which are always in pairs) into separate split
702    * frames.
703    */
 
704  0 toggle protected void expandViews_actionPerformed()
705    {
706  0 Desktop.instance.explodeViews(this);
707    }
708   
709    /**
710    * Gather any other SplitFrame views of this alignment back in as multiple
711    * (pairs of) views in this SplitFrame.
712    */
 
713  0 toggle protected void gatherViews_actionPerformed()
714    {
715  0 Desktop.instance.gatherViews(this);
716    }
717   
718    /**
719    * Returns the alignment in the complementary frame to the one given.
720    */
 
721  0 toggle @Override
722    public AlignmentI getComplement(Object alignFrame)
723    {
724  0 if (alignFrame == this.getTopFrame())
725    {
726  0 return ((AlignFrame) getBottomFrame()).viewport.getAlignment();
727    }
728  0 else if (alignFrame == this.getBottomFrame())
729    {
730  0 return ((AlignFrame) getTopFrame()).viewport.getAlignment();
731    }
732  0 return null;
733    }
734   
735    /**
736    * Returns the title of the complementary frame to the one given.
737    */
 
738  0 toggle @Override
739    public String getComplementTitle(Object alignFrame)
740    {
741  0 if (alignFrame == this.getTopFrame())
742    {
743  0 return ((AlignFrame) getBottomFrame()).getTitle();
744    }
745  0 else if (alignFrame == this.getBottomFrame())
746    {
747  0 return ((AlignFrame) getTopFrame()).getTitle();
748    }
749  0 return null;
750    }
751   
752    /**
753    * Set the 'other half' to hidden / revealed.
754    */
 
755  0 toggle @Override
756    public void setComplementVisible(Object alignFrame, boolean show)
757    {
758    /*
759    * Hiding the AlignPanel suppresses unnecessary repaints
760    */
761  0 if (alignFrame == getTopFrame())
762    {
763  0 ((AlignFrame) getBottomFrame()).alignPanel.setVisible(show);
764    }
765  0 else if (alignFrame == getBottomFrame())
766    {
767  0 ((AlignFrame) getTopFrame()).alignPanel.setVisible(show);
768    }
769  0 super.setComplementVisible(alignFrame, show);
770    }
771   
772    /**
773    * return the AlignFrames held by this container
774    *
775    * @return { Top alignFrame (Usually CDS), Bottom AlignFrame (Usually
776    * Protein)}
777    */
 
778  0 toggle public List<AlignFrame> getAlignFrames()
779    {
780  0 return Arrays
781    .asList(new AlignFrame[]
782    { (AlignFrame) getTopFrame(), (AlignFrame) getBottomFrame() });
783    }
784   
 
785  0 toggle @Override
786    public AlignFrame getComplementAlignFrame(
787    AlignViewControllerGuiI alignFrame)
788    {
789  0 if (getTopFrame() == alignFrame)
790    {
791  0 return (AlignFrame) getBottomFrame();
792    }
793  0 if (getBottomFrame() == alignFrame)
794    {
795  0 return (AlignFrame) getTopFrame();
796    }
797    // we didn't know anything about this frame...
798  0 return null;
799    }
800   
801    /**
802    * Replace Cmd-F Find action with our version. This is necessary because the
803    * 'default' Finder searches in the first AlignFrame it finds. We need it to
804    * search in the half of the SplitFrame that has the mouse.
805    */
 
806  3 toggle protected void overrideFind()
807    {
808    /*
809    * Ctrl-F / Cmd-F open Finder dialog, 'focused' on the right alignment
810    */
811  3 KeyStroke key_cmdF = KeyStroke.getKeyStroke(KeyEvent.VK_F,
812    jalview.util.ShortcutKeyMaskExWrapper
813    .getMenuShortcutKeyMaskEx(),
814    false);
815  3 AbstractAction action = new AbstractAction()
816    {
 
817  0 toggle @Override
818    public void actionPerformed(ActionEvent e)
819    {
820  0 Component c = getFrameAtMouse();
821  0 if (c != null && c instanceof AlignFrame)
822    {
823  0 AlignFrame af = (AlignFrame) c;
824  0 boolean dna = af.getViewport().getAlignment().isNucleotide();
825  0 String scope = MessageManager.getString("label.in") + " "
826  0 + (dna ? MessageManager.getString("label.nucleotide")
827    : MessageManager.getString("label.protein"));
828  0 new Finder(af.alignPanel, true, scope);
829    }
830    }
831    };
832  3 overrideKeyBinding(key_cmdF, action);
833    }
834   
835    /**
836    * Override to do nothing if triggered from one of the child frames
837    */
 
838  11 toggle @Override
839    public void setSelected(boolean selected) throws PropertyVetoException
840    {
841  11 JDesktopPane desktopPane = getDesktopPane();
842  11 JInternalFrame fr = desktopPane == null ? null
843    : desktopPane.getSelectedFrame();
844  11 if (fr == getTopFrame() || fr == getBottomFrame())
845    {
846    /*
847    * patch for JAL-3288 (deselecting top/bottom frame closes popup menu);
848    * it may be possible to remove this method in future
849    * if the underlying Java behaviour changes
850    */
851  0 if (selected)
852    {
853  0 moveToFront();
854    }
855  0 return;
856    }
857  11 super.setSelected(selected);
858    }
859   
860    /**
861    * holds the frame for feature settings, so Protein and DNA tabs can be
862    * managed
863    */
864    JInternalFrame featureSettingsUI;
865   
866    JTabbedPane featureSettingsPanels;
867   
 
868  0 toggle @Override
869    public void addFeatureSettingsUI(
870    FeatureSettingsControllerGuiI featureSettings)
871    {
872  0 boolean showInternalFrame = false;
873  0 if (featureSettingsUI == null || featureSettingsPanels == null)
874    {
875  0 showInternalFrame = true;
876  0 featureSettingsPanels = new JTabbedPane();
877  0 featureSettingsPanels.addChangeListener(new ChangeListener()
878    {
879   
 
880  0 toggle @Override
881    public void stateChanged(ChangeEvent e)
882    {
883  0 if (e.getSource() != featureSettingsPanels
884    || featureSettingsUI == null
885    || featureSettingsUI.isClosed()
886    || !featureSettingsUI.isVisible())
887    {
888    // not our tabbed pane
889  0 return;
890    }
891  0 int tab = featureSettingsPanels.getSelectedIndex();
892  0 if (tab < 0 || featureSettingsPanels
893    .getSelectedComponent() instanceof FeatureSettingsControllerGuiI)
894    {
895    // no tab selected or already showing a feature settings GUI
896  0 return;
897    }
898  0 getAlignFrames().get(tab).showFeatureSettingsUI();
899    }
900    });
901  0 featureSettingsUI = new JInternalFrame(MessageManager.getString(
902    "label.sequence_feature_settings_for_CDS_and_Protein"));
903  0 featureSettingsUI.setFrameIcon(null);
904  0 featureSettingsPanels.setOpaque(true);
905   
906  0 JPanel dialog = new JPanel();
907  0 dialog.setOpaque(true);
908  0 dialog.setLayout(new BorderLayout());
909  0 dialog.add(featureSettingsPanels, BorderLayout.CENTER);
910  0 JPanel buttons = new JPanel();
911  0 JButton ok = new JButton(MessageManager.getString("action.ok"));
912  0 ok.addActionListener(new ActionListener()
913    {
914   
 
915  0 toggle @Override
916    public void actionPerformed(ActionEvent e)
917    {
918  0 try
919    {
920  0 featureSettingsUI.setClosed(true);
921    } catch (PropertyVetoException pv)
922    {
923  0 pv.printStackTrace();
924    }
925    }
926    });
927  0 JButton cancel = new JButton(
928    MessageManager.getString("action.cancel"));
929  0 cancel.addActionListener(new ActionListener()
930    {
931   
 
932  0 toggle @Override
933    public void actionPerformed(ActionEvent e)
934    {
935  0 try
936    {
937  0 for (Component fspanel : featureSettingsPanels.getComponents())
938    {
939  0 if (fspanel instanceof FeatureSettingsControllerGuiI)
940    {
941  0 ((FeatureSettingsControllerGuiI) fspanel).revert();
942    }
943    }
944  0 featureSettingsUI.setClosed(true);
945    } catch (Exception pv)
946    {
947  0 pv.printStackTrace();
948    }
949    }
950    });
951  0 buttons.add(ok);
952  0 buttons.add(cancel);
953  0 dialog.add(buttons, BorderLayout.SOUTH);
954  0 featureSettingsUI.setContentPane(dialog);
955  0 createDummyTabs();
956    }
957  0 if (featureSettingsPanels
958    .indexOfTabComponent((Component) featureSettings) > -1)
959    {
960    // just show the feature settings !
961  0 featureSettingsPanels
962    .setSelectedComponent((Component) featureSettings);
963  0 return;
964    }
965    // otherwise replace the dummy tab with the given feature settings
966  0 int pos = getAlignFrames().indexOf(featureSettings.getAlignframe());
967    // if pos==-1 then alignFrame isn't managed by this splitframe
968  0 if (pos == 0)
969    {
970  0 featureSettingsPanels.removeTabAt(0);
971  0 featureSettingsPanels.insertTab(tabName[0], null,
972    (Component) featureSettings,
973    MessageManager.formatMessage(
974    "label.sequence_feature_settings_for", tabName[0]),
975    0);
976    }
977  0 if (pos == 1)
978    {
979  0 featureSettingsPanels.removeTabAt(1);
980  0 featureSettingsPanels.insertTab(tabName[1], null,
981    (Component) featureSettings,
982    MessageManager.formatMessage(
983    "label.sequence_feature_settings_for", tabName[1]),
984    1);
985    }
986  0 featureSettingsPanels.setSelectedComponent((Component) featureSettings);
987   
988    // TODO: JAL-3535 - construct a feature settings title including names of
989    // currently selected CDS and Protein names
990   
991  0 if (showInternalFrame)
992    {
993  0 if (Platform.isAMacAndNotJS())
994    {
995  0 Desktop.addInternalFrame(featureSettingsUI,
996    MessageManager.getString(
997    "label.sequence_feature_settings_for_CDS_and_Protein"),
998    600, 480);
999    }
1000    else
1001    {
1002  0 Desktop.addInternalFrame(featureSettingsUI,
1003    MessageManager.getString(
1004    "label.sequence_feature_settings_for_CDS_and_Protein"),
1005    600, 450);
1006    }
1007  0 featureSettingsUI
1008    .setMinimumSize(new Dimension(FS_MIN_WIDTH, FS_MIN_HEIGHT));
1009   
1010  0 featureSettingsUI.addInternalFrameListener(
1011    new javax.swing.event.InternalFrameAdapter()
1012    {
 
1013  0 toggle @Override
1014    public void internalFrameClosed(
1015    javax.swing.event.InternalFrameEvent evt)
1016    {
1017  0 for (int tab = 0; tab < featureSettingsPanels
1018    .getTabCount();)
1019    {
1020  0 FeatureSettingsControllerGuiI fsettings = (FeatureSettingsControllerGuiI) featureSettingsPanels
1021    .getTabComponentAt(tab);
1022  0 if (fsettings != null)
1023    {
1024  0 featureSettingsPanels.removeTabAt(tab);
1025  0 fsettings.featureSettings_isClosed();
1026    }
1027    else
1028    {
1029  0 tab++;
1030    }
1031    }
1032  0 featureSettingsPanels = null;
1033  0 featureSettingsUI = null;
1034    };
1035    });
1036  0 featureSettingsUI.setLayer(JLayeredPane.PALETTE_LAYER);
1037    }
1038    }
1039   
1040    /**
1041    * tab names for feature settings
1042    */
1043    private String[] tabName = new String[] {
1044    MessageManager.getString("label.CDS"),
1045    MessageManager.getString("label.protein") };
1046   
1047    /**
1048    * create placeholder tabs which materialise the feature settings for a given
1049    * view. Also reinitialises any tabs containing stale feature settings
1050    */
 
1051  0 toggle private void createDummyTabs()
1052    {
1053  0 for (int tabIndex = 0; tabIndex < 2; tabIndex++)
1054    {
1055  0 JPanel dummyTab = new JPanel();
1056  0 featureSettingsPanels.addTab(tabName[tabIndex], dummyTab);
1057    }
1058    }
1059   
 
1060  0 toggle private void replaceWithDummyTab(FeatureSettingsControllerI toClose)
1061    {
1062  0 Component dummyTab = null;
1063  0 for (int tabIndex = 0; tabIndex < 2; tabIndex++)
1064    {
1065  0 if (featureSettingsPanels.getTabCount() > tabIndex)
1066    {
1067  0 dummyTab = featureSettingsPanels.getTabComponentAt(tabIndex);
1068  0 if (dummyTab instanceof FeatureSettingsControllerGuiI
1069    && !dummyTab.isVisible())
1070    {
1071  0 featureSettingsPanels.removeTabAt(tabIndex);
1072    // close the feature Settings tab
1073  0 ((FeatureSettingsControllerGuiI) dummyTab)
1074    .featureSettings_isClosed();
1075    // create a dummy tab in its place
1076  0 dummyTab = new JPanel();
1077  0 featureSettingsPanels.insertTab(tabName[tabIndex], null, dummyTab,
1078    MessageManager.formatMessage(
1079    "label.sequence_feature_settings_for",
1080    tabName[tabIndex]),
1081    tabIndex);
1082    }
1083    }
1084    }
1085    }
1086   
 
1087  0 toggle @Override
1088    public void closeFeatureSettings(
1089    FeatureSettingsControllerI featureSettings,
1090    boolean closeContainingFrame)
1091    {
1092  0 if (featureSettingsUI != null)
1093    {
1094  0 if (closeContainingFrame)
1095    {
1096  0 try
1097    {
1098  0 featureSettingsUI.setClosed(true);
1099    } catch (Exception x)
1100    {
1101    }
1102  0 featureSettingsUI = null;
1103    }
1104    else
1105    {
1106  0 replaceWithDummyTab(featureSettings);
1107    }
1108    }
1109    }
1110   
 
1111  0 toggle @Override
1112    public boolean isFeatureSettingsOpen()
1113    {
1114  0 return featureSettingsUI != null && !featureSettingsUI.isClosed();
1115    }
1116    }