View Javadoc
1   /**
2    * This file Copyright (c) 2010-2015 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.magnoliashell.MagnoliaShell;
58  
59  import java.util.Iterator;
60  import java.util.List;
61  import java.util.Map.Entry;
62  import java.util.logging.Level;
63  import java.util.logging.Logger;
64  
65  import com.google.gwt.core.client.Scheduler;
66  import com.google.gwt.event.logical.shared.ValueChangeEvent;
67  import com.google.gwt.event.logical.shared.ValueChangeHandler;
68  import com.google.gwt.user.client.History;
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.ComponentConnector;
73  import com.vaadin.client.ConnectorHierarchyChangeEvent;
74  import com.vaadin.client.Util;
75  import com.vaadin.client.communication.RpcProxy;
76  import com.vaadin.client.communication.StateChangeEvent;
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  /**
85   * MagnoliaShellConnector.
86   */
87  @Connect(MagnoliaShell.class)
88  public class MagnoliaShellConnector extends AbstractLayoutConnector implements MagnoliaShellView.Presenter {
89  
90      private static final Logger log = Logger.getLogger(MagnoliaShellConnector.class.getName());
91  
92      private ShellServerRpc rpc = RpcProxy.create(ShellServerRpc.class, this);
93      private MagnoliaShellView view;
94      private EventBus eventBus = new SimpleEventBus();
95      private Fragment lastHandledFragment;
96      private boolean isHistoryInitialized = false;
97  
98      @Override
99      protected void init() {
100         super.init();
101         addStateChangeHandler(new StateChangeHandler() {
102             @Override
103             public void onStateChanged(StateChangeEvent 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         });
112 
113         registerRpc(ShellClientRpc.class, new ShellClientRpc() {
114             @Override
115             public void showMessage(String type, String topic, String msg, String id) {
116                 view.showMessage(MessageType.valueOf(type), topic, msg, id);
117             }
118 
119             @Override
120             public void hideAllMessages() {
121                 view.hideAllMessages();
122             }
123 
124             @Override
125             public void setFullScreen(boolean isFullScreen) {
126                 MagnoliaShellConnector.this.doFullScreen(isFullScreen);
127             }
128         });
129 
130         getLayoutManager().addElementResizeListener(getWidget().getElement(), new ElementResizeListener() {
131             @Override
132             public void onElementResize(ElementResizeEvent e) {
133                 view.updateShellDivet();
134             }
135         });
136 
137         eventBus.addHandler(CurrentAppCloseEvent.TYPE, new CurrentAppCloseEvent.Handler() {
138             @Override
139             public void onViewportClose(CurrentAppCloseEvent event) {
140                 closeCurrentApp();
141             }
142         });
143 
144         /**
145          * Fired when the transition that reveals a shell app has just started.
146          */
147         eventBus.addHandler(ShellAppStartingEvent.TYPE, new ShellAppStartingEvent.Handler() {
148             @Override
149             public void onShellAppStarting(ShellAppStartingEvent event) {
150                 ShellState.get().setShellAppStarting();
151                 view.onShellAppStarting(event.getType());
152             }
153         });
154 
155         /**
156          * Fired when the transition that reveals a shell app has just finished.
157          */
158         eventBus.addHandler(ShellAppStartedEvent.TYPE, new ShellAppStartedEvent.Handler() {
159             @Override
160             public void onShellAppStarted(ShellAppStartedEvent event) {
161                 final String currentHistoryToken = History.getToken();
162                 final Fragment fragment = Fragment.fromString(currentHistoryToken);
163                 String newShellAppName = event.getType().name();
164                 ShellState.get().setShellAppStarted();
165                 if (currentHistoryToken.isEmpty() || !fragment.isShellApp() || !fragment.getAppName().equalsIgnoreCase(newShellAppName)) {
166                     final Fragment newFragment = Fragment.fromString("shell:" + newShellAppName.toLowerCase() + ":");
167                     History.newItem(newFragment.toFragment(), false);
168                     lastHandledFragment = newFragment;
169                     activateShellApp(newFragment);
170                 }
171             }
172         });
173 
174 
175         /**
176          * Fired when the shell app icon was clicked twice, or area outside of a shell app was clicked.
177          */
178         eventBus.addHandler(HideShellAppsRequestedEvent.TYPE, new HideShellAppsRequestedEvent.Handler() {
179             @Override
180             public void onHideShellAppsRequest(HideShellAppsRequestedEvent event) {
181                 if (ShellState.get().isShellAppStarted() || ShellState.get().isShellAppStarting()) {
182                     onHideShellAppsRequested();
183                 }
184             }
185         });
186 
187         /**
188          * Fired when the shell app viewport is completely hidden.
189          */
190         eventBus.addHandler(ShellAppsHiddenEvent.TYPE, new ShellAppsHiddenEvent.Handler() {
191             @Override
192             public void onShellAppsHidden(ShellAppsHiddenEvent event) {
193                     rpc.stopCurrentShellApp();
194             }
195         });
196 
197         /**
198          * This one is only fired after swipe/keyboard navigation.
199          */
200         eventBus.addHandler(ActivateAppEvent.TYPE, new ActivateAppEvent.Handler() {
201             @Override
202             public void onActivateApp(ActivateAppEvent event) {
203                 if (!ShellState.get().isShellAppStarting()) {
204                     log.log(Level.WARNING, "Starting from swipe/keyboard: " + event.getName());
205                     ShellState.get().setAppStarting();
206                     rpc.activateApp(Fragment.fromString("app:" + event.getName()));
207                 }
208             }
209         });
210 
211         /**
212          * Handles the address bar navigation.
213          */
214         History.addValueChangeHandler(new ValueChangeHandler<String>() {
215             @Override
216             public void onValueChange(ValueChangeEvent<String> event) {
217                 final Fragment newFragment = Fragment.fromString(event.getValue());
218 
219                 if (newFragment.equals(lastHandledFragment)) {
220                     return;
221                 }
222 
223                 if (newFragment.isShellApp()) {
224                     if (!getConnection().hasActiveRequest() || !ShellState.get().isAppStarting()) {
225                         doShowShellApp(newFragment.resolveShellAppType());
226                     }
227                 } else {
228 
229                     if (lastHandledFragment != null) {
230                         log.warning("App navigation from " +lastHandledFragment.toFragment()+ " to " + newFragment.toFragment() + (!newFragment.sameSubApp(lastHandledFragment) ? "" : "not") +", request will %s be sent");
231                     }
232 
233                     // The fragment of the app that was last displayed in the viewport.
234                     final Fragment latestLoadedAppLocation = getState().currentAppUriFragment;
235 
236                     /**
237                      * The new location points to the same app as before, means probably we have returned from the server roundtrip and app
238                      * state/location was refined. Either was - we should mark the state as 'app started'.
239                      */
240                     if (newFragment.isSameApp(latestLoadedAppLocation)) {
241                         log.warning("Switching to 'APP STARTED' state since the updated app is already active");
242                         ShellState.get().setAppStarted();
243                     }
244 
245                     /**
246                      * We either have no active request -> the location change came directly from address bar, so we have to navigate,
247                      * or the new app is requested, so we notify the server about it.
248                      */
249 
250                     if (!getConnection().hasActiveRequest() || !newFragment.isSameApp(lastHandledFragment)) {
251                         loadApp(newFragment.getAppName());
252                     }
253 
254                     if (ShellState.get().isAppStarting() || !getConnection().hasActiveRequest()) {
255                         rpc.activateApp(newFragment);
256                         Scheduler.get().scheduleFinally(new Scheduler.RepeatingCommand() {
257                             @Override
258                             public boolean execute() {
259                                 boolean doneProcessingRequest = !getConnection().hasActiveRequest();
260                                 if (doneProcessingRequest) {
261 
262                                 }
263                                 return !doneProcessingRequest;
264                             }
265                         });
266 
267                     }
268                 }
269                 lastHandledFragment = newFragment;
270             }
271         });
272     }
273 
274     @Override
275     public void activateShellApp(Fragment f) {
276         rpc.activateShellApp(f);
277     }
278 
279     @Override
280     public void closeCurrentApp() {
281         ShellState.get().setAppClosing();
282         rpc.stopCurrentApp();
283     }
284 
285     @Override
286     public void removeMessage(String id) {
287         rpc.removeMessage(id);
288     }
289 
290     @Override
291     public void loadApp(String appName) {
292         view.onAppStarting();
293         eventBus.fireEvent(new AppRequestedEvent(appName));
294     }
295 
296     private void doShowShellApp(ShellAppType shellAppType) {
297         eventBus.fireEvent(new ShellAppRequestedEvent(shellAppType));
298     }
299 
300     private void doFullScreen(boolean isFullScreen) {
301         eventBus.fireEvent(new FullScreenEvent(isFullScreen));
302     }
303 
304     @Override
305     public void onHideShellAppsRequested() {
306         final AppsViewportWidget appViewportWidget = (AppsViewportWidget) ((ComponentConnector)getState().viewports.get(ViewportType.APP)).getWidget();
307 
308         // If no app is active, then show or keep the applauncher.
309         Widget currentApp = appViewportWidget.getCurrentApp();
310         if (currentApp != null) {
311             view.onAppStarting();
312             ShellState.get().setShellAppClosing();
313             eventBus.fireEvent(new HideShellAppsEvent());
314         } else {
315             doShowShellApp(ShellAppType.APPLAUNCHER);
316         }
317     }
318 
319     @Override
320     public void showShellApp(ShellAppType type) {
321         // We don't trigger the shell apps via trinity icons and/or 1-3 buttons
322         // if there is a request being processed because it will cause another request (fired after transition is done)
323         // and eventually could lead to the location change race (so called Disco App effect).
324         if (!getConnection().hasActiveRequest()) {
325             doShowShellApp(type);
326         }
327     }
328 
329     @Override
330     public void updateCaption(ComponentConnector connector) {}
331 
332     @Override
333     public void onConnectorHierarchyChange(ConnectorHierarchyChangeEvent event) {
334         List<ComponentConnector> children = getChildComponents();
335         for (ComponentConnector connector : children) {
336             if (connector instanceof ViewportConnector) {
337                 final ViewportConnector vc = (ViewportConnector) connector;
338                 view.updateViewport(vc.getWidget(), vc.getType());
339                 vc.setEventBus(eventBus);
340             } else if (connector instanceof OverlayConnector) {
341                 if (!view.hasOverlay(connector.getWidget())) {
342                     final OverlayConnector oc = (OverlayConnector) connector;
343                     ComponentConnector overlayParent = (ComponentConnector) oc.getState().overlayParent;
344                     Widget parentWidget = overlayParent.getWidget();
345                     view.openOverlayOnWidget(oc.getWidget(), parentWidget);
346                 }
347             } else if (connector instanceof NativeButtonConnector) {
348                 view.setUserMenu(connector.getWidget());
349             }
350         }
351 
352         List<ComponentConnector> oldChildren = event.getOldChildren();
353         oldChildren.removeAll(children);
354         for (ComponentConnector cc : oldChildren) {
355             cc.getWidget().removeFromParent();
356         }
357     }
358 
359     @Override
360     protected Widget createWidget() {
361         this.view = new MagnoliaShellViewImpl();
362         this.view.setPresenter(this);
363         return view.asWidget();
364     }
365 
366     @Override
367     public MagnoliaShellState getState() {
368         return (MagnoliaShellState) super.getState();
369     }
370 
371     @Override
372     public void updateViewportLayout(ViewportWidget activeViewport) {
373         getLayoutManager().setNeedsMeasure(Util.findConnectorFor(activeViewport));
374         getLayoutManager().layoutNow();
375     }
376 
377 
378     @Override
379     public void initHistory() {
380         Scheduler.get().scheduleDeferred(new Scheduler.ScheduledCommand() {
381             @Override
382             public void execute() {
383                 if (!isHistoryInitialized) {
384                     isHistoryInitialized = true;
385                     History.fireCurrentHistoryState();
386                 }
387             }
388         });
389     }
390 
391 }