PageRenderTime 27ms CodeModel.GetById 23ms RepoModel.GetById 1ms app.codeStats 0ms

/tiwulfx/src/main/java/eu/schudt/javafx/controls/calendar/DatePicker.java

https://bitbucket.org/panemu/tiwulfx
Java | 415 lines | 262 code | 72 blank | 81 comment | 34 complexity | 7877aa2046689f8aad0a813175460f34 MD5 | raw file
  1. package eu.schudt.javafx.controls.calendar;
  2. import javafx.application.Platform;
  3. import javafx.beans.InvalidationListener;
  4. import javafx.beans.Observable;
  5. import javafx.beans.binding.StringBinding;
  6. import javafx.beans.property.*;
  7. import javafx.beans.value.ChangeListener;
  8. import javafx.beans.value.ObservableValue;
  9. import javafx.event.ActionEvent;
  10. import javafx.event.EventHandler;
  11. import javafx.geometry.Bounds;
  12. import javafx.scene.control.Button;
  13. import javafx.scene.control.TextField;
  14. import javafx.scene.effect.DropShadow;
  15. import javafx.scene.input.KeyCode;
  16. import javafx.scene.input.KeyEvent;
  17. import javafx.scene.input.MouseEvent;
  18. import javafx.scene.layout.HBox;
  19. import javafx.stage.Popup;
  20. import java.text.DateFormat;
  21. import java.text.ParseException;
  22. import java.text.SimpleDateFormat;
  23. import java.util.Date;
  24. import java.util.Locale;
  25. import java.util.Timer;
  26. import java.util.TimerTask;
  27. /**
  28. * This is the class DateControl is based on. I change the modifier to default
  29. * (package visibility) so it won't be visible by TiwulFX developer. I didn't delete
  30. * it just for future reference
  31. * @author Christian Schudt
  32. * http://myjavafx.blogspot.com/2012/01/javafx-calendar-control.html
  33. */
  34. class DatePicker extends HBox {
  35. private static final String CSS_DATE_PICKER_VALID = "datepicker-valid";
  36. private static final String CSS_DATE_PICKER_INVALID = "datepicker-invalid";
  37. /**
  38. * Initializes the date picker with the default locale.
  39. */
  40. public DatePicker() {
  41. this(Locale.getDefault());
  42. }
  43. private Timer timer;
  44. /**
  45. * Initializes the date picker with the given locale.
  46. *
  47. * @param locale The locale.
  48. */
  49. public DatePicker(Locale locale) {
  50. calendarView = new CalendarView(locale);
  51. textField = new TextField();
  52. this.locale.set(locale);
  53. calendarView.setEffect(new DropShadow());
  54. // Use the same locale.
  55. calendarView.localeProperty().bind(localeProperty());
  56. // Bind the current date of the calendar view with the selected date, so that the calendar shows up with the same month as in the text field.
  57. calendarView.currentDateProperty().bind(selectedDateProperty());
  58. // When the user selects a date in the calendar view, hide it.
  59. calendarView.selectedDateProperty().addListener(new InvalidationListener() {
  60. @Override
  61. public void invalidated(Observable observable) {
  62. selectedDate.set(calendarView.selectedDateProperty().get());
  63. hidePopup();
  64. }
  65. });
  66. // Let the prompt text property listen to locale or date format changes.
  67. textField.promptTextProperty().bind(new StringBinding() {
  68. {
  69. super.bind(localeProperty(), promptTextProperty(), dateFormatProperty());
  70. }
  71. @Override
  72. protected String computeValue() {
  73. // First check, if there is a custom prompt text.
  74. if (promptTextProperty().get() != null) {
  75. return promptTextProperty().get();
  76. }
  77. // If not, use the the date format's pattern.
  78. DateFormat dateFormat = getActualDateFormat();
  79. if (dateFormat instanceof SimpleDateFormat) {
  80. return ((SimpleDateFormat) dateFormat).toPattern();
  81. }
  82. return "";
  83. }
  84. });
  85. // Change the CSS styles, when this control becomes invalid.
  86. invalid.addListener(new InvalidationListener() {
  87. @Override
  88. public void invalidated(Observable observable) {
  89. if (invalid.get()) {
  90. textField.getStyleClass().add(CSS_DATE_PICKER_INVALID);
  91. textField.getStyleClass().remove(CSS_DATE_PICKER_VALID);
  92. } else {
  93. textField.getStyleClass().remove(CSS_DATE_PICKER_INVALID);
  94. textField.getStyleClass().add(CSS_DATE_PICKER_VALID);
  95. }
  96. }
  97. });
  98. // When the text field no longer has the focus, try to parse the date.
  99. textField.addEventHandler(MouseEvent.MOUSE_PRESSED, new EventHandler<MouseEvent>() {
  100. @Override
  101. public void handle(MouseEvent event) {
  102. if (!textField.focusedProperty().get()) {
  103. if (!textField.getText().equals("")) {
  104. tryParse(true);
  105. }
  106. } else {
  107. showPopup();
  108. }
  109. }
  110. });
  111. // Listen to user input.
  112. textField.textProperty().addListener(new ChangeListener<String>() {
  113. @Override
  114. public void changed(ObservableValue<? extends String> observableValue, String s, String s1) {
  115. // Only evaluate the input, it it wasn't set programmatically.
  116. if (textSetProgrammatically) {
  117. return;
  118. }
  119. if (timer != null) {
  120. timer.cancel();
  121. }
  122. // If the user clears the text field, set the date to null and the field to valid.
  123. if (s1.equals("")) {
  124. selectedDate.set(null);
  125. invalid.set(false);
  126. } else {
  127. // Start a timer, so that the user input is not evaluated immediately, but after a second.
  128. // This way, input like 01/01/1 is not immediately parsed as 01/01/01.
  129. // The user gets one second time, to complete his date, maybe his intention was to enter 01/01/12.
  130. timer = new Timer();
  131. timer.schedule(new TimerTask() {
  132. @Override
  133. public void run() {
  134. Platform.runLater(new Runnable() {
  135. @Override
  136. public void run() {
  137. tryParse(false);
  138. }
  139. });
  140. }
  141. }, 1000);
  142. }
  143. }
  144. });
  145. selectedDateProperty().addListener(new InvalidationListener() {
  146. @Override
  147. public void invalidated(Observable observable) {
  148. updateTextField();
  149. invalid.set(false);
  150. }
  151. });
  152. localeProperty().addListener(new InvalidationListener() {
  153. @Override
  154. public void invalidated(Observable observable) {
  155. updateTextField();
  156. }
  157. });
  158. textField.addEventHandler(KeyEvent.KEY_PRESSED, new EventHandler<KeyEvent>() {
  159. @Override
  160. public void handle(KeyEvent keyEvent) {
  161. if (keyEvent.getCode() == KeyCode.DOWN) {
  162. showPopup();
  163. }
  164. }
  165. });
  166. Button button = new Button(">");
  167. button.setFocusTraversable(false);
  168. button.setOnAction(new EventHandler<ActionEvent>() {
  169. @Override
  170. public void handle(ActionEvent actionEvent) {
  171. showPopup();
  172. }
  173. });
  174. getChildren().add(textField);
  175. //getChildren().add(button);
  176. }
  177. private void hidePopup() {
  178. if (popup != null) {
  179. popup.hide();
  180. }
  181. }
  182. /**
  183. * Tries to parse the text field for a valid date.
  184. *
  185. * @param setDateToNullOnException True, if the date should be set to null, when a {@link ParseException} occurs.
  186. * This is the case, when the text field loses focus.
  187. */
  188. private void tryParse(boolean setDateToNullOnException) {
  189. if (timer != null) {
  190. timer.cancel();
  191. }
  192. try {
  193. // Double parse the date here, since e.g. 01.01.1 is parsed as year 1, and then formatted as 01.01.01 and then parsed as year 2001.
  194. // This might lead to an undesired date.
  195. DateFormat dateFormat = getActualDateFormat();
  196. Date parsedDate = dateFormat.parse(textField.getText());
  197. parsedDate = dateFormat.parse(dateFormat.format(parsedDate));
  198. if (selectedDate.get() == null || selectedDate.get() != null && parsedDate.getTime() != selectedDate.get().getTime()) {
  199. selectedDate.set(parsedDate);
  200. }
  201. invalid.set(false);
  202. updateTextField();
  203. } catch (ParseException e) {
  204. invalid.set(true);
  205. if (setDateToNullOnException) {
  206. selectedDate.set(null);
  207. }
  208. }
  209. }
  210. private boolean textSetProgrammatically;
  211. /**
  212. * Updates the text field.
  213. */
  214. public void updateTextField() {
  215. // Mark the we update the text field (and not the user), so that it can be ignored, by textField.textProperty()
  216. textSetProgrammatically = true;
  217. if (selectedDateProperty().get() != null) {
  218. String date = getActualDateFormat().format(selectedDateProperty().get());
  219. if (!textField.getText().equals(date)) {
  220. textField.setText(date);
  221. }
  222. } else {
  223. textField.setText("");
  224. }
  225. textSetProgrammatically = false;
  226. }
  227. /**
  228. * Gets the actual date format. If {@link #dateFormatProperty()} is set, take it, otherwise get a default format for the current locale.
  229. *
  230. * @return The date format.
  231. */
  232. private DateFormat getActualDateFormat() {
  233. if (dateFormat.get() != null) {
  234. return dateFormat.get();
  235. }
  236. DateFormat format = DateFormat.getDateInstance(DateFormat.SHORT, locale.get());
  237. format.setCalendar(calendarView.getCalendar());
  238. format.setLenient(false);
  239. return format;
  240. }
  241. private CalendarView calendarView;
  242. /**
  243. * Use this to set further properties of the calendar.
  244. *
  245. * @return The calendar view.
  246. */
  247. public CalendarView getCalendarView() {
  248. return calendarView;
  249. }
  250. private TextField textField;
  251. private BooleanProperty invalid = new SimpleBooleanProperty();
  252. /**
  253. * States whether the user input is invalid (is no valid date).
  254. *
  255. * @return The property.
  256. */
  257. public ReadOnlyBooleanProperty invalidProperty() {
  258. return invalid;
  259. }
  260. /**
  261. * The locale.
  262. *
  263. * @return The property.
  264. */
  265. public ObjectProperty<Locale> localeProperty() {
  266. return locale;
  267. }
  268. private ObjectProperty<Locale> locale = new SimpleObjectProperty<Locale>();
  269. public void setLocale(Locale locale) {
  270. this.locale.set(locale);
  271. }
  272. public Locale getLocale() {
  273. return locale.get();
  274. }
  275. /**
  276. * The selected date.
  277. *
  278. * @return The property.
  279. */
  280. public ObjectProperty<Date> selectedDateProperty() {
  281. return selectedDate;
  282. }
  283. private ObjectProperty<Date> selectedDate = new SimpleObjectProperty<Date>();
  284. public void setSelectedDate(Date date) {
  285. this.selectedDate.set(date);
  286. }
  287. public Date getSelectedDate() {
  288. return selectedDate.get();
  289. }
  290. /**
  291. * Gets the date format.
  292. *
  293. * @return The date format.
  294. */
  295. public ObjectProperty<DateFormat> dateFormatProperty() {
  296. return dateFormat;
  297. }
  298. private ObjectProperty<DateFormat> dateFormat = new SimpleObjectProperty<DateFormat>();
  299. public void setDateFormat(DateFormat dateFormat) {
  300. this.dateFormat.set(dateFormat);
  301. }
  302. public DateFormat getDateFormat() {
  303. return dateFormat.get();
  304. }
  305. private StringProperty promptText = new SimpleStringProperty();
  306. /**
  307. * The prompt text for the text field.
  308. * By default, the prompt text is taken from the date format pattern.
  309. *
  310. * @return The property.
  311. */
  312. public StringProperty promptTextProperty() {
  313. return promptText;
  314. }
  315. public void setPromptText(String promptText) {
  316. this.promptText.set(promptText);
  317. }
  318. public String getPromptText() {
  319. return promptText.get();
  320. }
  321. private Popup popup;
  322. /**
  323. * Shows the pop up.
  324. */
  325. private void showPopup() {
  326. if (popup == null) {
  327. popup = new Popup();
  328. popup.setAutoHide(true);
  329. popup.setHideOnEscape(true);
  330. popup.setAutoFix(true);
  331. popup.getContent().add(calendarView);
  332. }
  333. Bounds calendarBounds = calendarView.getBoundsInLocal();
  334. Bounds bounds = localToScene(getBoundsInLocal());
  335. double posX = calendarBounds.getMinX() + bounds.getMinX() + getScene().getX() + getScene().getWindow().getX();
  336. double posY = calendarBounds.getMinY() + bounds.getHeight() + bounds.getMinY() + getScene().getY() + getScene().getWindow().getY();
  337. popup.show(this, posX, posY);
  338. }
  339. @Override
  340. public void requestFocus() {
  341. textField.requestFocus();
  342. }
  343. }