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