View Javadoc
1   /**
2    * This file Copyright (c) 2010-2018 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.ui.vaadin.gwt.client.magnoliashell.viewport.widget;
35  
36  import info.magnolia.ui.vaadin.gwt.client.CloseButton;
37  import info.magnolia.ui.vaadin.gwt.client.FullScreenButton;
38  import info.magnolia.ui.vaadin.gwt.client.jquerywrapper.JQueryCallback;
39  import info.magnolia.ui.vaadin.gwt.client.jquerywrapper.JQueryWrapper;
40  import info.magnolia.ui.vaadin.gwt.client.magnoliashell.ShellState;
41  import info.magnolia.ui.vaadin.gwt.client.magnoliashell.viewport.AppsTransitionDelegate;
42  import info.magnolia.ui.vaadin.gwt.client.magnoliashell.viewport.MagnoliaSwipeRecognizer;
43  import info.magnolia.ui.vaadin.gwt.client.magnoliashell.viewport.animation.FadeAnimation;
44  import info.magnolia.ui.vaadin.gwt.client.magnoliashell.viewport.animation.SlideAnimation;
45  
46  import java.util.Iterator;
47  
48  import com.google.gwt.dom.client.Style.Visibility;
49  import com.google.gwt.event.shared.HandlerRegistration;
50  import com.google.gwt.user.client.DOM;
51  import com.google.gwt.user.client.Element;
52  import com.google.gwt.user.client.Event;
53  import com.google.gwt.user.client.ui.RootPanel;
54  import com.google.gwt.user.client.ui.Widget;
55  import com.googlecode.mgwt.dom.client.event.touch.TouchCancelEvent;
56  import com.googlecode.mgwt.dom.client.event.touch.TouchCancelHandler;
57  import com.googlecode.mgwt.dom.client.event.touch.TouchEndEvent;
58  import com.googlecode.mgwt.dom.client.event.touch.TouchEndHandler;
59  import com.googlecode.mgwt.dom.client.recognizer.swipe.HasSwipeHandlers;
60  import com.googlecode.mgwt.dom.client.recognizer.swipe.SwipeEndEvent;
61  import com.googlecode.mgwt.dom.client.recognizer.swipe.SwipeEndHandler;
62  import com.googlecode.mgwt.dom.client.recognizer.swipe.SwipeEvent;
63  import com.googlecode.mgwt.dom.client.recognizer.swipe.SwipeEvent.DIRECTION;
64  import com.googlecode.mgwt.dom.client.recognizer.swipe.SwipeMoveEvent;
65  import com.googlecode.mgwt.dom.client.recognizer.swipe.SwipeMoveHandler;
66  import com.googlecode.mgwt.dom.client.recognizer.swipe.SwipeStartEvent;
67  import com.googlecode.mgwt.dom.client.recognizer.swipe.SwipeStartHandler;
68  import com.googlecode.mgwt.ui.client.widget.touch.TouchDelegate;
69  
70  /**
71   * Client side implementation of Apps viewport.
72   */
73  public class AppsViewportWidget extends ViewportWidget<AppsTransitionDelegate> implements HasSwipeHandlers {
74  
75      public static final String APP_INACTIVE_CLASS_NAME = "app-inactive";
76  
77      /**
78       * Listener interface for {@link AppsViewportWidget}.
79       */
80      public interface Listener {
81  
82          void closeCurrentApp();
83  
84          void activateApp(Widget appWidget);
85  
86      };
87  
88      private static final int SWIPE_OUT_THRESHOLD = 300;
89  
90      private final AppPreloader preloader = new AppPreloader();
91  
92      private boolean isAppClosing = false;
93  
94      private boolean isCurtainVisible = false;
95  
96      private Listener listener;
97  
98      private final TouchDelegate delegate = new TouchDelegate(this);
99  
100     private final FullScreenButton fullScreenButton = new FullScreenButton();
101 
102     private Element curtain = DOM.createDiv();
103 
104     /**
105      * Checks whether there are enough apps to navigate to next/previous app (> 1), that some app is loaded and displayed and
106      * that no animation is currently in progress.
107      */
108     public boolean readyForAppSwipeOrShortcutNavigation() {
109         return ShellState.get().isAppStarted() && getWidgetCount() > 1 && !getTransitionDelegate().inProgress();
110     }
111 
112     private void closeCurrentApp() {
113         if (!isAppClosing()) {
114             isAppClosing = true;
115             listener.closeCurrentApp();
116         }
117     }
118 
119     private void toggleFullScreen() {
120         String cssClasses = RootPanel.get().getStyleName();
121         boolean isFullScreen = cssClasses.contains("fullscreen");
122         // toggle.
123         setFullScreen(!isFullScreen);
124     }
125 
126     private CloseButton closeButton = new CloseButton();
127 
128     /**
129      * Set the look of the application and the state of the button.
130      */
131     public void setFullScreen(boolean isFullScreen) {
132 
133         if (isFullScreen) {
134             // enable fullscreen
135             RootPanel.get().addStyleName("fullscreen");
136 
137             fullScreenButton.getElement().addClassName("icon-extend-header");
138             fullScreenButton.getElement().removeClassName("icon-collapse-header");
139         } else {
140 
141             // disable fullscreen
142             RootPanel.get().removeStyleName("fullscreen");
143 
144             fullScreenButton.getElement().addClassName("icon-collapse-header");
145             fullScreenButton.getElement().removeClassName("icon-extend-header");
146         }
147     }
148 
149     public AppsViewportWidget(final Listener listener) {
150         super();
151         this.listener = listener;
152         DOM.sinkEvents(getElement(), Event.ONCLICK);
153         curtain.setClassName("v-curtain v-curtain-green");
154         closeButton.addStyleDependentName("app");
155         delegate.addTouchEndHandler(new TouchEndHandler() {
156             @Override
157             public void onTouchEnd(TouchEndEvent event) {
158                 Element target = event.getNativeEvent().getEventTarget().cast();
159                 if (closeButton.getElement().isOrHasChild(target)) {
160                     closeCurrentApp();
161                 } else if (fullScreenButton.getElement().isOrHasChild(target)) {
162                     toggleFullScreen();
163                 }
164             }
165         });
166 
167         bindTouchHandlers();
168 
169     }
170 
171     public void goToNextApp() {
172         if (getWidgetCount() > 1) {
173             processSwipe(-1);
174             switchToApp(DIRECTION.RIGHT_TO_LEFT);
175         }
176     }
177 
178     public void goToPreviousApp() {
179         if (getWidgetCount() > 1) {
180             processSwipe(1);
181             switchToApp(DIRECTION.LEFT_TO_RIGHT);
182         }
183     }
184 
185     /**
186      * Get the app that is currently open.
187      * @return The currently open app or null if none are open.
188      */
189     public Widget getCurrentApp(){
190         if (getWidgetCount() < 1) {
191             return null;
192         }else{
193             for (int w=0; w < getWidgetCount(); w++){
194                 Widget app = getWidget(w);
195                 String style = app.getStyleName();
196                 if (!app.getStyleName().contains("app-inactive")){
197                     return app;
198                 }
199             }
200         }
201         // No app is active.
202         return null;
203     }
204 
205     public Element getCurtain() {
206         return curtain;
207     }
208 
209     public void setCurtainVisible(boolean visible) {
210         if (isCurtainVisible != visible) {
211             this.isCurtainVisible = visible;
212             getTransitionDelegate().setCurtainVisible(isCurtainVisible);
213         }
214     }
215 
216     /* APP CLOSING */
217     @Override
218     public void showChildNoTransition(Widget w) {
219         getElement().appendChild(closeButton.getElement());
220         getElement().appendChild(fullScreenButton.getElement());
221         Widget formerVisible = getVisibleChild();
222         // do not hide app if closing
223         if (formerVisible != null && !isAppClosing()) {
224             formerVisible.getElement().getStyle().setVisibility(Visibility.HIDDEN);
225             formerVisible.addStyleName(APP_INACTIVE_CLASS_NAME);
226         }
227         w.setVisible(true);
228         w.removeStyleName(APP_INACTIVE_CLASS_NAME);
229         w.getElement().getStyle().clearVisibility();
230     }
231 
232     @Override
233     public void removeChild(Widget w) {
234         getTransitionDelegate().removeWidget(w);
235         getElement().removeChild(closeButton.getElement());
236         getElement().removeChild(fullScreenButton.getElement());
237     }
238 
239     @Override
240     public void removeChildNoTransition(Widget w) {
241         super.removeChildNoTransition(w);
242         isAppClosing = false;
243     }
244 
245     public boolean isAppClosing() {
246         return isAppClosing;
247     }
248 
249     /* APP PRELOADER */
250     public void showAppPreloader(final String appName) {
251         preloader.setCaption(appName);
252         preloader.addStyleName("zoom-in");
253         RootPanel.get().add(preloader);
254     }
255 
256     public boolean hasPreloader() {
257         return RootPanel.get().getWidgetIndex(preloader) >= 0;
258     }
259 
260     public void removePreloader() {
261         final FadeAnimation preloaderFadeOut = new FadeAnimation(0d, true);
262         preloaderFadeOut.addCallback(new JQueryCallback() {
263             @Override
264             public void execute(JQueryWrapper query) {
265                 RootPanel.get().remove(preloader);
266             }
267         });
268         preloaderFadeOut.run(500, preloader.getElement());
269     }
270 
271     /* SWIPE GESTURES */
272     private void bindTouchHandlers() {
273         DOM.sinkEvents(getElement(), Event.TOUCHEVENTS);
274         delegate.addTouchHandler(new MagnoliaSwipeRecognizer(delegate, SWIPE_OUT_THRESHOLD));
275         addSwipeStartHandler(new SwipeStartHandler() {
276             @Override
277             public void onSwipeStart(SwipeStartEvent event) {
278                 processSwipe(event.getDistance() * (event.getDirection() == DIRECTION.LEFT_TO_RIGHT ? 1 : -1));
279             }
280         });
281 
282         addSwipeMoveHandler(new SwipeMoveHandler() {
283             @Override
284             public void onSwipeMove(SwipeMoveEvent event) {
285                 processSwipe(event.getDistance() * (event.getDirection() == SwipeEvent.DIRECTION.LEFT_TO_RIGHT ? 1 : -1));
286             }
287         });
288 
289         addSwipeEndHandler(new SwipeEndHandler() {
290             @Override
291             public void onSwipeEnd(SwipeEndEvent event) {
292                 if (getWidgetCount() > 1) {
293                     final SwipeEvent.DIRECTION direction = event.getDirection();
294                     if (event.isDistanceReached()) {
295                         switchToApp(direction);
296                     } else {
297                         final Widget visibleChild = getVisibleChild();
298                         final SlideAnimation slideAnimation = new SlideAnimation(false, true);
299                         slideAnimation.setTargetValue(0);
300                         slideAnimation.run(500, visibleChild.getElement());
301                         slideAnimation.addCallback(new JQueryCallback() {
302                             @Override
303                             public void execute(JQueryWrapper query) {
304                                 // do not trigger transitions
305                                 dropZIndeces();
306                                 Widget next = getNextWidget();
307                                 Widget previous = getPreviousWidget();
308                                 if (next != null) {
309                                     next.getElement().getStyle().setVisibility(Visibility.HIDDEN);
310                                 }
311 
312                                 if (previous != null) {
313                                     previous.getElement().getStyle().setVisibility(Visibility.HIDDEN);
314                                 }
315                             }
316                         });
317                     }
318                 }
319             }
320         });
321 
322         delegate.addTouchCancelHandler(new TouchCancelHandler() {
323             @Override
324             public void onTouchCanceled(TouchCancelEvent event) {
325                 dropZIndeces();
326                 Widget next = getNextWidget();
327                 Widget previous = getPreviousWidget();
328                 if (next != null) {
329                     next.getElement().getStyle().setVisibility(Visibility.HIDDEN);
330                 }
331 
332                 if (previous != null) {
333                     previous.getElement().getStyle().setVisibility(Visibility.HIDDEN);
334                 }
335 
336                 getVisibleChild().getElement().getStyle().clearLeft();
337             }
338         });
339     }
340 
341     private void switchToApp(final SwipeEvent.DIRECTION direction) {
342 
343         final Widget newVisibleWidget = direction == DIRECTION.LEFT_TO_RIGHT ? getPreviousWidget() : getNextWidget();
344         SlideAnimation slideAnimation = new SlideAnimation(false, true);
345         slideAnimation.addCallback(new JQueryCallback() {
346             @Override
347             public void execute(JQueryWrapper query) {
348                 // do not trigger transitions
349                 showChild(newVisibleWidget);
350                 dropZIndeces();
351                 listener.activateApp(newVisibleWidget);
352             }
353         });
354 
355         Element targetElement = getVisibleChild().getElement();
356         slideAnimation.setTargetValue(getOffsetWidth() * (direction == DIRECTION.LEFT_TO_RIGHT ? 1 : -1));
357         slideAnimation.run(450, targetElement);
358 
359         if (direction == DIRECTION.RIGHT_TO_LEFT && getWidgetCount() > 2) {
360             final SlideAnimation slideNewVisibleToEdgeAnimation = new SlideAnimation(false, true);
361             slideNewVisibleToEdgeAnimation.setTargetValue(0);
362             slideNewVisibleToEdgeAnimation.run(500, newVisibleWidget.getElement());
363         }
364 
365     }
366 
367     private void processSwipe(int translationValue) {
368         if (getWidgetCount() > 1) {
369             JQueryWrapper.select(getVisibleChild()).setCss("-webkit-transform", "translate3d(" + translationValue + "px,0,0)");
370             showCandidateApp(translationValue);
371         }
372     }
373 
374     private void showCandidateApp(int translationValue) {
375         final Widget nextWidget = getNextWidget();
376         final Widget previousWidget = getPreviousWidget();
377         boolean isNext = translationValue < 0;
378         if (isNext) {
379             nextWidget.getElement().getStyle().setZIndex(250);
380             getVisibleChild().getElement().getStyle().setZIndex(251);
381         } else {
382             previousWidget.getElement().getStyle().setZIndex(250);
383             getVisibleChild().getElement().getStyle().setZIndex(251);
384         }
385 
386         if (isNext && getWidgetCount() > 2) {
387             JQueryWrapper.select(nextWidget).setCss("-webkit-transform", "translate3d(" + (translationValue + getVisibleChild().getOffsetWidth()) + "px,0,0)");
388         }
389 
390         nextWidget.getElement().getStyle().setVisibility(isNext || nextWidget == previousWidget ? Visibility.VISIBLE : Visibility.HIDDEN);
391         previousWidget.getElement().getStyle().setVisibility(!isNext || nextWidget == previousWidget ? Visibility.VISIBLE : Visibility.HIDDEN);
392     }
393 
394     private Widget getNextWidget() {
395         int index = getWidgetIndex(getVisibleChild());
396         return getWidget((index + 1) % getWidgetCount());
397     }
398 
399     private Widget getPreviousWidget() {
400         int index = getWidgetIndex(getVisibleChild());
401         int count = getWidgetCount();
402         return getWidget((index + (count - 1)) % count);
403     }
404 
405     private void dropZIndeces() {
406         final Iterator<Widget> it = iterator();
407         while (it.hasNext()) {
408             it.next().getElement().getStyle().clearZIndex();
409         }
410     }
411 
412     @Override
413     public HandlerRegistration addSwipeStartHandler(SwipeStartHandler handler) {
414         return addHandler(handler, SwipeStartEvent.getType());
415     }
416 
417     @Override
418     public HandlerRegistration addSwipeMoveHandler(SwipeMoveHandler handler) {
419         return addHandler(handler, SwipeMoveEvent.getType());
420     }
421 
422     @Override
423     public HandlerRegistration addSwipeEndHandler(SwipeEndHandler handler) {
424         return addHandler(handler, SwipeEndEvent.getType());
425     }
426 
427 }