View Javadoc
1   /**
2    * This file Copyright (c) 2011-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.jquerywrapper.JQueryCallback;
37  import info.magnolia.ui.vaadin.gwt.client.jquerywrapper.JQueryWrapper;
38  import info.magnolia.ui.vaadin.gwt.client.magnoliashell.ShellState;
39  import info.magnolia.ui.vaadin.gwt.client.magnoliashell.shellmessage.ShellMessageWidget;
40  import info.magnolia.ui.vaadin.gwt.client.magnoliashell.shellmessage.ShellMessageWidget.MessageType;
41  import info.magnolia.ui.vaadin.gwt.client.magnoliashell.shellmessage.VInfoMessage;
42  import info.magnolia.ui.vaadin.gwt.client.magnoliashell.shellmessage.VShellErrorMessage;
43  import info.magnolia.ui.vaadin.gwt.client.magnoliashell.shellmessage.VWarningMessage;
44  import info.magnolia.ui.vaadin.gwt.client.magnoliashell.viewport.animation.JQueryAnimation;
45  import info.magnolia.ui.vaadin.gwt.client.magnoliashell.viewport.widget.AppsViewportWidget;
46  import info.magnolia.ui.vaadin.gwt.client.magnoliashell.viewport.widget.ViewportWidget;
47  import info.magnolia.ui.vaadin.gwt.client.shared.magnoliashell.Fragment;
48  import info.magnolia.ui.vaadin.gwt.client.shared.magnoliashell.ShellAppType;
49  import info.magnolia.ui.vaadin.gwt.client.shared.magnoliashell.ViewportType;
50  
51  import java.util.EnumMap;
52  import java.util.Map;
53  
54  import com.google.gwt.core.client.Scheduler;
55  import com.google.gwt.core.client.Scheduler.ScheduledCommand;
56  import com.google.gwt.event.dom.client.KeyCodes;
57  import com.google.gwt.event.dom.client.KeyPressEvent;
58  import com.google.gwt.event.dom.client.KeyPressHandler;
59  import com.google.gwt.regexp.shared.RegExp;
60  import com.google.gwt.user.client.DOM;
61  import com.google.gwt.user.client.Element;
62  import com.google.gwt.user.client.Window;
63  import com.google.gwt.user.client.ui.FocusPanel;
64  import com.google.gwt.user.client.ui.RootPanel;
65  import com.google.gwt.user.client.ui.Widget;
66  import com.googlecode.mgwt.ui.client.widget.touch.TouchPanel;
67  
68  /**
69   * GWT implementation of MagnoliaShell client side (the view part basically).
70   */
71  public class MagnoliaShellViewImpl extends TouchPanel implements MagnoliaShellView {
72  
73      public static final String CLASS_NAME = "v-magnolia-shell";
74  
75      public static final String VIEWPORT_SLOT_CLASS_NAME = "v-shell-viewport-slot";
76  
77      private final Map<ViewportType, ViewportWidget> viewports = new EnumMap<ViewportType, ViewportWidget>(ViewportType.class);
78  
79      private final ShellAppLauncher mainAppLauncher;
80  
81      private ShellMessageWidget lowPriorityMessage;
82  
83      private ShellMessageWidget hiPriorityMessage;
84  
85      private Presenter presenter;
86  
87      private JQueryAnimation viewportShifter = new JQueryAnimation();
88  
89      private final Element viewportSlot = DOM.createDiv();
90  
91      // notice that also a fragment like "#app:groovy:;" (with no subapp id, i.e. console)
92      // can represent the console and must be taken into account
93      private static final RegExp GROOVY_CONSOLE_URL_FRAGMENT_REGEX = RegExp.compile("(app:groovy:)(console|;)");
94  
95      // To remove focus (blur) all input elements, one just needs to focus this element.
96      private final FocusPanel blurHelper = new FocusPanel();
97  
98      public MagnoliaShellViewImpl() {
99          super();
100         this.mainAppLauncher = new ShellAppLauncher();
101         getElement().setClassName(CLASS_NAME);
102         viewportSlot.setClassName(VIEWPORT_SLOT_CLASS_NAME);
103         add(mainAppLauncher, getElement());
104 
105 
106         getElement().appendChild(viewportSlot);
107         viewportShifter.addCallback(new JQueryCallback() {
108             @Override
109             public void execute(JQueryWrapper query) {
110                 presenter.updateViewportLayout(appViewport());
111             }
112         });
113 
114         initKeyboardShortcutSupport();
115     }
116 
117 
118     /**
119      * Bind keyboard handlers.
120      * These commands are only processed if an input area does not have focus.
121      */
122     protected void initKeyboardShortcutSupport() {
123 
124         add(blurHelper, getElement());
125 
126         KeyPressHandler keyboardShortcutHandler = new KeyPressHandler() {
127 
128             @Override
129             public void onKeyPress(KeyPressEvent event) {
130 
131                 // Only process keyboard shortcuts if user is not in an input field.
132                 if (isFocusedElementAnInputField()) {
133                     return;
134                 }
135 
136                 // Only process if no modifier keys are held down to avoid collision with OS or Browser hotkeys.
137                 if (event.isAnyModifierKeyDown()) {
138                     return;
139                 }
140 
141                 if (isAppInFocusGroovyConsole()) {
142                     return;
143                 }
144 
145                 char c = event.getCharCode();
146 
147                 switch (c) {
148 
149                 // Shell Apps
150                 case '1':
151                     mainAppLauncher.toggleShellApp(ShellAppType.APPLAUNCHER);
152                     break;
153                 case '2':
154                     mainAppLauncher.toggleShellApp(ShellAppType.PULSE);
155                     break;
156                 case '3':
157                     mainAppLauncher.toggleShellApp(ShellAppType.FAVORITE);
158                     break;
159 
160                 // App Stack Navigation.
161                 case '9':
162                     // We have more than one app open
163                     if (appViewport().readyForAppSwipeOrShortcutNavigation()) {
164                         ShellState.get().setAppStarting();
165                         appViewport().goToPreviousApp();
166                     }
167                     break;
168                 case '0':
169                     // We have more than one app open
170                     if (appViewport().readyForAppSwipeOrShortcutNavigation()) {
171                         ShellState.get().setAppStarting();
172                         appViewport().goToNextApp();
173                     }
174                     break;
175 
176                 default:
177                     // Nothing
178                 }
179             }
180         };
181         RootPanel.get().addDomHandler(keyboardShortcutHandler, KeyPressEvent.getType());
182 
183 
184         /**
185          * Pressing the escape key causes all elements to loose focus.
186          * This is a handy way to be able to start using the single-key keyboard shortcuts.
187          */
188         KeyPressHandler escapeKeyPressHandler = new KeyPressHandler() {
189             @Override
190             public void onKeyPress(KeyPressEvent event) {
191                 int code = event.getNativeEvent().getKeyCode();
192                 if (code == KeyCodes.KEY_ESCAPE) {
193                     blurHelper.getElement().focus();
194                 }
195             }
196         };
197         RootPanel.get().addDomHandler(escapeKeyPressHandler, KeyPressEvent.getType());
198     }
199 
200     /**
201      * Returns whether the currently focused element is one that accepts keyboard input.
202      */
203     protected boolean isFocusedElementAnInputField() {
204         Element focused = elementInFocus(RootPanel.get().getElement());
205         String tagName = focused.getTagName();
206 
207         if ("input".equalsIgnoreCase(tagName) || "select".equalsIgnoreCase(tagName) || "textarea".equalsIgnoreCase(tagName)) {
208             return true;
209         }
210         return false;
211     }
212 
213     protected native Element elementInFocus(Element element) /*-{
214         return element.ownerDocument.activeElement;
215     }-*/;
216 
217 
218     protected AppsViewportWidget appViewport() {
219         return (AppsViewportWidget) viewports.get(ViewportType.APP);
220     }
221 
222     protected void replaceWidget(final Widget oldWidget, final Widget newWidget) {
223         if (oldWidget != newWidget) {
224             if (oldWidget != null) {
225                 remove(oldWidget);
226             }
227         }
228         if (getWidgetIndex(newWidget) < 0) {
229             add(newWidget, viewportSlot);
230         }
231     }
232 
233     @Override
234     public void setPresenter(Presenter presenter) {
235         this.presenter = presenter;
236         mainAppLauncher.setListener(presenter);
237     }
238 
239     @Override
240     public void showMessage(MessageType type, String topic, String message, String id) {
241         final ShellMessageWidget msg;
242         switch (type) {
243         case WARNING:
244             msg = new VWarningMessage(this, topic, message, id);
245             if (lowPriorityMessage != null && getWidgetIndex(lowPriorityMessage) != -1) {
246                 lowPriorityMessage.hide();
247             }
248             lowPriorityMessage = msg;
249             break;
250         case INFO:
251             msg = new VInfoMessage(this, topic, message, id);
252             if (lowPriorityMessage != null && getWidgetIndex(lowPriorityMessage) != -1) {
253                 lowPriorityMessage.hide();
254             }
255             lowPriorityMessage = msg;
256             break;
257         case ERROR:
258             msg = new VShellErrorMessage(this, topic, message, id);
259             if (hiPriorityMessage != null && getWidgetIndex(hiPriorityMessage) != -1) {
260                 hiPriorityMessage.hide();
261             }
262             hiPriorityMessage = msg;
263             break;
264         default:
265             msg = null;
266         }
267         Scheduler.get().scheduleDeferred(new ScheduledCommand() {
268 
269             @Override
270             public void execute() {
271                 if (msg != null) {
272                     add(msg, getElement());
273                 }
274             }
275         });
276     }
277 
278     @Override
279     public void hideAllMessages() {
280         if (hiPriorityMessage != null && getWidgetIndex(hiPriorityMessage) != -1) {
281             hiPriorityMessage.hideWithoutTransition();
282         }
283         if (lowPriorityMessage != null && getWidgetIndex(lowPriorityMessage) != -1) {
284             lowPriorityMessage.hideWithoutTransition();
285         }
286 
287         hiPriorityMessage = null;
288         lowPriorityMessage = null;
289     }
290 
291     @Override
292     public void updateViewport(ViewportWidget viewport, ViewportType type) {
293         ViewportWidget oldViewport = viewports.get(type);
294         if (oldViewport != viewport) {
295             replaceWidget(oldViewport, viewport);
296             viewports.put(type, viewport);
297         }
298     }
299 
300     @Override
301     public void shiftViewportsVertically(int shiftPx, boolean animated) {
302         viewportShifter.setProperty("top", mainAppLauncher.getOffsetHeight() + shiftPx);
303         if (shiftPx == 0 || shiftPx == 60) {
304             viewportShifter.clearTopAfterThisAnimation();
305         }
306         viewportShifter.run(animated ? 300 : 0, viewportSlot);
307     }
308 
309     @Override
310     public void setShellAppIndication(ShellAppType type, int indication) {
311         mainAppLauncher.setIndication(type, indication);
312     }
313 
314     @Override
315     public void closeMessageEager(final String id) {
316         presenter.removeMessage(id);
317     }
318 
319     @Override
320     public void navigateToMessageDetails(String id) {
321         presenter.activateShellApp(Fragment.fromString("shell:pulse:messages/" + id));
322     }
323 
324     @Override
325     public void updateShellDivet() {
326         mainAppLauncher.updateDivet();
327     }
328 
329     @Override
330     public void openOverlayOnWidget(Widget overlayWidget, Widget overlayParent) {
331         add(overlayWidget, overlayParent.getElement());
332     }
333 
334     @Override
335     public void onShellAppStarting(ShellAppType type) {
336         mainAppLauncher.activateControl(type);
337     }
338 
339     @Override
340     public void onAppStarting() {
341         mainAppLauncher.deactivateControls();
342     }
343 
344     @Override
345     public void setUserMenu(Widget widget) {
346         mainAppLauncher.setUserMenu(widget);
347     }
348 
349     @Override
350     public void onLoad() {
351         super.onLoad();
352         presenter.initHistory();
353     }
354 
355     @Override
356     public boolean hasOverlay(Widget widget) {
357         return getWidgetIndex(widget) != -1;
358     }
359 
360     /*
361      * If the current app in focus is groovy and skip the shell apps shortcuts. See http://jira.magnolia-cms.com/browse/MGNLUI-3526
362      */
363     private boolean isAppInFocusGroovyConsole() {
364         String hash = Window.Location.getHash();
365         return GROOVY_CONSOLE_URL_FRAGMENT_REGEX.test(hash);
366     }
367 
368 }