You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

VNotification.java 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528
  1. /*
  2. * Copyright 2000-2013 Vaadin Ltd.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License"); you may not
  5. * use this file except in compliance with the License. You may obtain a copy of
  6. * the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  12. * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  13. * License for the specific language governing permissions and limitations under
  14. * the License.
  15. */
  16. package com.vaadin.client.ui;
  17. import java.util.ArrayList;
  18. import java.util.Date;
  19. import java.util.EventObject;
  20. import java.util.Iterator;
  21. import com.google.gwt.aria.client.Roles;
  22. import com.google.gwt.core.client.GWT;
  23. import com.google.gwt.event.dom.client.KeyCodes;
  24. import com.google.gwt.user.client.DOM;
  25. import com.google.gwt.user.client.Event;
  26. import com.google.gwt.user.client.Timer;
  27. import com.google.gwt.user.client.ui.FlowPanel;
  28. import com.google.gwt.user.client.ui.HTML;
  29. import com.google.gwt.user.client.ui.Label;
  30. import com.google.gwt.user.client.ui.Widget;
  31. import com.vaadin.client.ApplicationConnection;
  32. import com.vaadin.client.BrowserInfo;
  33. import com.vaadin.client.UIDL;
  34. import com.vaadin.client.Util;
  35. import com.vaadin.client.ui.aria.AriaHelper;
  36. import com.vaadin.shared.Position;
  37. import com.vaadin.shared.ui.ui.NotificationConfigurationBean;
  38. import com.vaadin.shared.ui.ui.NotificationConfigurationBean.Role;
  39. import com.vaadin.shared.ui.ui.UIConstants;
  40. public class VNotification extends VOverlay {
  41. public static final Position CENTERED = Position.MIDDLE_CENTER;
  42. public static final Position CENTERED_TOP = Position.TOP_CENTER;
  43. public static final Position CENTERED_BOTTOM = Position.BOTTOM_CENTER;
  44. public static final Position TOP_LEFT = Position.TOP_LEFT;
  45. public static final Position TOP_RIGHT = Position.TOP_RIGHT;
  46. public static final Position BOTTOM_LEFT = Position.BOTTOM_LEFT;
  47. public static final Position BOTTOM_RIGHT = Position.BOTTOM_RIGHT;
  48. /**
  49. * Position that is only accessible for assistive devices, invisible for
  50. * visual users.
  51. */
  52. public static final Position ASSISTIVE = Position.ASSISTIVE;
  53. public static final int DELAY_FOREVER = -1;
  54. public static final int DELAY_NONE = 0;
  55. private static final String STYLENAME = "v-Notification";
  56. private static final int mouseMoveThreshold = 7;
  57. private static final int Z_INDEX_BASE = 20000;
  58. public static final String STYLE_SYSTEM = "system";
  59. private static final int FADE_ANIMATION_INTERVAL = 50; // == 20 fps
  60. private static final ArrayList<VNotification> notifications = new ArrayList<VNotification>();
  61. private int startOpacity = 90;
  62. private int fadeMsec = 400;
  63. private int delayMsec = 1000;
  64. private Timer fader;
  65. private Timer delay;
  66. private int x = -1;
  67. private int y = -1;
  68. private String temporaryStyle;
  69. private ArrayList<EventListener> listeners;
  70. private static final int TOUCH_DEVICE_IDLE_DELAY = 1000;
  71. /**
  72. * Default constructor. You should use GWT.create instead.
  73. */
  74. public VNotification() {
  75. setStyleName(STYLENAME);
  76. sinkEvents(Event.ONCLICK);
  77. DOM.setStyleAttribute(getElement(), "zIndex", "" + Z_INDEX_BASE);
  78. }
  79. /**
  80. * @deprecated Use static {@link #createNotification(int)} instead to enable
  81. * GWT deferred binding.
  82. *
  83. * @param delayMsec
  84. */
  85. @Deprecated
  86. public VNotification(int delayMsec) {
  87. this();
  88. this.delayMsec = delayMsec;
  89. if (BrowserInfo.get().isTouchDevice()) {
  90. new Timer() {
  91. @Override
  92. public void run() {
  93. if (isAttached()) {
  94. fade();
  95. }
  96. }
  97. }.schedule(delayMsec + TOUCH_DEVICE_IDLE_DELAY);
  98. }
  99. }
  100. /**
  101. * @deprecated Use static {@link #createNotification(int, int, int)} instead
  102. * to enable GWT deferred binding.
  103. *
  104. * @param delayMsec
  105. * @param fadeMsec
  106. * @param startOpacity
  107. */
  108. @Deprecated
  109. public VNotification(int delayMsec, int fadeMsec, int startOpacity) {
  110. this(delayMsec);
  111. this.fadeMsec = fadeMsec;
  112. this.startOpacity = startOpacity;
  113. }
  114. public void startDelay() {
  115. DOM.removeEventPreview(this);
  116. if (delayMsec > 0) {
  117. if (delay == null) {
  118. delay = new Timer() {
  119. @Override
  120. public void run() {
  121. fade();
  122. }
  123. };
  124. delay.schedule(delayMsec);
  125. }
  126. } else if (delayMsec == 0) {
  127. fade();
  128. }
  129. }
  130. @Override
  131. public void show() {
  132. show(CENTERED);
  133. }
  134. public void show(String style) {
  135. show(CENTERED, style);
  136. }
  137. public void show(com.vaadin.shared.Position position) {
  138. show(position, null);
  139. }
  140. public void show(Widget widget, Position position, String style) {
  141. NotificationConfigurationBean styleSetup = getUiState(style);
  142. setWaiAriaRole(styleSetup);
  143. FlowPanel panel = new FlowPanel();
  144. if (styleSetup.hasAssistivePrefix()) {
  145. panel.add(new Label(styleSetup.getAssistivePrefix()));
  146. AriaHelper.setVisibleForAssistiveDevicesOnly(panel.getElement(),
  147. true);
  148. }
  149. panel.add(widget);
  150. if (styleSetup.hasAssistivePostfix()) {
  151. panel.add(new Label(styleSetup.getAssistivePostfix()));
  152. AriaHelper.setVisibleForAssistiveDevicesOnly(panel.getElement(),
  153. true);
  154. }
  155. setWidget(panel);
  156. show(position, style);
  157. }
  158. public void show(String html, Position position, String style) {
  159. NotificationConfigurationBean styleSetup = getUiState(style);
  160. String assistiveDeviceOnlyStyle = AriaHelper.ASSISTIVE_DEVICE_ONLY_STYLE;
  161. setWaiAriaRole(styleSetup);
  162. String type = "";
  163. String usage = "";
  164. if (styleSetup != null && styleSetup.hasAssistivePrefix()) {
  165. type = "<span class='" + assistiveDeviceOnlyStyle + "'>"
  166. + styleSetup.getAssistivePrefix() + "</span>";
  167. }
  168. if (styleSetup != null && styleSetup.hasAssistivePostfix()) {
  169. usage = "<span class='" + assistiveDeviceOnlyStyle + "'>"
  170. + styleSetup.getAssistivePostfix() + "</span>";
  171. }
  172. setWidget(new HTML(type + html + usage));
  173. show(position, style);
  174. }
  175. private NotificationConfigurationBean getUiState(String style) {
  176. NotificationConfigurationBean styleSetup = getApplicationConnection()
  177. .getUIConnector().getState().notificationConfiguration.setup
  178. .get(style);
  179. return styleSetup;
  180. }
  181. private void setWaiAriaRole(NotificationConfigurationBean styleSetup) {
  182. Roles.getAlertRole().set(getElement());
  183. if (styleSetup != null && styleSetup.getAssistiveRole() != null) {
  184. if (Role.STATUS == styleSetup.getAssistiveRole()) {
  185. Roles.getStatusRole().set(getElement());
  186. }
  187. }
  188. }
  189. public void show(Position position, String style) {
  190. setOpacity(getElement(), startOpacity);
  191. if (style != null) {
  192. temporaryStyle = style;
  193. addStyleName(style);
  194. addStyleDependentName(style);
  195. }
  196. super.show();
  197. notifications.add(this);
  198. setPosition(position);
  199. positionOrSizeUpdated();
  200. /**
  201. * Android 4 fails to render notifications correctly without a little
  202. * nudge (#8551)
  203. */
  204. if (BrowserInfo.get().isAndroid()) {
  205. Util.setStyleTemporarily(getElement(), "display", "none");
  206. }
  207. }
  208. @Override
  209. public void hide() {
  210. DOM.removeEventPreview(this);
  211. cancelDelay();
  212. cancelFade();
  213. if (temporaryStyle != null) {
  214. removeStyleName(temporaryStyle);
  215. removeStyleDependentName(temporaryStyle);
  216. temporaryStyle = null;
  217. }
  218. super.hide();
  219. notifications.remove(this);
  220. fireEvent(new HideEvent(this));
  221. }
  222. public void fade() {
  223. DOM.removeEventPreview(this);
  224. cancelDelay();
  225. if (fader == null) {
  226. fader = new Timer() {
  227. private final long start = new Date().getTime();
  228. @Override
  229. public void run() {
  230. /*
  231. * To make animation smooth, don't count that event happens
  232. * on time. Reduce opacity according to the actual time
  233. * spent instead of fixed decrement.
  234. */
  235. long now = new Date().getTime();
  236. long timeEplaced = now - start;
  237. float remainingFraction = 1 - timeEplaced
  238. / (float) fadeMsec;
  239. int opacity = (int) (startOpacity * remainingFraction);
  240. if (opacity <= 0) {
  241. cancel();
  242. hide();
  243. } else {
  244. setOpacity(getElement(), opacity);
  245. }
  246. }
  247. };
  248. fader.scheduleRepeating(FADE_ANIMATION_INTERVAL);
  249. }
  250. }
  251. public void setPosition(com.vaadin.shared.Position position) {
  252. final com.google.gwt.user.client.Element el = getElement();
  253. DOM.setStyleAttribute(el, "top", "");
  254. DOM.setStyleAttribute(el, "left", "");
  255. DOM.setStyleAttribute(el, "bottom", "");
  256. DOM.setStyleAttribute(el, "right", "");
  257. switch (position) {
  258. case TOP_LEFT:
  259. DOM.setStyleAttribute(el, "top", "0px");
  260. DOM.setStyleAttribute(el, "left", "0px");
  261. break;
  262. case TOP_RIGHT:
  263. DOM.setStyleAttribute(el, "top", "0px");
  264. DOM.setStyleAttribute(el, "right", "0px");
  265. break;
  266. case BOTTOM_RIGHT:
  267. DOM.setStyleAttribute(el, "position", "absolute");
  268. DOM.setStyleAttribute(el, "bottom", "0px");
  269. DOM.setStyleAttribute(el, "right", "0px");
  270. break;
  271. case BOTTOM_LEFT:
  272. DOM.setStyleAttribute(el, "bottom", "0px");
  273. DOM.setStyleAttribute(el, "left", "0px");
  274. break;
  275. case TOP_CENTER:
  276. center();
  277. DOM.setStyleAttribute(el, "top", "0px");
  278. break;
  279. case BOTTOM_CENTER:
  280. center();
  281. DOM.setStyleAttribute(el, "top", "");
  282. DOM.setStyleAttribute(el, "bottom", "0px");
  283. break;
  284. case ASSISTIVE:
  285. DOM.setStyleAttribute(el, "top", "-2000px");
  286. DOM.setStyleAttribute(el, "left", "-2000px");
  287. break;
  288. default:
  289. case MIDDLE_CENTER:
  290. center();
  291. break;
  292. }
  293. }
  294. private void cancelFade() {
  295. if (fader != null) {
  296. fader.cancel();
  297. fader = null;
  298. }
  299. }
  300. private void cancelDelay() {
  301. if (delay != null) {
  302. delay.cancel();
  303. delay = null;
  304. }
  305. }
  306. private void setOpacity(com.google.gwt.user.client.Element el, int opacity) {
  307. DOM.setStyleAttribute(el, "opacity", "" + (opacity / 100.0));
  308. if (BrowserInfo.get().isIE()) {
  309. DOM.setStyleAttribute(el, "filter", "Alpha(opacity=" + opacity
  310. + ")");
  311. }
  312. }
  313. @Override
  314. public void onBrowserEvent(Event event) {
  315. DOM.removeEventPreview(this);
  316. if (fader == null) {
  317. fade();
  318. }
  319. }
  320. @Override
  321. public boolean onEventPreview(Event event) {
  322. int type = DOM.eventGetType(event);
  323. // "modal"
  324. if (delayMsec == -1 || temporaryStyle == STYLE_SYSTEM) {
  325. if (type == Event.ONCLICK) {
  326. if (DOM.isOrHasChild(getElement(), DOM.eventGetTarget(event))) {
  327. fade();
  328. return false;
  329. }
  330. } else if (type == Event.ONKEYDOWN
  331. && event.getKeyCode() == KeyCodes.KEY_ESCAPE) {
  332. fade();
  333. return false;
  334. }
  335. if (temporaryStyle == STYLE_SYSTEM) {
  336. return true;
  337. } else {
  338. return false;
  339. }
  340. }
  341. // default
  342. switch (type) {
  343. case Event.ONMOUSEMOVE:
  344. if (x < 0) {
  345. x = DOM.eventGetClientX(event);
  346. y = DOM.eventGetClientY(event);
  347. } else if (Math.abs(DOM.eventGetClientX(event) - x) > mouseMoveThreshold
  348. || Math.abs(DOM.eventGetClientY(event) - y) > mouseMoveThreshold) {
  349. startDelay();
  350. }
  351. break;
  352. case Event.ONMOUSEDOWN:
  353. case Event.ONMOUSEWHEEL:
  354. case Event.ONSCROLL:
  355. startDelay();
  356. break;
  357. case Event.ONKEYDOWN:
  358. if (event.getRepeat()) {
  359. return true;
  360. }
  361. startDelay();
  362. break;
  363. default:
  364. break;
  365. }
  366. return true;
  367. }
  368. public void addEventListener(EventListener listener) {
  369. if (listeners == null) {
  370. listeners = new ArrayList<EventListener>();
  371. }
  372. listeners.add(listener);
  373. }
  374. public void removeEventListener(EventListener listener) {
  375. if (listeners == null) {
  376. return;
  377. }
  378. listeners.remove(listener);
  379. }
  380. private void fireEvent(HideEvent event) {
  381. if (listeners != null) {
  382. for (Iterator<EventListener> it = listeners.iterator(); it
  383. .hasNext();) {
  384. EventListener l = it.next();
  385. l.notificationHidden(event);
  386. }
  387. }
  388. }
  389. public static void showNotification(ApplicationConnection client,
  390. final UIDL notification) {
  391. boolean onlyPlainText = notification
  392. .hasAttribute(UIConstants.NOTIFICATION_HTML_CONTENT_NOT_ALLOWED);
  393. String html = "";
  394. if (notification.hasAttribute(UIConstants.ATTRIBUTE_NOTIFICATION_ICON)) {
  395. String iconUri = notification
  396. .getStringAttribute(UIConstants.ATTRIBUTE_NOTIFICATION_ICON);
  397. html += client.getIcon(iconUri).getElement().getString();
  398. }
  399. if (notification
  400. .hasAttribute(UIConstants.ATTRIBUTE_NOTIFICATION_CAPTION)) {
  401. String caption = notification
  402. .getStringAttribute(UIConstants.ATTRIBUTE_NOTIFICATION_CAPTION);
  403. if (onlyPlainText) {
  404. caption = Util.escapeHTML(caption);
  405. caption = caption.replaceAll("\\n", "<br />");
  406. }
  407. html += "<h1>" + caption + "</h1>";
  408. }
  409. if (notification
  410. .hasAttribute(UIConstants.ATTRIBUTE_NOTIFICATION_MESSAGE)) {
  411. String message = notification
  412. .getStringAttribute(UIConstants.ATTRIBUTE_NOTIFICATION_MESSAGE);
  413. if (onlyPlainText) {
  414. message = Util.escapeHTML(message);
  415. message = message.replaceAll("\\n", "<br />");
  416. }
  417. html += "<p>" + message + "</p>";
  418. }
  419. final String style = notification
  420. .hasAttribute(UIConstants.ATTRIBUTE_NOTIFICATION_STYLE) ? notification
  421. .getStringAttribute(UIConstants.ATTRIBUTE_NOTIFICATION_STYLE)
  422. : null;
  423. final int pos = notification
  424. .getIntAttribute(UIConstants.ATTRIBUTE_NOTIFICATION_POSITION);
  425. Position position = Position.values()[pos];
  426. final int delay = notification
  427. .getIntAttribute(UIConstants.ATTRIBUTE_NOTIFICATION_DELAY);
  428. createNotification(delay, client.getUIConnector().getWidget()).show(
  429. html, position, style);
  430. }
  431. public static VNotification createNotification(int delayMsec, Widget owner) {
  432. final VNotification notification = GWT.create(VNotification.class);
  433. notification.setWaiAriaRole(null);
  434. notification.delayMsec = delayMsec;
  435. if (BrowserInfo.get().isTouchDevice()) {
  436. new Timer() {
  437. @Override
  438. public void run() {
  439. if (notification.isAttached()) {
  440. notification.fade();
  441. }
  442. }
  443. }.schedule(notification.delayMsec + TOUCH_DEVICE_IDLE_DELAY);
  444. }
  445. notification.setOwner(owner);
  446. return notification;
  447. }
  448. public class HideEvent extends EventObject {
  449. public HideEvent(Object source) {
  450. super(source);
  451. }
  452. }
  453. public interface EventListener extends java.util.EventListener {
  454. public void notificationHidden(HideEvent event);
  455. }
  456. /**
  457. * Moves currently visible notifications to the top of the event preview
  458. * stack. Can be called when opening other overlays such as subwindows to
  459. * ensure the notifications receive the events they need and don't linger
  460. * indefinitely. See #7136.
  461. *
  462. * TODO Should this be a generic Overlay feature instead?
  463. */
  464. public static void bringNotificationsToFront() {
  465. for (VNotification notification : notifications) {
  466. DOM.removeEventPreview(notification);
  467. DOM.addEventPreview(notification);
  468. }
  469. }
  470. }