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