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.shell;
35  
36  import info.magnolia.ui.vaadin.gwt.client.dialog.connector.OverlayConnector;
37  import info.magnolia.ui.vaadin.gwt.client.magnoliashell.ShellState;
38  import info.magnolia.ui.vaadin.gwt.client.magnoliashell.event.ActivateAppEvent;
39  import info.magnolia.ui.vaadin.gwt.client.magnoliashell.event.AppRequestedEvent;
40  import info.magnolia.ui.vaadin.gwt.client.magnoliashell.event.CurrentAppCloseEvent;
41  import info.magnolia.ui.vaadin.gwt.client.magnoliashell.event.FullScreenEvent;
42  import info.magnolia.ui.vaadin.gwt.client.magnoliashell.event.HideShellAppsEvent;
43  import info.magnolia.ui.vaadin.gwt.client.magnoliashell.event.HideShellAppsRequestedEvent;
44  import info.magnolia.ui.vaadin.gwt.client.magnoliashell.event.ShellAppRequestedEvent;
45  import info.magnolia.ui.vaadin.gwt.client.magnoliashell.event.ShellAppStartedEvent;
46  import info.magnolia.ui.vaadin.gwt.client.magnoliashell.event.ShellAppStartingEvent;
47  import info.magnolia.ui.vaadin.gwt.client.magnoliashell.event.ShellAppsHiddenEvent;
48  import info.magnolia.ui.vaadin.gwt.client.magnoliashell.shell.rpc.ShellClientRpc;
49  import info.magnolia.ui.vaadin.gwt.client.magnoliashell.shell.rpc.ShellServerRpc;
50  import info.magnolia.ui.vaadin.gwt.client.magnoliashell.shellmessage.ShellMessageWidget.MessageType;
51  import info.magnolia.ui.vaadin.gwt.client.magnoliashell.viewport.connector.ViewportConnector;
52  import info.magnolia.ui.vaadin.gwt.client.magnoliashell.viewport.widget.AppsViewportWidget;
53  import info.magnolia.ui.vaadin.gwt.client.magnoliashell.viewport.widget.ViewportWidget;
54  import info.magnolia.ui.vaadin.gwt.client.shared.magnoliashell.Fragment;
55  import info.magnolia.ui.vaadin.gwt.client.shared.magnoliashell.ShellAppType;
56  import info.magnolia.ui.vaadin.gwt.client.shared.magnoliashell.ViewportType;
57  import info.magnolia.ui.vaadin.gwt.client.sticker.connector.StickerConnector;
58  import info.magnolia.ui.vaadin.magnoliashell.MagnoliaShell;
59  
60  import java.util.Iterator;
61  import java.util.List;
62  import java.util.Map.Entry;
63  import java.util.logging.Level;
64  import java.util.logging.Logger;
65  
66  import com.google.gwt.core.client.Scheduler;
67  import com.google.gwt.user.client.History;
68  import com.google.gwt.user.client.Window;
69  import com.google.gwt.user.client.ui.Widget;
70  import com.google.web.bindery.event.shared.EventBus;
71  import com.google.web.bindery.event.shared.SimpleEventBus;
72  import com.vaadin.client.BrowserInfo;
73  import com.vaadin.client.ComponentConnector;
74  import com.vaadin.client.ConnectorHierarchyChangeEvent;
75  import com.vaadin.client.Util;
76  import com.vaadin.client.communication.RpcProxy;
77  import com.vaadin.client.communication.StateChangeEvent.StateChangeHandler;
78  import com.vaadin.client.ui.AbstractLayoutConnector;
79  import com.vaadin.client.ui.layout.ElementResizeEvent;
80  import com.vaadin.client.ui.layout.ElementResizeListener;
81  import com.vaadin.client.ui.nativebutton.NativeButtonConnector;
82  import com.vaadin.shared.ui.Connect;
83  
84  import elemental.client.Browser;
85  
86  /**
87   * MagnoliaShellConnector.
88   */
89  @Connect(MagnoliaShell.class)
90  public class MagnoliaShellConnector extends AbstractLayoutConnector implements MagnoliaShellView.Presenter {
91  
92      private static final Logger log = Logger.getLogger(MagnoliaShellConnector.class.getName());
93  
94      private ShellServerRpc rpc = RpcProxy.create(ShellServerRpc.class, this);
95      private MagnoliaShellView view;
96      private EventBus eventBus = new SimpleEventBus();
97      private Fragment lastHandledFragment;
98      private boolean isHistoryInitialized = false;
99  
100     @Override
101     protected void init() {
102         super.init();
103         addStateChangeHandler((StateChangeHandler) event -> {
104             MagnoliaShellState state = getState();
105             Iterator<Entry<ShellAppType, Integer>> it = state.indications.entrySet().iterator();
106             while (it.hasNext()) {
107                 final Entry<ShellAppType, Integer> entry = it.next();
108                 view.setShellAppIndication(entry.getKey(), entry.getValue());
109             }
110 
111             if (event.hasPropertyChanged("currentUriFragment")) {
112                 // Do not "push" the fragment remembered on the server-side if the page is created (e.g. after re-load).
113                 // Rather rely on the current fragment set by user.
114                 if (!event.isInitialStateChange()) {
115                     History.newItem(getState().currentUriFragment.toFragment(), true);
116                 }
117             }
118         });
119 
120         registerRpc(ShellClientRpc.class, new ShellClientRpc() {
121             @Override
122             public void showMessage(String type, String topic, String msg, String id) {
123                 view.showMessage(MessageType.valueOf(type), topic, msg, id);
124             }
125 
126             @Override
127             public void hideAllMessages() {
128                 view.hideAllMessages();
129             }
130 
131             @Override
132             public void setFullScreen(boolean isFullScreen) {
133                 MagnoliaShellConnector.this.doFullScreen(isFullScreen);
134             }
135         });
136 
137         getLayoutManager().addElementResizeListener(getWidget().getElement(), new ElementResizeListener() {
138             @Override
139             public void onElementResize(ElementResizeEvent e) {
140                 view.updateShellDivet();
141             }
142         });
143 
144         eventBus.addHandler(CurrentAppCloseEvent.TYPE, event -> closeCurrentApp());
145 
146         /**
147          * Fired when the transition that reveals a shell app has just started.
148          */
149         eventBus.addHandler(ShellAppStartingEvent.TYPE, event -> {
150             ShellState.get().setShellAppStarting();
151             view.onShellAppStarting(event.getType());
152         });
153 
154         /**
155          * Fired when the transition that reveals a shell app has just finished.
156          */
157         eventBus.addHandler(ShellAppStartedEvent.TYPE, event -> {
158             final String currentHistoryToken = History.getToken();
159             final Fragment fragment = Fragment.fromString(currentHistoryToken);
160             String newShellAppName = event.getType().name();
161             ShellState.get().setShellAppStarted();
162             if (currentHistoryToken.isEmpty() || !fragment.isShellApp() || !fragment.getAppName().equalsIgnoreCase(newShellAppName)) {
163                 final Fragment newFragment = Fragment.fromString("shell:" + newShellAppName.toLowerCase() + ":");
164                 History.newItem(newFragment.toFragment(), false);
165                 lastHandledFragment = newFragment;
166                 activateShellApp(newFragment);
167             }
168         });
169 
170 
171         /**
172          * Fired when the shell app icon was clicked twice, or area outside of a shell app was clicked.
173          */
174         eventBus.addHandler(HideShellAppsRequestedEvent.TYPE, event -> {
175             if (ShellState.get().isShellAppStarted() || ShellState.get().isShellAppStarting()) {
176                 onHideShellAppsRequested();
177             }
178         });
179 
180         /**
181          * Fired when the shell app viewport is completely hidden.
182          */
183         eventBus.addHandler(ShellAppsHiddenEvent.TYPE, event -> rpc.stopCurrentShellApp());
184 
185         /**
186          * This one is only fired after swipe/keyboard navigation.
187          */
188         eventBus.addHandler(ActivateAppEvent.TYPE, event -> {
189             if (!ShellState.get().isShellAppStarting()) {
190                 log.log(Level.WARNING, "Starting from swipe/keyboard: " + event.getName());
191                 ShellState.get().setAppStarting();
192                 rpc.activateApp(Fragment.fromString("app:" + event.getName()));
193             }
194         });
195 
196         // IE11 doesn't fire popstate event when hash changes.
197         // https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/3740423/
198         // And they're not going to fix it (see comments on the issue)
199         if (BrowserInfo.get().isIE11()) {
200             Browser.getWindow().setOnhashchange(event -> navigate());
201         } else {
202             Browser.getWindow().setOnpopstate(event -> navigate());
203         }
204     }
205 
206     private void navigate() {
207         String hash = Window.Location.getHash();
208         if (hash.startsWith("#")) {
209             hash = hash.substring(1);
210         }
211 
212         final Fragment newFragment = Fragment.fromString(hash);
213 
214         if (newFragment.equals(lastHandledFragment)) {
215             return;
216         }
217 
218         boolean hasActiveRequest = getConnection().getMessageSender().hasActiveRequest();
219 
220         if (newFragment.isShellApp()) {
221             if (!hasActiveRequest || !ShellState.get().isAppStarting()) {
222                 doShowShellApp(newFragment.resolveShellAppType());
223             }
224         } else {
225 
226             if (lastHandledFragment != null) {
227                 log.warning(
228                         "App navigation from " +lastHandledFragment.toFragment()+ " to " + newFragment.toFragment() + (!newFragment.sameSubApp(lastHandledFragment) ? "" : "not") +", request will %s be sent");
229             }
230 
231             // The fragment of the app that was last displayed in the viewport.
232             final Fragment latestLoadedAppLocation = getState().currentUriFragment;
233 
234             /**
235              * The new location points to the same app as before, means probably we have returned from the server roundtrip and app
236              * state/location was refined. Either was - we should mark the state as 'app started'.
237              */
238             if (newFragment.isSameApp(latestLoadedAppLocation)) {
239                 log.warning("Switching to 'APP STARTED' state since the updated app is already active");
240                 ShellState.get().setAppStarted();
241             }
242 
243             /**
244              * We either have no active request -> the location change came directly from address bar, so we have to navigate,
245              * or the new app is requested, so we notify the server about it.
246              */
247 
248             if (!hasActiveRequest || !newFragment.isSameApp(lastHandledFragment)) {
249                 loadApp(newFragment.getAppName());
250             }
251 
252             if (ShellState.get().isAppStarting() || !hasActiveRequest) {
253                 rpc.activateApp(newFragment);
254             }
255         }
256 
257         lastHandledFragment = newFragment;
258     }
259 
260     @Override
261     public void activateShellApp(Fragment f) {
262         rpc.activateShellApp(f);
263     }
264 
265     @Override
266     public void closeCurrentApp() {
267         ShellState.get().setAppClosing();
268         rpc.stopCurrentApp();
269     }
270 
271     @Override
272     public void removeMessage(String id) {
273         rpc.removeMessage(id);
274     }
275 
276     @Override
277     public void loadApp(String appName) {
278         view.onAppStarting();
279         eventBus.fireEvent(new AppRequestedEvent(appName));
280     }
281 
282     private void doShowShellApp(ShellAppType shellAppType) {
283         eventBus.fireEvent(new ShellAppRequestedEvent(shellAppType));
284     }
285 
286     private void doFullScreen(boolean isFullScreen) {
287         eventBus.fireEvent(new FullScreenEvent(isFullScreen));
288     }
289 
290     @Override
291     public void onHideShellAppsRequested() {
292         final AppsViewportWidget/info/magnolia/ui/vaadin/gwt/client/magnoliashell/viewport/widget/AppsViewportWidget.html#AppsViewportWidget">AppsViewportWidget appViewportWidget = (AppsViewportWidget) ((ComponentConnector)getState().viewports.get(ViewportType.APP)).getWidget();
293 
294         // If no app is active, then show or keep the applauncher.
295         Widget currentApp = appViewportWidget.getCurrentApp();
296         if (currentApp != null) {
297             view.onAppStarting();
298             ShellState.get().setShellAppClosing();
299             eventBus.fireEvent(new HideShellAppsEvent());
300         } else {
301             doShowShellApp(ShellAppType.APPLAUNCHER);
302         }
303     }
304 
305     @Override
306     public void showShellApp(ShellAppType type) {
307         // We don't trigger the shell apps via trinity icons and/or 1-3 buttons
308         // if there is a request being processed because it will cause another request (fired after transition is done)
309         // and eventually could lead to the location change race (so called Disco App effect).
310         if (!getConnection().getMessageSender().hasActiveRequest()) {
311             doShowShellApp(type);
312         }
313     }
314 
315     @Override
316     public void updateCaption(ComponentConnector connector) {}
317 
318     @Override
319     public void onConnectorHierarchyChange(ConnectorHierarchyChangeEvent event) {
320         List<ComponentConnector> children = getChildComponents();
321         for (ComponentConnector connector : children) {
322             if (connector instanceof ViewportConnector) {
323                 final ViewportConnector./../../../../../info/magnolia/ui/vaadin/gwt/client/magnoliashell/viewport/connector/ViewportConnector.html#ViewportConnector">ViewportConnector vc = (ViewportConnector) connector;
324                 view.updateViewport(vc.getWidget(), vc.getType());
325                 vc.setEventBus(eventBus);
326             } else if (connector instanceof OverlayConnector) {
327                 if (!view.hasOverlay(connector.getWidget())) {
328                     final OverlayConnector../../../../../../info/magnolia/ui/vaadin/gwt/client/dialog/connector/OverlayConnector.html#OverlayConnector">OverlayConnector oc = (OverlayConnector) connector;
329                     ComponentConnector overlayParent = (ComponentConnector) oc.getState().overlayParent;
330                     Widget parentWidget = overlayParent.getWidget();
331                     view.openOverlayOnWidget(oc.getWidget(), parentWidget);
332                 }
333             } else if (connector instanceof NativeButtonConnector) {
334                 view.setUserMenu(connector.getWidget());
335             } else if (connector instanceof StickerConnector) {
336                 view.setSticker(connector.getWidget());
337             }
338         }
339 
340         List<ComponentConnector> oldChildren = event.getOldChildren();
341         oldChildren.removeAll(children);
342         for (ComponentConnector cc : oldChildren) {
343             cc.getWidget().removeFromParent();
344         }
345     }
346 
347     @Override
348     protected Widget createWidget() {
349         this.view = new MagnoliaShellViewImpl();
350         this.view.setPresenter(this);
351         return view.asWidget();
352     }
353 
354     @Override
355     public MagnoliaShellState getState() {
356         return (MagnoliaShellState) super.getState();
357     }
358 
359     @Override
360     public void updateViewportLayout(ViewportWidget activeViewport) {
361         getLayoutManager().setNeedsMeasure(Util.findConnectorFor(activeViewport));
362         getLayoutManager().layoutNow();
363     }
364 
365 
366     @Override
367     public void initHistory() {
368         Scheduler.get().scheduleDeferred(() -> {
369             if (!isHistoryInitialized) {
370                 isHistoryInitialized = true;
371                 navigate();
372             }
373         });
374     }
375 
376 }