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.

VSlider.java 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573
  1. /*
  2. @VaadinApache2LicenseForJavaFiles@
  3. */
  4. //
  5. package com.vaadin.terminal.gwt.client.ui;
  6. import com.google.gwt.core.client.Scheduler;
  7. import com.google.gwt.core.client.Scheduler.ScheduledCommand;
  8. import com.google.gwt.event.dom.client.KeyCodes;
  9. import com.google.gwt.user.client.Command;
  10. import com.google.gwt.user.client.DOM;
  11. import com.google.gwt.user.client.Element;
  12. import com.google.gwt.user.client.Event;
  13. import com.google.gwt.user.client.Window;
  14. import com.google.gwt.user.client.ui.HTML;
  15. import com.google.gwt.user.client.ui.Widget;
  16. import com.vaadin.terminal.gwt.client.ApplicationConnection;
  17. import com.vaadin.terminal.gwt.client.BrowserInfo;
  18. import com.vaadin.terminal.gwt.client.ContainerResizedListener;
  19. import com.vaadin.terminal.gwt.client.VPaintableWidget;
  20. import com.vaadin.terminal.gwt.client.UIDL;
  21. import com.vaadin.terminal.gwt.client.Util;
  22. import com.vaadin.terminal.gwt.client.VConsole;
  23. public class VSlider extends SimpleFocusablePanel implements VPaintableWidget,
  24. Field, ContainerResizedListener {
  25. public static final String CLASSNAME = "v-slider";
  26. /**
  27. * Minimum size (width or height, depending on orientation) of the slider
  28. * base.
  29. */
  30. private static final int MIN_SIZE = 50;
  31. ApplicationConnection client;
  32. String id;
  33. private boolean immediate;
  34. private boolean disabled;
  35. private boolean readonly;
  36. private int acceleration = 1;
  37. private double min;
  38. private double max;
  39. private int resolution;
  40. private Double value;
  41. private boolean vertical;
  42. private final HTML feedback = new HTML("", false);
  43. private final VOverlay feedbackPopup = new VOverlay(true, false, true) {
  44. @Override
  45. public void show() {
  46. super.show();
  47. updateFeedbackPosition();
  48. }
  49. };
  50. /* DOM element for slider's base */
  51. private final Element base;
  52. private final int BASE_BORDER_WIDTH = 1;
  53. /* DOM element for slider's handle */
  54. private final Element handle;
  55. /* DOM element for decrement arrow */
  56. private final Element smaller;
  57. /* DOM element for increment arrow */
  58. private final Element bigger;
  59. /* Temporary dragging/animation variables */
  60. private boolean dragging = false;
  61. private VLazyExecutor delayedValueUpdater = new VLazyExecutor(100,
  62. new ScheduledCommand() {
  63. public void execute() {
  64. updateValueToServer();
  65. acceleration = 1;
  66. }
  67. });
  68. public VSlider() {
  69. super();
  70. base = DOM.createDiv();
  71. handle = DOM.createDiv();
  72. smaller = DOM.createDiv();
  73. bigger = DOM.createDiv();
  74. setStyleName(CLASSNAME);
  75. DOM.setElementProperty(base, "className", CLASSNAME + "-base");
  76. DOM.setElementProperty(handle, "className", CLASSNAME + "-handle");
  77. DOM.setElementProperty(smaller, "className", CLASSNAME + "-smaller");
  78. DOM.setElementProperty(bigger, "className", CLASSNAME + "-bigger");
  79. DOM.appendChild(getElement(), bigger);
  80. DOM.appendChild(getElement(), smaller);
  81. DOM.appendChild(getElement(), base);
  82. DOM.appendChild(base, handle);
  83. // Hide initially
  84. DOM.setStyleAttribute(smaller, "display", "none");
  85. DOM.setStyleAttribute(bigger, "display", "none");
  86. DOM.setStyleAttribute(handle, "visibility", "hidden");
  87. sinkEvents(Event.MOUSEEVENTS | Event.ONMOUSEWHEEL | Event.KEYEVENTS
  88. | Event.FOCUSEVENTS | Event.TOUCHEVENTS);
  89. feedbackPopup.addStyleName(CLASSNAME + "-feedback");
  90. feedbackPopup.setWidget(feedback);
  91. }
  92. public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
  93. this.client = client;
  94. id = uidl.getId();
  95. // Ensure correct implementation
  96. if (client.updateComponent(this, uidl, true)) {
  97. return;
  98. }
  99. immediate = uidl.getBooleanAttribute("immediate");
  100. disabled = uidl.getBooleanAttribute("disabled");
  101. readonly = uidl.getBooleanAttribute("readonly");
  102. vertical = uidl.hasAttribute("vertical");
  103. String style = "";
  104. if (uidl.hasAttribute("style")) {
  105. style = uidl.getStringAttribute("style");
  106. }
  107. if (vertical) {
  108. addStyleName(CLASSNAME + "-vertical");
  109. } else {
  110. removeStyleName(CLASSNAME + "-vertical");
  111. }
  112. min = uidl.getDoubleAttribute("min");
  113. max = uidl.getDoubleAttribute("max");
  114. resolution = uidl.getIntAttribute("resolution");
  115. value = new Double(uidl.getDoubleVariable("value"));
  116. setFeedbackValue(value);
  117. buildBase();
  118. if (!vertical) {
  119. // Draw handle with a delay to allow base to gain maximum width
  120. Scheduler.get().scheduleDeferred(new Command() {
  121. public void execute() {
  122. buildHandle();
  123. setValue(value, false);
  124. }
  125. });
  126. } else {
  127. buildHandle();
  128. setValue(value, false);
  129. }
  130. }
  131. private void setFeedbackValue(double value) {
  132. String currentValue = "" + value;
  133. if (resolution == 0) {
  134. currentValue = "" + new Double(value).intValue();
  135. }
  136. feedback.setText(currentValue);
  137. }
  138. private void updateFeedbackPosition() {
  139. if (vertical) {
  140. feedbackPopup.setPopupPosition(
  141. DOM.getAbsoluteLeft(handle) + handle.getOffsetWidth(),
  142. DOM.getAbsoluteTop(handle) + handle.getOffsetHeight() / 2
  143. - feedbackPopup.getOffsetHeight() / 2);
  144. } else {
  145. feedbackPopup.setPopupPosition(
  146. DOM.getAbsoluteLeft(handle) + handle.getOffsetWidth() / 2
  147. - feedbackPopup.getOffsetWidth() / 2,
  148. DOM.getAbsoluteTop(handle)
  149. - feedbackPopup.getOffsetHeight());
  150. }
  151. }
  152. private void buildBase() {
  153. final String styleAttribute = vertical ? "height" : "width";
  154. final String domProperty = vertical ? "offsetHeight" : "offsetWidth";
  155. final Element p = DOM.getParent(getElement());
  156. if (DOM.getElementPropertyInt(p, domProperty) > 50) {
  157. if (vertical) {
  158. setHeight();
  159. } else {
  160. DOM.setStyleAttribute(base, styleAttribute, "");
  161. }
  162. } else {
  163. // Set minimum size and adjust after all components have
  164. // (supposedly) been drawn completely.
  165. DOM.setStyleAttribute(base, styleAttribute, MIN_SIZE + "px");
  166. Scheduler.get().scheduleDeferred(new Command() {
  167. public void execute() {
  168. final Element p = DOM.getParent(getElement());
  169. if (DOM.getElementPropertyInt(p, domProperty) > (MIN_SIZE + 5)) {
  170. if (vertical) {
  171. setHeight();
  172. } else {
  173. DOM.setStyleAttribute(base, styleAttribute, "");
  174. }
  175. // Ensure correct position
  176. setValue(value, false);
  177. }
  178. }
  179. });
  180. }
  181. // TODO attach listeners for focusing and arrow keys
  182. }
  183. private void buildHandle() {
  184. final String handleAttribute = vertical ? "marginTop" : "marginLeft";
  185. DOM.setStyleAttribute(handle, handleAttribute, "0");
  186. // Restore visibility
  187. DOM.setStyleAttribute(handle, "visibility", "visible");
  188. }
  189. private void setValue(Double value, boolean updateToServer) {
  190. if (value == null) {
  191. return;
  192. }
  193. if (value < min) {
  194. value = min;
  195. } else if (value > max) {
  196. value = max;
  197. }
  198. // Update handle position
  199. final String styleAttribute = vertical ? "marginTop" : "marginLeft";
  200. final String domProperty = vertical ? "offsetHeight" : "offsetWidth";
  201. final int handleSize = Integer.parseInt(DOM.getElementProperty(handle,
  202. domProperty));
  203. final int baseSize = Integer.parseInt(DOM.getElementProperty(base,
  204. domProperty)) - (2 * BASE_BORDER_WIDTH);
  205. final int range = baseSize - handleSize;
  206. double v = value.doubleValue();
  207. // Round value to resolution
  208. if (resolution > 0) {
  209. v = Math.round(v * Math.pow(10, resolution));
  210. v = v / Math.pow(10, resolution);
  211. } else {
  212. v = Math.round(v);
  213. }
  214. final double valueRange = max - min;
  215. double p = 0;
  216. if (valueRange > 0) {
  217. p = range * ((v - min) / valueRange);
  218. }
  219. if (p < 0) {
  220. p = 0;
  221. }
  222. if (vertical) {
  223. p = range - p;
  224. }
  225. final double pos = p;
  226. DOM.setStyleAttribute(handle, styleAttribute, (Math.round(pos)) + "px");
  227. // Update value
  228. this.value = new Double(v);
  229. setFeedbackValue(v);
  230. if (updateToServer) {
  231. updateValueToServer();
  232. }
  233. }
  234. @Override
  235. public void onBrowserEvent(Event event) {
  236. if (disabled || readonly) {
  237. return;
  238. }
  239. final Element targ = DOM.eventGetTarget(event);
  240. if (DOM.eventGetType(event) == Event.ONMOUSEWHEEL) {
  241. processMouseWheelEvent(event);
  242. } else if (dragging || targ == handle) {
  243. processHandleEvent(event);
  244. } else if (targ == smaller) {
  245. decreaseValue(true);
  246. } else if (targ == bigger) {
  247. increaseValue(true);
  248. } else if (DOM.eventGetType(event) == Event.MOUSEEVENTS) {
  249. processBaseEvent(event);
  250. } else if ((BrowserInfo.get().isGecko() && DOM.eventGetType(event) == Event.ONKEYPRESS)
  251. || (!BrowserInfo.get().isGecko() && DOM.eventGetType(event) == Event.ONKEYDOWN)) {
  252. if (handleNavigation(event.getKeyCode(), event.getCtrlKey(),
  253. event.getShiftKey())) {
  254. feedbackPopup.show();
  255. delayedValueUpdater.trigger();
  256. DOM.eventPreventDefault(event);
  257. DOM.eventCancelBubble(event, true);
  258. }
  259. } else if (targ.equals(getElement())
  260. && DOM.eventGetType(event) == Event.ONFOCUS) {
  261. feedbackPopup.show();
  262. } else if (targ.equals(getElement())
  263. && DOM.eventGetType(event) == Event.ONBLUR) {
  264. feedbackPopup.hide();
  265. } else if (DOM.eventGetType(event) == Event.ONMOUSEDOWN) {
  266. feedbackPopup.show();
  267. }
  268. if (Util.isTouchEvent(event)) {
  269. event.preventDefault(); // avoid simulated events
  270. event.stopPropagation();
  271. }
  272. }
  273. private void processMouseWheelEvent(final Event event) {
  274. final int dir = DOM.eventGetMouseWheelVelocityY(event);
  275. if (dir < 0) {
  276. increaseValue(false);
  277. } else {
  278. decreaseValue(false);
  279. }
  280. delayedValueUpdater.trigger();
  281. DOM.eventPreventDefault(event);
  282. DOM.eventCancelBubble(event, true);
  283. }
  284. private void processHandleEvent(Event event) {
  285. switch (DOM.eventGetType(event)) {
  286. case Event.ONMOUSEDOWN:
  287. case Event.ONTOUCHSTART:
  288. if (!disabled && !readonly) {
  289. focus();
  290. feedbackPopup.show();
  291. dragging = true;
  292. DOM.setElementProperty(handle, "className", CLASSNAME
  293. + "-handle " + CLASSNAME + "-handle-active");
  294. DOM.setCapture(getElement());
  295. DOM.eventPreventDefault(event); // prevent selecting text
  296. DOM.eventCancelBubble(event, true);
  297. event.stopPropagation();
  298. VConsole.log("Slider move start");
  299. }
  300. break;
  301. case Event.ONMOUSEMOVE:
  302. case Event.ONTOUCHMOVE:
  303. if (dragging) {
  304. VConsole.log("Slider move");
  305. setValueByEvent(event, false);
  306. updateFeedbackPosition();
  307. event.stopPropagation();
  308. }
  309. break;
  310. case Event.ONTOUCHEND:
  311. feedbackPopup.hide();
  312. case Event.ONMOUSEUP:
  313. // feedbackPopup.hide();
  314. VConsole.log("Slider move end");
  315. dragging = false;
  316. DOM.setElementProperty(handle, "className", CLASSNAME + "-handle");
  317. DOM.releaseCapture(getElement());
  318. setValueByEvent(event, true);
  319. event.stopPropagation();
  320. break;
  321. default:
  322. break;
  323. }
  324. }
  325. private void processBaseEvent(Event event) {
  326. if (DOM.eventGetType(event) == Event.ONMOUSEDOWN) {
  327. if (!disabled && !readonly && !dragging) {
  328. setValueByEvent(event, true);
  329. DOM.eventCancelBubble(event, true);
  330. }
  331. } else if (DOM.eventGetType(event) == Event.ONMOUSEDOWN && dragging) {
  332. dragging = false;
  333. DOM.releaseCapture(getElement());
  334. setValueByEvent(event, true);
  335. }
  336. }
  337. private void decreaseValue(boolean updateToServer) {
  338. setValue(new Double(value.doubleValue() - Math.pow(10, -resolution)),
  339. updateToServer);
  340. }
  341. private void increaseValue(boolean updateToServer) {
  342. setValue(new Double(value.doubleValue() + Math.pow(10, -resolution)),
  343. updateToServer);
  344. }
  345. private void setValueByEvent(Event event, boolean updateToServer) {
  346. double v = min; // Fallback to min
  347. final int coord = getEventPosition(event);
  348. final int handleSize, baseSize, baseOffset;
  349. if (vertical) {
  350. handleSize = handle.getOffsetHeight();
  351. baseSize = base.getOffsetHeight();
  352. baseOffset = base.getAbsoluteTop() - Window.getScrollTop()
  353. - handleSize / 2;
  354. } else {
  355. handleSize = handle.getOffsetWidth();
  356. baseSize = base.getOffsetWidth();
  357. baseOffset = base.getAbsoluteLeft() - Window.getScrollLeft()
  358. + handleSize / 2;
  359. }
  360. if (vertical) {
  361. v = ((baseSize - (coord - baseOffset)) / (double) (baseSize - handleSize))
  362. * (max - min) + min;
  363. } else {
  364. v = ((coord - baseOffset) / (double) (baseSize - handleSize))
  365. * (max - min) + min;
  366. }
  367. if (v < min) {
  368. v = min;
  369. } else if (v > max) {
  370. v = max;
  371. }
  372. setValue(v, updateToServer);
  373. }
  374. /**
  375. * TODO consider extracting touches support to an impl class specific for
  376. * webkit (only browser that really supports touches).
  377. *
  378. * @param event
  379. * @return
  380. */
  381. protected int getEventPosition(Event event) {
  382. if (vertical) {
  383. return Util.getTouchOrMouseClientY(event);
  384. } else {
  385. return Util.getTouchOrMouseClientX(event);
  386. }
  387. }
  388. public void iLayout() {
  389. if (vertical) {
  390. setHeight();
  391. }
  392. // Update handle position
  393. setValue(value, false);
  394. }
  395. private void setHeight() {
  396. // Calculate decoration size
  397. DOM.setStyleAttribute(base, "height", "0");
  398. DOM.setStyleAttribute(base, "overflow", "hidden");
  399. int h = DOM.getElementPropertyInt(getElement(), "offsetHeight");
  400. if (h < MIN_SIZE) {
  401. h = MIN_SIZE;
  402. }
  403. DOM.setStyleAttribute(base, "height", h + "px");
  404. DOM.setStyleAttribute(base, "overflow", "");
  405. }
  406. private void updateValueToServer() {
  407. client.updateVariable(id, "value", value.doubleValue(), immediate);
  408. }
  409. /**
  410. * Handles the keyboard events handled by the Slider
  411. *
  412. * @param event
  413. * The keyboard event received
  414. * @return true iff the navigation event was handled
  415. */
  416. public boolean handleNavigation(int keycode, boolean ctrl, boolean shift) {
  417. // No support for ctrl moving
  418. if (ctrl) {
  419. return false;
  420. }
  421. if ((keycode == getNavigationUpKey() && vertical)
  422. || (keycode == getNavigationRightKey() && !vertical)) {
  423. if (shift) {
  424. for (int a = 0; a < acceleration; a++) {
  425. increaseValue(false);
  426. }
  427. acceleration++;
  428. } else {
  429. increaseValue(false);
  430. }
  431. return true;
  432. } else if (keycode == getNavigationDownKey() && vertical
  433. || (keycode == getNavigationLeftKey() && !vertical)) {
  434. if (shift) {
  435. for (int a = 0; a < acceleration; a++) {
  436. decreaseValue(false);
  437. }
  438. acceleration++;
  439. } else {
  440. decreaseValue(false);
  441. }
  442. return true;
  443. }
  444. return false;
  445. }
  446. /**
  447. * Get the key that increases the vertical slider. By default it is the up
  448. * arrow key but by overriding this you can change the key to whatever you
  449. * want.
  450. *
  451. * @return The keycode of the key
  452. */
  453. protected int getNavigationUpKey() {
  454. return KeyCodes.KEY_UP;
  455. }
  456. /**
  457. * Get the key that decreases the vertical slider. By default it is the down
  458. * arrow key but by overriding this you can change the key to whatever you
  459. * want.
  460. *
  461. * @return The keycode of the key
  462. */
  463. protected int getNavigationDownKey() {
  464. return KeyCodes.KEY_DOWN;
  465. }
  466. /**
  467. * Get the key that decreases the horizontal slider. By default it is the
  468. * left arrow key but by overriding this you can change the key to whatever
  469. * you want.
  470. *
  471. * @return The keycode of the key
  472. */
  473. protected int getNavigationLeftKey() {
  474. return KeyCodes.KEY_LEFT;
  475. }
  476. /**
  477. * Get the key that increases the horizontal slider. By default it is the
  478. * right arrow key but by overriding this you can change the key to whatever
  479. * you want.
  480. *
  481. * @return The keycode of the key
  482. */
  483. protected int getNavigationRightKey() {
  484. return KeyCodes.KEY_RIGHT;
  485. }
  486. public Widget getWidgetForPaintable() {
  487. return this;
  488. }
  489. }