1 /*
2  * AnimationHelper.java
3  *
4  * Copyright (C) 2021 by RStudio, PBC
5  *
6  * Unless you have received this program directly from RStudio pursuant
7  * to the terms of a commercial license agreement with RStudio, then
8  * this program is licensed to you under the terms of version 3 of the
9  * GNU Affero General Public License. This program is distributed WITHOUT
10  * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
11  * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
12  * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
13  *
14  */
15 package org.rstudio.core.client.layout;
16 
17 import com.google.gwt.dom.client.Document;
18 import com.google.gwt.layout.client.Layout;
19 import com.google.gwt.user.client.ui.Widget;
20 
21 import org.rstudio.core.client.theme.WindowFrame;
22 
23 import static org.rstudio.core.client.layout.WindowState.*;
24 
25 class AnimationHelper
26 {
create(BinarySplitLayoutPanel panel, LogicalWindow top, LogicalWindow bottom, int normal, int splitterHeight, boolean animate, boolean skipFocusChange)27    public static AnimationHelper create(BinarySplitLayoutPanel panel,
28                                         LogicalWindow top,
29                                         LogicalWindow bottom,
30                                         int normal,
31                                         int splitterHeight,
32                                         boolean animate,
33                                         boolean skipFocusChange)
34    {
35       boolean focusGoesOnTop = animate && focusGoesOnTop(top, bottom);
36 
37       int splitterPos;
38       boolean splitterPosFromTop;
39       if (bottom.getState() == WindowState.NORMAL)
40       {
41          splitterPos = normal;
42          splitterPosFromTop = false;
43       }
44       else if (top.getState() == WindowState.HIDE)
45       {
46          splitterPos = -splitterHeight;
47          splitterPosFromTop = true;
48       }
49       else if (bottom.getState() == WindowState.HIDE)
50       {
51          splitterPos = -splitterHeight;
52          splitterPosFromTop = false;
53       }
54       else if (top.getState() == WindowState.MINIMIZE)
55       {
56          splitterPos = top.getMinimized().getDesiredHeight() - splitterHeight / 2;
57          splitterPosFromTop = true;
58       }
59       else if (bottom.getState() == WindowState.MINIMIZE)
60       {
61          splitterPos = bottom.getMinimized().getDesiredHeight() - splitterHeight / 2;
62          splitterPosFromTop = false;
63       }
64       else
65       {
66          throw new RuntimeException("Unexpected condition");
67       }
68 
69       return new AnimationHelper(panel,
70                                  getVisible(top),
71                                  getVisible(bottom),
72                                  top.getNormal(),
73                                  bottom.getNormal(),
74                                  top.getActiveWidget(),
75                                  bottom.getActiveWidget(),
76                                  splitterPos,
77                                  splitterPosFromTop,
78                                  bottom.getState() == WindowState.NORMAL,
79                                  animate,
80                                  focusGoesOnTop,
81                                  skipFocusChange);
82    }
83 
getVisible(LogicalWindow window)84    private static Widget getVisible(LogicalWindow window)
85    {
86       return window.getNormal().isVisible() ? window.getNormal() :
87              window.getMinimized().isVisible() ? window.getMinimized() :
88              null;
89    }
90 
AnimationHelper(BinarySplitLayoutPanel panel, Widget startWidgetTop, Widget startWidgetBottom, Widget animWidgetTop, Widget animWidgetBottom, Widget endWidgetTop, Widget endWidgetBottom, int endSplitterPos, boolean splitterPosFromTop, boolean splitterVisible, boolean animate, boolean focusGoesOnTop, boolean skipFocusChange)91    public AnimationHelper(BinarySplitLayoutPanel panel,
92                           Widget startWidgetTop,
93                           Widget startWidgetBottom,
94                           Widget animWidgetTop,
95                           Widget animWidgetBottom,
96                           Widget endWidgetTop,
97                           Widget endWidgetBottom,
98                           int endSplitterPos,
99                           boolean splitterPosFromTop,
100                           boolean splitterVisible,
101                           boolean animate,
102                           boolean focusGoesOnTop,
103                           boolean skipFocusChange)
104    {
105       panel_ = panel;
106       startWidgetTop_ = startWidgetTop;
107       startWidgetBottom_ = startWidgetBottom;
108       animWidgetTop_ = animWidgetTop;
109       animWidgetBottom_ = animWidgetBottom;
110       endWidgetTop_ = endWidgetTop;
111       endWidgetBottom_ = endWidgetBottom;
112       endSplitterPos_ = endSplitterPos;
113       splitterPosFromTop_ = splitterPosFromTop;
114       splitterVisible_ = splitterVisible;
115       animate_ = animate;
116       focusGoesOnTop_ = focusGoesOnTop;
117       skipFocusChange_ = skipFocusChange;
118    }
119 
animate()120    public void animate()
121    {
122       Document.get().getBody().addClassName("rstudio-animating");
123 
124       panel_.setSplitterVisible(false);
125 
126       if (startWidgetTop_ != animWidgetTop_)
127          panel_.setTopWidget(animWidgetTop_, true);
128       if (startWidgetBottom_ != animWidgetBottom_)
129          panel_.setBottomWidget(animWidgetBottom_, true);
130 
131       panel_.forceLayout();
132 
133       panel_.setSplitterPos(endSplitterPos_,
134                             splitterPosFromTop_);
135       if (animate_)
136       {
137          panel_.animate(250, new Layout.AnimationCallback()
138          {
139             public void onAnimationComplete()
140             {
141                finish();
142                if (skipFocusChange_)
143                   return;
144 
145                ((WindowFrame)(focusGoesOnTop_
146                               ? endWidgetTop_
147                               : endWidgetBottom_)).focus();
148             }
149 
150             public void onLayout(Layout.Layer layer, double progress)
151             {
152             }
153          });
154       }
155       else
156       {
157          finish();
158       }
159    }
160 
finish()161    private void finish()
162    {
163       panel_.setSplitterVisible(splitterVisible_);
164 
165       if (animWidgetTop_ != endWidgetTop_)
166          panel_.setTopWidget(endWidgetTop_, true);
167       if (animWidgetBottom_ != endWidgetBottom_)
168          panel_.setBottomWidget(endWidgetBottom_, true);
169 
170       if (endWidgetTop_ != startWidgetTop_)
171          setParentZindex(startWidgetTop_, -10);
172       setParentZindex(endWidgetTop_, 0);
173 
174       if (endWidgetBottom_ != startWidgetBottom_)
175          setParentZindex(startWidgetBottom_, -10);
176       setParentZindex(endWidgetBottom_, 0);
177 
178       Document.get().getBody().removeClassName("rstudio-animating");
179       panel_.onResize();
180    }
181 
focusGoesOnTop(LogicalWindow top, LogicalWindow bottom)182    private static boolean focusGoesOnTop(LogicalWindow top, LogicalWindow bottom)
183    {
184       // If one window is maximized and the other is minimized, focus the
185       // maximized one.
186       WindowState topState = top.getState();
187       WindowState bottomState = bottom.getState();
188 
189       if (topState == WindowState.MAXIMIZE && bottomState == WindowState.MINIMIZE)
190          return true;
191       else if (topState == WindowState.MINIMIZE && bottomState == WindowState.MAXIMIZE)
192          return false;
193 
194       // If both windows are "normal", focus the one that was previously
195       // minimized.
196 
197       if (top.getState() == MAXIMIZE || bottom.getState() == MAXIMIZE ||
198             top.getState() == EXCLUSIVE || bottom.getState() == EXCLUSIVE)
199       {
200          assert top.getState() == MINIMIZE || bottom.getState() == MINIMIZE
201                || top.getState() == HIDE || bottom.getState() == HIDE;
202          // If one of the windows is minimized, focus the other one.
203          return top.getState() == MAXIMIZE || top.getState() == EXCLUSIVE;
204       }
205 
206       assert top.getState() == NORMAL && bottom.getState() == NORMAL;
207       assert top.getNormal().isVisible() || bottom.getNormal().isVisible();
208 
209       return !top.getNormal().isVisible();
210    }
211 
setParentZindex(Widget widget, int zIndex)212    public static void setParentZindex(Widget widget, int zIndex)
213    {
214       if (widget != null)
215          widget.getElement().getParentElement().getStyle().setZIndex(zIndex);
216    }
217 
218    BinarySplitLayoutPanel panel_;
219 
220    Widget startWidgetTop_;
221    Widget startWidgetBottom_;
222 
223    Widget animWidgetTop_;
224    Widget animWidgetBottom_;
225 
226    Widget endWidgetTop_;
227    Widget endWidgetBottom_;
228 
229    int endSplitterPos_;
230    boolean splitterPosFromTop_;
231    boolean splitterVisible_;
232    private final boolean animate_;
233    private final boolean focusGoesOnTop_;
234    private final boolean skipFocusChange_;
235 }
236