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