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 21KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687
  1. /*
  2. * Copyright 2000-2018 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 com.google.gwt.core.client.Scheduler;
  18. import com.google.gwt.dom.client.Element;
  19. import com.google.gwt.dom.client.Style.Display;
  20. import com.google.gwt.dom.client.Style.Overflow;
  21. import com.google.gwt.dom.client.Style.Unit;
  22. import com.google.gwt.event.dom.client.KeyCodes;
  23. import com.google.gwt.event.logical.shared.ValueChangeEvent;
  24. import com.google.gwt.event.logical.shared.ValueChangeHandler;
  25. import com.google.gwt.event.shared.HandlerRegistration;
  26. import com.google.gwt.user.client.Command;
  27. import com.google.gwt.user.client.DOM;
  28. import com.google.gwt.user.client.Event;
  29. import com.google.gwt.user.client.Window;
  30. import com.google.gwt.user.client.ui.HTML;
  31. import com.google.gwt.user.client.ui.HasValue;
  32. import com.vaadin.client.ApplicationConnection;
  33. import com.vaadin.client.BrowserInfo;
  34. import com.vaadin.client.WidgetUtil;
  35. import com.vaadin.shared.ui.slider.SliderOrientation;
  36. public class VSlider extends SimpleFocusablePanel
  37. implements Field, HasValue<Double>, SubPartAware {
  38. public static final String CLASSNAME = "v-slider";
  39. /**
  40. * Minimum size (width or height, depending on orientation) of the slider
  41. * base.
  42. */
  43. private static final int MIN_SIZE = 50;
  44. protected ApplicationConnection client;
  45. protected String id;
  46. protected boolean disabled;
  47. protected boolean readonly;
  48. private int acceleration = 1;
  49. protected double min;
  50. protected double max;
  51. protected int resolution;
  52. protected Double value;
  53. private boolean updateValueOnClick;
  54. protected SliderOrientation orientation = SliderOrientation.HORIZONTAL;
  55. private final HTML feedback = new HTML("", false);
  56. private final VOverlay feedbackPopup = new VOverlay(true, false) {
  57. {
  58. setOwner(VSlider.this);
  59. }
  60. @Override
  61. public void show() {
  62. super.show();
  63. updateFeedbackPosition();
  64. }
  65. };
  66. /* DOM element for slider's base */
  67. private final Element base;
  68. private static final int BASE_BORDER_WIDTH = 1;
  69. /* DOM element for slider's handle */
  70. private final Element handle;
  71. /* DOM element for decrement arrow */
  72. private final Element smaller;
  73. /* DOM element for increment arrow */
  74. private final Element bigger;
  75. /* Temporary dragging/animation variables */
  76. private boolean dragging = false;
  77. private VLazyExecutor delayedValueUpdater = new VLazyExecutor(100, () -> {
  78. fireValueChanged();
  79. acceleration = 1;
  80. });
  81. public VSlider() {
  82. super();
  83. base = DOM.createDiv();
  84. handle = DOM.createDiv();
  85. smaller = DOM.createDiv();
  86. bigger = DOM.createDiv();
  87. setStyleName(CLASSNAME);
  88. getElement().appendChild(bigger);
  89. getElement().appendChild(smaller);
  90. getElement().appendChild(base);
  91. base.appendChild(handle);
  92. // Hide initially
  93. smaller.getStyle().setDisplay(Display.NONE);
  94. bigger.getStyle().setDisplay(Display.NONE);
  95. sinkEvents(Event.MOUSEEVENTS | Event.ONMOUSEWHEEL | Event.KEYEVENTS
  96. | Event.FOCUSEVENTS | Event.TOUCHEVENTS);
  97. feedbackPopup.setWidget(feedback);
  98. }
  99. @Override
  100. public void setStyleName(String style) {
  101. updateStyleNames(style, false);
  102. }
  103. @Override
  104. public void setStylePrimaryName(String style) {
  105. updateStyleNames(style, true);
  106. }
  107. protected void updateStyleNames(String styleName,
  108. boolean isPrimaryStyleName) {
  109. feedbackPopup.removeStyleName(getStylePrimaryName() + "-feedback");
  110. removeStyleName(getStylePrimaryName() + "-vertical");
  111. if (isPrimaryStyleName) {
  112. super.setStylePrimaryName(styleName);
  113. } else {
  114. super.setStyleName(styleName);
  115. }
  116. feedbackPopup.addStyleName(getStylePrimaryName() + "-feedback");
  117. base.setClassName(getStylePrimaryName() + "-base");
  118. handle.setClassName(getStylePrimaryName() + "-handle");
  119. smaller.setClassName(getStylePrimaryName() + "-smaller");
  120. bigger.setClassName(getStylePrimaryName() + "-bigger");
  121. if (isVertical()) {
  122. addStyleName(getStylePrimaryName() + "-vertical");
  123. }
  124. }
  125. public void setFeedbackValue(double value) {
  126. feedback.setText(String.valueOf(value));
  127. }
  128. private void updateFeedbackPosition() {
  129. if (isVertical()) {
  130. feedbackPopup.setPopupPosition(
  131. handle.getAbsoluteLeft() + handle.getOffsetWidth(),
  132. handle.getAbsoluteTop() + handle.getOffsetHeight() / 2
  133. - feedbackPopup.getOffsetHeight() / 2);
  134. } else {
  135. feedbackPopup.setPopupPosition(
  136. handle.getAbsoluteLeft() + handle.getOffsetWidth() / 2
  137. - feedbackPopup.getOffsetWidth() / 2,
  138. handle.getAbsoluteTop() - feedbackPopup.getOffsetHeight());
  139. }
  140. }
  141. /** For internal use only. May be removed or replaced in the future. */
  142. public void buildBase() {
  143. final String styleAttribute = isVertical() ? "height" : "width";
  144. final String oppositeStyleAttribute = isVertical() ? "width" : "height";
  145. final String domProperty = isVertical() ? "offsetHeight"
  146. : "offsetWidth";
  147. // clear unnecessary opposite style attribute
  148. base.getStyle().clearProperty(oppositeStyleAttribute);
  149. /*
  150. * To resolve defect #13681 we should not return from method buildBase()
  151. * if slider has no parentElement, because such operations as
  152. * buildHandle() and setValues(), which are needed for Slider, are
  153. * called at the end of method buildBase(). And these methods will not
  154. * be called if there is no parentElement. So, instead of returning from
  155. * method buildBase() if there is no parentElement "if condition" is
  156. * applied to call code for parentElement only in case it exists.
  157. */
  158. if (getElement().hasParentElement()) {
  159. final Element p = getElement();
  160. if (p.getPropertyInt(domProperty) > MIN_SIZE) {
  161. if (isVertical()) {
  162. setHeight();
  163. } else {
  164. base.getStyle().clearProperty(styleAttribute);
  165. }
  166. } else {
  167. // Set minimum size and adjust after all components have
  168. // (supposedly) been drawn completely.
  169. base.getStyle().setPropertyPx(styleAttribute, MIN_SIZE);
  170. Scheduler.get().scheduleDeferred(new Command() {
  171. @Override
  172. public void execute() {
  173. final Element p = getElement();
  174. if (p.getPropertyInt(domProperty) > MIN_SIZE + 5
  175. || propertyNotNullOrEmpty(styleAttribute, p)) {
  176. if (isVertical()) {
  177. setHeight();
  178. } else {
  179. base.getStyle().clearProperty(styleAttribute);
  180. }
  181. // Ensure correct position
  182. setValue(value, false);
  183. }
  184. }
  185. // Style has non empty property
  186. private boolean propertyNotNullOrEmpty(
  187. final String styleAttribute, final Element p) {
  188. return p.getStyle().getProperty(styleAttribute) != null
  189. && !p.getStyle().getProperty(styleAttribute)
  190. .isEmpty();
  191. }
  192. });
  193. }
  194. }
  195. if (!isVertical()) {
  196. // Draw handle with a delay to allow base to gain maximum width
  197. Scheduler.get().scheduleDeferred(() -> {
  198. buildHandle();
  199. setValue(value, false);
  200. });
  201. } else {
  202. buildHandle();
  203. setValue(value, false);
  204. }
  205. // TODO attach listeners for focusing and arrow keys
  206. }
  207. void buildHandle() {
  208. final String handleAttribute = isVertical() ? "marginTop"
  209. : "marginLeft";
  210. final String oppositeHandleAttribute = isVertical() ? "marginLeft"
  211. : "marginTop";
  212. handle.getStyle().setProperty(handleAttribute, "0");
  213. // clear unnecessary opposite handle attribute
  214. handle.getStyle().clearProperty(oppositeHandleAttribute);
  215. }
  216. @Override
  217. public void onBrowserEvent(Event event) {
  218. if (disabled || readonly) {
  219. return;
  220. }
  221. final Element targ = DOM.eventGetTarget(event);
  222. if (DOM.eventGetType(event) == Event.ONMOUSEWHEEL) {
  223. processMouseWheelEvent(event);
  224. } else if (dragging || targ == handle) {
  225. processHandleEvent(event);
  226. } else if (targ.equals(base)
  227. && DOM.eventGetType(event) == Event.ONMOUSEUP
  228. && updateValueOnClick) {
  229. processBaseEvent(event);
  230. feedbackPopup.show();
  231. } else if (targ == smaller) {
  232. decreaseValue(true);
  233. } else if (targ == bigger) {
  234. increaseValue(true);
  235. } else if (isNavigationEvent(event)) {
  236. if (handleNavigation(event.getKeyCode(), event.getCtrlKey(),
  237. event.getShiftKey())) {
  238. feedbackPopup.show();
  239. delayedValueUpdater.trigger();
  240. DOM.eventPreventDefault(event);
  241. DOM.eventCancelBubble(event, true);
  242. }
  243. } else if (targ.equals(getElement())
  244. && DOM.eventGetType(event) == Event.ONFOCUS) {
  245. feedbackPopup.show();
  246. } else if (targ.equals(getElement())
  247. && DOM.eventGetType(event) == Event.ONBLUR) {
  248. feedbackPopup.hide();
  249. } else if (DOM.eventGetType(event) == Event.ONMOUSEDOWN) {
  250. feedbackPopup.show();
  251. }
  252. if (WidgetUtil.isTouchEvent(event)) {
  253. event.preventDefault(); // avoid simulated events
  254. event.stopPropagation();
  255. }
  256. }
  257. private boolean isNavigationEvent(Event event) {
  258. if (BrowserInfo.get().isGecko()
  259. && BrowserInfo.get().getGeckoVersion() < 65) {
  260. return DOM.eventGetType(event) == Event.ONKEYPRESS;
  261. } else {
  262. return DOM.eventGetType(event) == Event.ONKEYDOWN;
  263. }
  264. }
  265. private void processMouseWheelEvent(final Event event) {
  266. final int dir = DOM.eventGetMouseWheelVelocityY(event);
  267. if (dir < 0) {
  268. increaseValue(false);
  269. } else {
  270. decreaseValue(false);
  271. }
  272. delayedValueUpdater.trigger();
  273. DOM.eventPreventDefault(event);
  274. DOM.eventCancelBubble(event, true);
  275. }
  276. private void processHandleEvent(Event event) {
  277. switch (DOM.eventGetType(event)) {
  278. case Event.ONMOUSEDOWN:
  279. case Event.ONTOUCHSTART:
  280. if (!disabled && !readonly) {
  281. focus();
  282. feedbackPopup.show();
  283. dragging = true;
  284. handle.setClassName(getStylePrimaryName() + "-handle");
  285. handle.addClassName(getStylePrimaryName() + "-handle-active");
  286. DOM.setCapture(getElement());
  287. DOM.eventPreventDefault(event); // prevent selecting text
  288. DOM.eventCancelBubble(event, true);
  289. event.stopPropagation();
  290. }
  291. break;
  292. case Event.ONMOUSEMOVE:
  293. case Event.ONTOUCHMOVE:
  294. if (dragging) {
  295. setValueByEvent(event, false);
  296. updateFeedbackPosition();
  297. event.stopPropagation();
  298. }
  299. break;
  300. case Event.ONTOUCHEND:
  301. feedbackPopup.hide();
  302. case Event.ONMOUSEUP:
  303. // feedbackPopup.hide();
  304. dragging = false;
  305. handle.setClassName(getStylePrimaryName() + "-handle");
  306. DOM.releaseCapture(getElement());
  307. setValueByEvent(event, true);
  308. event.stopPropagation();
  309. break;
  310. default:
  311. break;
  312. }
  313. }
  314. private void processBaseEvent(Event event) {
  315. if (!disabled && !readonly && !dragging) {
  316. setValueByEvent(event, true);
  317. DOM.eventCancelBubble(event, true);
  318. }
  319. }
  320. private void decreaseValue(boolean updateToServer) {
  321. setValue(new Double(value.doubleValue() - Math.pow(10, -resolution)),
  322. updateToServer);
  323. }
  324. private void increaseValue(boolean updateToServer) {
  325. setValue(new Double(value.doubleValue() + Math.pow(10, -resolution)),
  326. updateToServer);
  327. }
  328. private void setValueByEvent(Event event, boolean updateToServer) {
  329. double v = min; // Fallback to min
  330. final int coord = getEventPosition(event);
  331. final int handleSize, baseSize, baseOffset;
  332. if (isVertical()) {
  333. handleSize = handle.getOffsetHeight();
  334. baseSize = base.getOffsetHeight();
  335. baseOffset = base.getAbsoluteTop() - Window.getScrollTop()
  336. - handleSize / 2;
  337. } else {
  338. handleSize = handle.getOffsetWidth();
  339. baseSize = base.getOffsetWidth();
  340. baseOffset = base.getAbsoluteLeft() - Window.getScrollLeft()
  341. + handleSize / 2;
  342. }
  343. if (isVertical()) {
  344. v = (baseSize - (coord - baseOffset))
  345. / (double) (baseSize - handleSize) * (max - min) + min;
  346. } else {
  347. v = (coord - baseOffset) / (double) (baseSize - handleSize)
  348. * (max - min) + min;
  349. }
  350. if (v < min) {
  351. v = min;
  352. } else if (v > max) {
  353. v = max;
  354. }
  355. setValue(v, updateToServer);
  356. }
  357. /**
  358. * TODO consider extracting touches support to an impl class specific for
  359. * webkit (only browser that really supports touches).
  360. *
  361. * @param event
  362. * @return
  363. */
  364. protected int getEventPosition(Event event) {
  365. if (isVertical()) {
  366. return WidgetUtil.getTouchOrMouseClientY(event);
  367. } else {
  368. return WidgetUtil.getTouchOrMouseClientX(event);
  369. }
  370. }
  371. public void iLayout() {
  372. if (isVertical()) {
  373. setHeight();
  374. }
  375. // Update handle position
  376. setValue(value, false);
  377. }
  378. private void setHeight() {
  379. // Calculate decoration size
  380. base.getStyle().setHeight(0, Unit.PX);
  381. base.getStyle().setOverflow(Overflow.HIDDEN);
  382. int h = getElement().getOffsetHeight();
  383. if (h < MIN_SIZE) {
  384. h = MIN_SIZE;
  385. }
  386. base.getStyle().setHeight(h, Unit.PX);
  387. base.getStyle().clearOverflow();
  388. }
  389. private void fireValueChanged() {
  390. ValueChangeEvent.fire(VSlider.this, value);
  391. }
  392. /**
  393. * Handles the keyboard events handled by the Slider.
  394. *
  395. * @param keycode
  396. * The key code received
  397. * @param ctrl
  398. * Whether {@code CTRL} was pressed
  399. * @param shift
  400. * Whether {@code SHIFT} was pressed
  401. * @return true if the navigation event was handled
  402. */
  403. public boolean handleNavigation(int keycode, boolean ctrl, boolean shift) {
  404. // No support for ctrl moving
  405. if (ctrl) {
  406. return false;
  407. }
  408. if (keycode == getNavigationUpKey() && isVertical()
  409. || keycode == getNavigationRightKey() && !isVertical()) {
  410. if (shift) {
  411. for (int a = 0; a < acceleration; a++) {
  412. increaseValue(false);
  413. }
  414. acceleration++;
  415. } else {
  416. increaseValue(false);
  417. }
  418. return true;
  419. } else if (keycode == getNavigationDownKey() && isVertical()
  420. || keycode == getNavigationLeftKey() && !isVertical()) {
  421. if (shift) {
  422. for (int a = 0; a < acceleration; a++) {
  423. decreaseValue(false);
  424. }
  425. acceleration++;
  426. } else {
  427. decreaseValue(false);
  428. }
  429. return true;
  430. }
  431. return false;
  432. }
  433. /**
  434. * Get the key that increases the vertical slider. By default it is the up
  435. * arrow key but by overriding this you can change the key to whatever you
  436. * want.
  437. *
  438. * @return The keycode of the key
  439. */
  440. protected int getNavigationUpKey() {
  441. return KeyCodes.KEY_UP;
  442. }
  443. /**
  444. * Get the key that decreases the vertical slider. By default it is the down
  445. * arrow key but by overriding this you can change the key to whatever you
  446. * want.
  447. *
  448. * @return The keycode of the key
  449. */
  450. protected int getNavigationDownKey() {
  451. return KeyCodes.KEY_DOWN;
  452. }
  453. /**
  454. * Get the key that decreases the horizontal slider. By default it is the
  455. * left arrow key but by overriding this you can change the key to whatever
  456. * you want.
  457. *
  458. * @return The keycode of the key
  459. */
  460. protected int getNavigationLeftKey() {
  461. return KeyCodes.KEY_LEFT;
  462. }
  463. /**
  464. * Get the key that increases the horizontal slider. By default it is the
  465. * right arrow key but by overriding this you can change the key to whatever
  466. * you want.
  467. *
  468. * @return The keycode of the key
  469. */
  470. protected int getNavigationRightKey() {
  471. return KeyCodes.KEY_RIGHT;
  472. }
  473. public void setConnection(ApplicationConnection client) {
  474. this.client = client;
  475. }
  476. public void setId(String id) {
  477. this.id = id;
  478. }
  479. public void setDisabled(boolean disabled) {
  480. this.disabled = disabled;
  481. }
  482. public void setReadOnly(boolean readonly) {
  483. this.readonly = readonly;
  484. }
  485. private boolean isVertical() {
  486. return orientation == SliderOrientation.VERTICAL;
  487. }
  488. public void setOrientation(SliderOrientation orientation) {
  489. if (this.orientation != orientation) {
  490. this.orientation = orientation;
  491. updateStyleNames(getStylePrimaryName(), true);
  492. }
  493. }
  494. public void setMinValue(double value) {
  495. min = value;
  496. }
  497. public void setMaxValue(double value) {
  498. max = value;
  499. }
  500. public void setResolution(int resolution) {
  501. this.resolution = resolution;
  502. }
  503. @Override
  504. public HandlerRegistration addValueChangeHandler(
  505. ValueChangeHandler<Double> handler) {
  506. return addHandler(handler, ValueChangeEvent.getType());
  507. }
  508. @Override
  509. public Double getValue() {
  510. return value;
  511. }
  512. @Override
  513. public void setValue(Double value) {
  514. if (value < min) {
  515. value = min;
  516. } else if (value > max) {
  517. value = max;
  518. }
  519. // Update handle position
  520. final String styleAttribute = isVertical() ? "marginTop" : "marginLeft";
  521. final String domProperty = isVertical() ? "offsetHeight"
  522. : "offsetWidth";
  523. final int handleSize = handle.getPropertyInt(domProperty);
  524. final int baseSize = base.getPropertyInt(domProperty)
  525. - 2 * BASE_BORDER_WIDTH;
  526. final int range = baseSize - handleSize;
  527. double v = value.doubleValue();
  528. // Round value to resolution
  529. if (resolution > 0) {
  530. v = Math.round(v * Math.pow(10, resolution));
  531. v = v / Math.pow(10, resolution);
  532. } else {
  533. v = Math.round(v);
  534. }
  535. final double valueRange = max - min;
  536. double p = 0;
  537. if (valueRange > 0) {
  538. p = range * ((v - min) / valueRange);
  539. }
  540. if (p < 0) {
  541. p = 0;
  542. }
  543. if (isVertical()) {
  544. p = range - p;
  545. }
  546. final double pos = p;
  547. handle.getStyle().setPropertyPx(styleAttribute, (int) Math.round(pos));
  548. // Update value
  549. this.value = new Double(v);
  550. setFeedbackValue(v);
  551. }
  552. @Override
  553. public void setValue(Double value, boolean fireEvents) {
  554. if (value == null) {
  555. return;
  556. }
  557. setValue(value);
  558. if (fireEvents) {
  559. fireValueChanged();
  560. }
  561. }
  562. @Override
  563. public com.google.gwt.user.client.Element getSubPartElement(
  564. String subPart) {
  565. if (subPart.equals("popup")) {
  566. feedbackPopup.show();
  567. return feedbackPopup.getElement();
  568. }
  569. return null;
  570. }
  571. @Override
  572. public String getSubPartName(
  573. com.google.gwt.user.client.Element subElement) {
  574. if (feedbackPopup.getElement().isOrHasChild(subElement)) {
  575. return "popup";
  576. }
  577. return null;
  578. }
  579. /**
  580. * Specifies whether or not click event should update the Slider's value.
  581. *
  582. * @param updateValueOnClick
  583. */
  584. public void setUpdateValueOnClick(boolean updateValueOnClick) {
  585. this.updateValueOnClick = updateValueOnClick;
  586. }
  587. }