├── .github └── workflows │ └── build.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── pom.xml ├── screenshot ├── datepicker-dark.png ├── datepicker-light.png ├── timepicker-dark.png └── timepicker-light.png └── src ├── main ├── java │ └── raven │ │ └── datetime │ │ ├── DatePicker.java │ │ ├── DateSelectionAble.java │ │ ├── PanelDateOption.java │ │ ├── PanelDateOptionLabel.java │ │ ├── TimePicker.java │ │ ├── TimeSelectionAble.java │ │ ├── component │ │ ├── PanelPopupEditor.java │ │ ├── date │ │ │ ├── ButtonDate.java │ │ │ ├── ButtonMonthYear.java │ │ │ ├── DateSelectionModel.java │ │ │ ├── DefaultDateCellRenderer.java │ │ │ ├── Header.java │ │ │ ├── PanelDate.java │ │ │ ├── PanelMonth.java │ │ │ ├── PanelYear.java │ │ │ ├── SingleDate.java │ │ │ └── event │ │ │ │ ├── DateControlEvent.java │ │ │ │ ├── DateControlListener.java │ │ │ │ ├── DateSelectionModelEvent.java │ │ │ │ └── DateSelectionModelListener.java │ │ └── time │ │ │ ├── AnimationChange.java │ │ │ ├── Header.java │ │ │ ├── PanelClock.java │ │ │ ├── TimeSelectionModel.java │ │ │ └── event │ │ │ ├── TimeActionListener.java │ │ │ ├── TimeSelectionModelEvent.java │ │ │ └── TimeSelectionModelListener.java │ │ ├── event │ │ ├── DateSelectionEvent.java │ │ ├── DateSelectionListener.java │ │ ├── TimeSelectionEvent.java │ │ └── TimeSelectionListener.java │ │ ├── swing │ │ └── slider │ │ │ ├── PanelSlider.java │ │ │ ├── SimpleTransition.java │ │ │ └── SliderTransition.java │ │ └── util │ │ ├── InputUtils.java │ │ ├── InputValidationListener.java │ │ └── Utils.java └── resources │ └── raven │ └── datetime │ └── icon │ ├── back.svg │ ├── calendar.svg │ ├── clock.svg │ └── forward.svg └── test └── java └── test ├── TestDate.java ├── TestFrame.java ├── TestTime.java └── renderer ├── CustomDateCellRenderer.java ├── LabelDate.java └── TestDateCellRenderer.java /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build and Release 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | tags: 7 | - 'v[0-9]*' 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-24.04 12 | name: Build JAR and Release 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Set up JDK 11 17 | uses: actions/setup-java@v2 18 | with: 19 | java-version: '11' 20 | distribution: 'adopt' 21 | 22 | - name: Cache local Maven repository 23 | uses: actions/cache@v3 24 | with: 25 | path: ~/.m2/repository 26 | key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} 27 | restore-keys: | 28 | ${{ runner.os }}-maven- 29 | 30 | - name: Build JAR with Maven 31 | run: mvn clean package -DskipTests --file pom.xml 32 | 33 | - name: Get Changelog Entry 34 | id: changelog_reader 35 | uses: mindsers/changelog-reader-action@v2 36 | with: 37 | validation_level: warn 38 | version: ${{ steps.tag_name.outputs.current_version }} 39 | path: ./CHANGELOG.md 40 | 41 | - name: Upload Release Asset 42 | uses: softprops/action-gh-release@v2 43 | with: 44 | files: ./target/*.jar 45 | tag_name: ${{ github.ref_name }} 46 | name: ${{ github.ref_name }} 47 | body: ${{ steps.changelog_reader.outputs.changes }} 48 | generate_release_notes: true 49 | env: 50 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | !.mvn/wrapper/maven-wrapper.jar 3 | !**/src/main/**/target/ 4 | !**/src/test/**/target/ 5 | 6 | ### IntelliJ IDEA ### 7 | .idea/ 8 | *.iws 9 | *.iml 10 | *.ipr 11 | 12 | ### Eclipse ### 13 | .apt_generated 14 | .classpath 15 | .factorypath 16 | .project 17 | .settings 18 | .springBeans 19 | .sts4-cache 20 | 21 | ### NetBeans ### 22 | /nbproject/private/ 23 | /nbbuild/ 24 | /dist/ 25 | /nbdist/ 26 | /.nb-gradle/ 27 | build/ 28 | !**/src/main/**/build/ 29 | !**/src/test/**/build/ 30 | 31 | ### VS Code ### 32 | .vscode/ 33 | 34 | ### Mac OS ### 35 | .DS_Store -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [2.1.2] - 2025-06-08 4 | 5 | ### New features and improvements 6 | 7 | - DatePicker: 8 | - Add new option `defaultDateCellRenderer` to custom date cell renderer (issue #18) and (PR #19) 9 | - Add global `DatePicker.defaultWeekdays` for change the default label weekdays (issue #20) 10 | - Add new method `getEditor()` return as `JFormattedTextField` is editor has set 11 | 12 | ### Changed 13 | 14 | - TimePicker: 15 | - Time header button AM and PM now use `Locale.ENGLISH` as string symbols 16 | 17 | ### Fixed bugs 18 | 19 | - DatePicker: 20 | - Fixed incorrect start day of week when `startWeekOnMonday` is true (issue #21) 21 | - TimePicker: 22 | - Fixed incorrect selection location in 24 hour view. 23 | 24 | ## [2.1.1] - 2025-05-05 25 | 26 | ### Changed 27 | 28 | - DatePicker: 29 | - Removed component fixed value size. (previous width:260, height:250) 30 | - Add new option `selectionArc` default `999f` 31 | 32 | ## [2.1.0] - 2025-04-25 33 | 34 | ### New features and improvements 35 | 36 | - DatePicker: 37 | - Add new option `startWeekOnMonday` for show the monday is the start of the week (default `false`) 38 | - Add new method `toDateSelectionView()` for show date selection state (show as current selected date. if selected 39 | date `null` show as system current date) and this method call by default when show with popup 40 | - TimePicker: 41 | - Add new method `setHourSelectionView(boolean hourSelectionView)` if `true` show hour selection state. if `false` 42 | show minute selection state. and this method call by default when show with popup 43 | - Date and Time Popup: 44 | - Add new method: 45 | - `showPopup(Component component)` to show datepicker or timePicker with popup without editor 46 | - `setPopupSpace(Point popupSpace)` to set the popup space with component or editor 47 | 48 | ### Changed 49 | 50 | - Add popup frame insets default `(5,5,5,5)` 51 | - FlatLaf: update to `v3.6` 52 | - Test: removed `flatlaf-fonts-roboto` and style properties in resources 53 | 54 | ## [2.0.0] - 2024-12-24 55 | 56 | ### New features and improvements 57 | 58 | - DatePicker: 59 | - When disabled, users can no longer change the date selection 60 | - TimePicker: 61 | - When disabled, users can no longer change the time selection 62 | - Add time selection able and editor input validation check (issue #14) and (PR #15) 63 | 64 | ### Changed 65 | 66 | - Rewrote and refactored existing code for improved readability and performance 67 | - Move `raven.datetime.component.date.DatePicker` to `raven.datetime.DatePicker` 68 | - Move `raven.datetime.component.time.TimePicker` to `raven.datetime.TimePicker` 69 | 70 | ## [1.4.1] - 2024-11-29 71 | 72 | ### New features and improvements 73 | 74 | - DatePicker: 75 | - Add new option `animationEnabled` 76 | - Date popup in `SINGLE_DATE_SELECTED` mode, now auto close when double click 77 | - Add `PanelDateOptionLabel` to custom panel date option (PR #10) 78 | 79 | ### Changed 80 | 81 | - FlatLaf: update to v3.5.2 82 | 83 | ### Fixed bugs 84 | 85 | - DatePicker: 86 | - Fixed editor input validation 87 | - Miglayout use `novisualpadding` and set slider fix size as `260,250` 88 | - TimePicker: 89 | - Fixed `editor` not changed value when switch between `24h` and `12h` view 90 | 91 | ## [1.4.0] - 2024-09-14 92 | 93 | ### New features and improvements 94 | 95 | - DatePicker: 96 | - Add editor input validation (PR #9) 97 | 98 | ### Changed 99 | 100 | - Miglayout: back to v5.3 to support java 8 101 | 102 | ## [1.3.0] - 2024-07-05 103 | 104 | ### New features and improvements 105 | 106 | - DatePicker: 107 | - Add new method: 108 | - `setDateFormat(String format)` to format date (issues #5) 109 | - `setColor(Color color)` (issues #6) 110 | 111 | ### Fixed bugs 112 | 113 | - PanelSlider `flush` image after end of animation 114 | - Fixed editor popup error when using JDialog 115 | 116 | ## [1.2.0] - 2024-05-06 117 | 118 | ### New features and improvements 119 | 120 | - DatePicker: 121 | - Add new method `selectCurrentMonth()` (PR #2) 122 | - TimePicker: 123 | - Add new method `setColor(Color color)` (issues #3) 124 | - Popup menu will show inside the windows frame 125 | - Add new method `setEditorIcon(Icon icon)` 126 | 127 | ### Fixed bugs 128 | 129 | - DatePicker: 130 | - Invert between date selected (when `date` is after `toDate`) (PR #2) 131 | - Fixed `between date` resend value when editor value changed 132 | - TimePicker: 133 | - Fixed event time changed not work while `am` or `pm` changed in editor 134 | - Fixed time format by use `Locale.ENGLISH` (issues #4) 135 | - Fixed reset old editor to default after changed new editor 136 | - Fixed event `SelectionListener` invoke only value changed 137 | 138 | ## [1.1.0] - 2024-04-13 139 | 140 | ### New features and improvements 141 | 142 | - Add new datepicker (PR #1) 143 | - Update style background in timepicker 144 | 145 | ## [1.0.0] - 2023-12-17 146 | 147 | - Initial release 148 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Raven Laing 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Swing Datetime Picker 2 | A simple datetime picker implementation using Java Swing, built with the `flatlaf` UI library and `miglayout` for layout management. 3 | 4 | This project provides a datetime picker component that can be easily integrated into Java Swing applications. It leverages flatlaf for a modern look and miglayout for flexible and easy-to-use layout management. 5 | 6 | timepicker dark  7 | timepicker light 8 |
9 | datepicker dark  10 | datepicker light 11 | ## Installation 12 | 13 | [![Maven Central](https://img.shields.io/maven-central/v/io.github.dj-raven/swing-datetime-picker?label=Maven%20Central)](https://central.sonatype.com/artifact/io.github.dj-raven/swing-datetime-picker/versions) 14 | 15 | Add the dependency 16 | ``` xml 17 | 18 | io.github.dj-raven 19 | swing-datetime-picker 20 | 2.1.2 21 | 22 | ``` 23 | 24 | ### Snapshots 25 | To get the latest updates before the release, you can use the snapshot version from [Sonatype Central](https://central.sonatype.com/service/rest/repository/browse/maven-snapshots/io/github/dj-raven/swing-datetime-picker/) 26 | 27 | ``` xml 28 | 29 | 30 | Central Portal Snapshots 31 | central-portal-snapshots 32 | https://central.sonatype.com/repository/maven-snapshots/ 33 | 34 | 35 | ``` 36 | Add the snapshot version 37 | ``` xml 38 | 39 | io.github.dj-raven 40 | swing-datetime-picker 41 | 2.1.2-SNAPSHOT 42 | 43 | ``` 44 | 45 | ## Usage TimePicker 46 | | Method | Return Value | Description | 47 | | ------------ | ------------ |----------------------------------------------------------------| 48 | | now() | | set the time to current local time | 49 | | setSelectedTime(LocalTime time) | | set the time to a specific value | 50 | | clearSelectedTime() | | clear the selected time | 51 | | isTimeSelected() | `boolean` | check time is selected | 52 | | getSelectedTime() | `LocalTime` | return the selected time | 53 | | getSelectedTimeAsString() | `String` | return selected time as string | 54 | | addTimeSelectionListener(TimeSelectionListener event) | | add event time selection | 55 | | removeTimeSelectionListener(TimeSelectionListener event) | | remove event time selection | 56 | | setOrientation(int orientation) | | `SwingConstants.VERTICAL` or `SwingConstants.HORIZONTAL` | 57 | | setEditor(JFormattedTextField editor) | | display the selected time on the editor and allow to edit time | 58 | | setTimeSelectionAble(TimeSelectionAble dsb) | | set time selection able | 59 | | set24HourView(boolean hour24) | | set time to 24h selection view | 60 | | is24HourView() | `boolean` | return `ture` is 24h selection view | 61 | | showPopup() | | if time have editor, timepicker will show up with popup menu | 62 | | closePopup() | | close editor popup | 63 | | setColor(Color color) | | change base color | 64 | | setEditorIcon(Icon icon) | | change icon to editor | 65 | | setEditorValidation(boolean validation) | | validation editor | 66 | | void setValidationOnNull(boolean validationOnNull) | | validation editor on null selection | 67 | | void showPopup(Component component) | | show timePicker with popup without editor | 68 | | void setPopupSpace(Point popupSpace) | | set the popup space with component or editor | 69 | | void toDateSelectionView() | | show date selection state | 70 | 71 | ## Usage DatePicker 72 | | Method | Return Value | Description | 73 | |--------------------------------------------------------| ----------- | ------------ | 74 | | now() | | set the date to current local date | 75 | | setToBack() | | slide panel to back with animation | 76 | | setToForward() | | slide panel to forward with animation | 77 | | selectMonth() | | show panel month slide with animation | 78 | | selectYear() | | show panel year slide with animation | 79 | | slideTo(LocalDate date) | | slide panel to specific date | 80 | | getDateSelectionMode() | `DateSelectionMode` | return the date selection mode | 81 | | setDateSelectionMode(DateSelectionMode mode) | | set mode `SINGLE_DATE_SELECTED` or `BETWEEN_DATE_SELECTED` | 82 | | setSelectedDate(LocalDate date) | | set the date to a specific value | 83 | | setSelectedDateRange(LocalDate from, LocalDate to) | | set the date range to a specific value | 84 | | setEditor(JFormattedTextField editor) | | display the selected date on the editor and allow to edit date | 85 | | setDateSelectionAble(DateSelectionAble dsb) | | set date selection able | 86 | | showPopup() | | if date have editor, datepicker will show up with popup menu | 87 | | closePopup() | | close editor popup | 88 | | setSeparator(String separator) | | set separator to between date | 89 | | setUsePanelOption(boolean usePanelOption) | | set datepicker use panel option | 90 | | setCloseAfterSelected(boolean closeAfterSelected) | | if true popup will close after selected date | 91 | | clearSelectedDate() | | clear the selected date | 92 | | isDateSelected() | `boolean` | check date is selected | 93 | | getSelectedDate() | `LocalDate` | return the selected date | 94 | | getSelectedDateRange() | `LocalDate[]` | return the selected date range | 95 | | getSelectedDateAsString() | `String` | return selected date as string | 96 | | addDateSelectionListener(DateSelectionListener evt) | | add event date selection | 97 | | removeDateSelectionListener(DateSelectionListener evt) | | remove event date selection | 98 | | selectCurrentMonth() | | select from first day to current day in current month | 99 | | setColor(Color color) | | change base color | 100 | | setEditorIcon(Icon icon) | | change icon to editor | 101 | | setDateFormat(String format) | | change date format | 102 | | setEditorValidation(boolean validation) | | validation editor | 103 | | void setValidationOnNull(boolean validationOnNull) | | validation editor on null selection | 104 | | void setAnimationEnabled(boolean animationEnabled) | | enable or disabled the animation | 105 | | void setPanelDateOptionLabel(PanelDateOptionLabel opt) | | set new panel date option label | 106 | | void setStartWeekOnMonday(boolean startWeekOnMonday) | | show the monday is the start of the week | 107 | | void showPopup(Component component) | | show datePicker with popup without editor | 108 | | void setPopupSpace(Point popupSpace) | | set the popup space with component or editor | 109 | | void setHourSelectionView(boolean hourSelectionView) | | show hour or minute selection state | 110 | | void setSelectionArc(float selectionArc) | | set date selection border arc | 111 | | void setDefaultDateCellRenderer(DefaultDateCellRenderer renderer) | | set date cell renderer to paint custom graphics | 112 | | public static void setDefaultWeekdays(String[] defaultWeekdays) | | `static` method for set default label week days | 113 | 114 | ## Library Resources 115 | - [FlatLaf](https://github.com/JFormDesigner/FlatLaf) - FlatLaf library for the modern UI design theme 116 | - [MigLayout](https://github.com/mikaelgrev/miglayout) - MigLayout library for flexible layout management 117 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | io.github.dj-raven 8 | swing-datetime-picker 9 | 2.1.2 10 | 11 | 12 | 1.8 13 | 1.8 14 | UTF-8 15 | 16 | 17 | Java Swing Datetime Picker 18 | Swing library build with flatlaf 19 | https://github.com/DJ-Raven/swing-datetime-picker 20 | 21 | 22 | 23 | MIT License 24 | http://www.opensource.org/licenses/mit-license.php 25 | 26 | 27 | 28 | 29 | 30 | Raven Laing 31 | com.github.dj-raven 32 | https://github.com/dj-raven 33 | 34 | 35 | 36 | 37 | scm:git:git://github.com/dj-raven/swing-datetime-picker.git 38 | scm:git:ssh://github.com:dj-raven/swing-datetime-picker.git 39 | https://github.com/dj-raven/swing-datetime-picker 40 | 41 | 42 | 43 | 44 | com.formdev 45 | flatlaf 46 | 3.6 47 | 48 | 49 | com.formdev 50 | flatlaf-extras 51 | 3.6 52 | 53 | 54 | com.miglayout 55 | miglayout-swing 56 | 5.3 57 | 58 | 59 | 60 | com.formdev 61 | flatlaf-intellij-themes 62 | 3.6 63 | test 64 | 65 | 66 | 67 | 68 | 69 | 70 | org.sonatype.central 71 | central-publishing-maven-plugin 72 | 0.7.0 73 | true 74 | 75 | central 76 | 77 | 78 | 79 | 80 | org.apache.maven.plugins 81 | maven-source-plugin 82 | 2.2.1 83 | 84 | 85 | attach-sources 86 | 87 | jar-no-fork 88 | 89 | 90 | 91 | 92 | 93 | org.apache.maven.plugins 94 | maven-javadoc-plugin 95 | 2.9.1 96 | 97 | 98 | attach-javadocs 99 | 100 | jar 101 | 102 | 103 | 104 | 105 | 106 | 107 | org.apache.maven.plugins 108 | maven-gpg-plugin 109 | 1.5 110 | 111 | 112 | sign-artifacts 113 | verify 114 | 115 | sign 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /screenshot/datepicker-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJ-Raven/swing-datetime-picker/f8b61c4c61b63cbd189d72019f0956377dc6b981/screenshot/datepicker-dark.png -------------------------------------------------------------------------------- /screenshot/datepicker-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJ-Raven/swing-datetime-picker/f8b61c4c61b63cbd189d72019f0956377dc6b981/screenshot/datepicker-light.png -------------------------------------------------------------------------------- /screenshot/timepicker-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJ-Raven/swing-datetime-picker/f8b61c4c61b63cbd189d72019f0956377dc6b981/screenshot/timepicker-dark.png -------------------------------------------------------------------------------- /screenshot/timepicker-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJ-Raven/swing-datetime-picker/f8b61c4c61b63cbd189d72019f0956377dc6b981/screenshot/timepicker-light.png -------------------------------------------------------------------------------- /src/main/java/raven/datetime/DatePicker.java: -------------------------------------------------------------------------------- 1 | package raven.datetime; 2 | 3 | import com.formdev.flatlaf.FlatClientProperties; 4 | import com.formdev.flatlaf.extras.FlatSVGIcon; 5 | import net.miginfocom.swing.MigLayout; 6 | import raven.datetime.component.PanelPopupEditor; 7 | import raven.datetime.component.date.*; 8 | import raven.datetime.component.date.event.DateControlEvent; 9 | import raven.datetime.component.date.event.DateControlListener; 10 | import raven.datetime.component.date.event.DateSelectionModelEvent; 11 | import raven.datetime.component.date.event.DateSelectionModelListener; 12 | import raven.datetime.event.DateSelectionEvent; 13 | import raven.datetime.event.DateSelectionListener; 14 | import raven.datetime.swing.slider.PanelSlider; 15 | import raven.datetime.swing.slider.SimpleTransition; 16 | import raven.datetime.swing.slider.SliderTransition; 17 | import raven.datetime.util.InputUtils; 18 | import raven.datetime.util.InputValidationListener; 19 | 20 | import javax.swing.*; 21 | import javax.swing.event.ChangeEvent; 22 | import javax.swing.event.ChangeListener; 23 | import java.awt.*; 24 | import java.time.LocalDate; 25 | import java.time.format.DateTimeFormatter; 26 | import java.util.Arrays; 27 | 28 | public class DatePicker extends PanelPopupEditor implements DateSelectionModelListener, DateControlListener, ChangeListener { 29 | 30 | private static String[] defaultWeekdays = null; 31 | 32 | private DateTimeFormatter format; 33 | private String dateFormatPattern = "dd/MM/yyyy"; 34 | private DateSelectionListener dateSelectionListener; 35 | private InputValidationListener inputValidationListener; 36 | private DateSelectionModel dateSelectionModel; 37 | private PanelDateOption panelDateOption; 38 | private PanelDateOptionLabel panelDateOptionLabel; 39 | private InputUtils.ValueCallback valueCallback; 40 | private Icon editorIcon; 41 | private String separator = " to "; 42 | private boolean usePanelOption; 43 | private boolean closeAfterSelected; 44 | private boolean animationEnabled = true; 45 | private boolean startWeekOnMonday; 46 | private float selectionArc = 999; 47 | private int month = 10; 48 | private int year = 2023; 49 | private Color color; 50 | private JButton editorButton; 51 | private SelectionState selectionState = SelectionState.DATE; 52 | private PanelDate panelDate; 53 | private PanelMonth panelMonth; 54 | private PanelYear panelYear; 55 | 56 | private DefaultDateCellRenderer defaultDateCellRenderer = new DefaultDateCellRenderer(); 57 | private final Header header = new Header(); 58 | private final PanelSlider panelSlider = new PanelSlider(); 59 | 60 | public DatePicker() { 61 | this(null); 62 | } 63 | 64 | public DatePicker(DateSelectionModel dateSelectionModel) { 65 | init(dateSelectionModel); 66 | } 67 | 68 | private void init(DateSelectionModel dateSelectionModel) { 69 | putClientProperty(FlatClientProperties.STYLE, "" + 70 | "[light]background:darken($Panel.background,2%);" + 71 | "[dark]background:lighten($Panel.background,2%);"); 72 | setLayout(new MigLayout( 73 | "wrap,insets 10,fill", 74 | "[fill]", 75 | "[top,grow 0][center,fill]")); 76 | 77 | format = DateTimeFormatter.ofPattern(dateFormatPattern); 78 | header.addDateControlListener(this); 79 | 80 | if (dateSelectionModel == null) { 81 | dateSelectionModel = createDefaultDateSelection(); 82 | } 83 | setDateSelectionModel(dateSelectionModel); 84 | 85 | add(header); 86 | add(panelSlider); 87 | initDate(); 88 | } 89 | 90 | private void initDate() { 91 | LocalDate date = LocalDate.now(); 92 | int month = date.getMonthValue() - 1; 93 | int year = date.getYear(); 94 | this.month = month; 95 | this.year = year; 96 | header.setDate(month, year); 97 | panelSlider.addSlide(createPanelDate(month, year), null); 98 | } 99 | 100 | public void setToBack() { 101 | if (selectionState == SelectionState.DATE) { 102 | if (month == 0) { 103 | month = 11; 104 | year--; 105 | } else { 106 | month--; 107 | } 108 | header.setDate(month, year); 109 | panelSlider.addSlide(createPanelDate(month, year), getSliderTransition(SimpleTransition.SliderType.BACK)); 110 | } else if (selectionState == SelectionState.MONTH) { 111 | year--; 112 | header.setDate(month, year); 113 | panelSlider.addSlide(createPanelMonth(year), getSliderTransition(SimpleTransition.SliderType.BACK)); 114 | } else { 115 | int oldYear = this.panelYear.getYear(); 116 | panelSlider.addSlide(createPanelYear(oldYear - PanelYear.YEAR_CELL), getSliderTransition(SimpleTransition.SliderType.BACK)); 117 | } 118 | } 119 | 120 | public void setToForward() { 121 | if (selectionState == SelectionState.DATE) { 122 | if (month == 11) { 123 | month = 0; 124 | year++; 125 | } else { 126 | month++; 127 | } 128 | header.setDate(month, year); 129 | panelSlider.addSlide(createPanelDate(month, year), getSliderTransition(SimpleTransition.SliderType.FORWARD)); 130 | } else if (selectionState == SelectionState.MONTH) { 131 | year++; 132 | header.setDate(month, year); 133 | panelSlider.addSlide(createPanelMonth(year), getSliderTransition(SimpleTransition.SliderType.FORWARD)); 134 | } else { 135 | int oldYear = this.panelYear.getYear(); 136 | panelSlider.addSlide(createPanelYear(oldYear + PanelYear.YEAR_CELL), getSliderTransition(SimpleTransition.SliderType.FORWARD)); 137 | } 138 | } 139 | 140 | public void selectMonth() { 141 | if (selectionState != SelectionState.MONTH) { 142 | panelSlider.addSlide(createPanelMonth(year), getSliderTransition(selectionState == SelectionState.DATE ? SimpleTransition.SliderType.TOP_DOWN : SimpleTransition.SliderType.DOWN_TOP)); 143 | selectionState = SelectionState.MONTH; 144 | } else { 145 | panelSlider.addSlide(createPanelDate(month, year), getSliderTransition(SimpleTransition.SliderType.DOWN_TOP)); 146 | selectionState = SelectionState.DATE; 147 | } 148 | } 149 | 150 | public void selectYear() { 151 | if (selectionState != SelectionState.YEAR) { 152 | panelSlider.addSlide(createPanelYear(year), getSliderTransition(SimpleTransition.SliderType.TOP_DOWN)); 153 | selectionState = SelectionState.YEAR; 154 | } else { 155 | panelSlider.addSlide(createPanelDate(month, year), getSliderTransition(SimpleTransition.SliderType.DOWN_TOP)); 156 | selectionState = SelectionState.DATE; 157 | } 158 | } 159 | 160 | public DateSelectionMode getDateSelectionMode() { 161 | return dateSelectionModel.getDateSelectionMode(); 162 | } 163 | 164 | public void setDateSelectionMode(DateSelectionMode dateSelectionMode) { 165 | if (getDateSelectionMode() != dateSelectionMode) { 166 | this.dateSelectionModel.setDateSelectionMode(dateSelectionMode); 167 | if (editor != null) { 168 | InputUtils.changeDateFormatted(editor, dateFormatPattern, getDateSelectionMode() == DateSelectionMode.BETWEEN_DATE_SELECTED, separator, getInputValidationListener()); 169 | this.defaultPlaceholder = null; 170 | clearSelectedDate(); 171 | commitEdit(); 172 | } 173 | repaint(); 174 | } 175 | } 176 | 177 | public void now() { 178 | LocalDate date = LocalDate.now(); 179 | if (getDateSelectionMode() == DateSelectionMode.BETWEEN_DATE_SELECTED) { 180 | setSelectedDateRange(date, date); 181 | } else { 182 | setSelectedDate(date); 183 | } 184 | } 185 | 186 | public void toDateSelectionView() { 187 | LocalDate date = getSelectedDate(); 188 | if (date == null) { 189 | date = LocalDate.now(); 190 | } 191 | int m = date.getMonthValue() - 1; 192 | int y = date.getYear(); 193 | if (selectionState != SelectionState.DATE || y != year || m != month) { 194 | panelSlider.addSlide(createPanelDate(m, y), getSliderTransition(SimpleTransition.SliderType.DEFAULT)); 195 | month = m; 196 | year = y; 197 | selectionState = SelectionState.DATE; 198 | header.setDate(month, year); 199 | updateSelected(); 200 | } 201 | } 202 | 203 | public void selectCurrentMonth() { 204 | LocalDate date = LocalDate.now(); 205 | if (getDateSelectionMode() == DateSelectionMode.BETWEEN_DATE_SELECTED) { 206 | setSelectedDateRange(date.withDayOfMonth(1), date); 207 | } else { 208 | setSelectedDate(date); 209 | } 210 | } 211 | 212 | public void setSelectedDate(LocalDate date) { 213 | dateSelectionModel.setDate(new SingleDate(date)); 214 | if (getDateSelectionMode() == DateSelectionMode.BETWEEN_DATE_SELECTED) { 215 | dateSelectionModel.setToDate(new SingleDate(date)); 216 | } 217 | slideTo(date); 218 | } 219 | 220 | public void setSelectedDateRange(LocalDate from, LocalDate to) { 221 | if (getDateSelectionMode() == DateSelectionMode.SINGLE_DATE_SELECTED) { 222 | throw new IllegalArgumentException("Single date mode can't accept the range date"); 223 | } 224 | dateSelectionModel.setSelectDate(new SingleDate(from), new SingleDate(to)); 225 | slideTo(from); 226 | } 227 | 228 | public void setEditor(JFormattedTextField editor) { 229 | if (editor != this.editor) { 230 | JFormattedTextField old = this.editor; 231 | if (old != null) { 232 | uninstallEditor(old); 233 | } 234 | this.editor = editor; 235 | if (this.editor != null) { 236 | installEditor(editor); 237 | if (editorValidation) { 238 | validChanged(editor, isValid); 239 | } else { 240 | validChanged(editor, true); 241 | } 242 | } 243 | } 244 | } 245 | 246 | public Icon getEditorIcon() { 247 | return editorIcon; 248 | } 249 | 250 | public void setEditorIcon(Icon editorIcon) { 251 | this.editorIcon = editorIcon; 252 | editorButton.setIcon(editorIcon); 253 | } 254 | 255 | public DateSelectionAble getDateSelectionAble() { 256 | return dateSelectionModel.getDateSelectionAble(); 257 | } 258 | 259 | public void setDateSelectionAble(DateSelectionAble dateSelectionAble) { 260 | this.dateSelectionModel.setDateSelectionAble(dateSelectionAble); 261 | if (selectionState == SelectionState.DATE) { 262 | panelDate.load(); 263 | } 264 | commitEdit(); 265 | } 266 | 267 | public Color getColor() { 268 | return color; 269 | } 270 | 271 | public void setColor(Color color) { 272 | this.color = color; 273 | repaint(); 274 | } 275 | 276 | public String getSeparator() { 277 | return separator; 278 | } 279 | 280 | public void setSeparator(String separator) { 281 | if (separator == null) { 282 | throw new IllegalArgumentException("separator can't be null"); 283 | } 284 | if (!this.separator.equals(separator)) { 285 | this.separator = separator; 286 | if (editor != null) { 287 | InputUtils.changeDateFormatted(editor, dateFormatPattern, getDateSelectionMode() == DateSelectionMode.BETWEEN_DATE_SELECTED, separator, getInputValidationListener()); 288 | this.defaultPlaceholder = null; 289 | setEditorValue(); 290 | } 291 | } 292 | } 293 | 294 | public String getDateFormat() { 295 | return this.dateFormatPattern; 296 | } 297 | 298 | public void setDateFormat(String format) { 299 | if (format == null) { 300 | throw new IllegalArgumentException("format can't be null"); 301 | } 302 | if (!this.dateFormatPattern.equals(format)) { 303 | this.format = DateTimeFormatter.ofPattern(format); 304 | if (editor != null) { 305 | InputUtils.changeDateFormatted(editor, format, getDateSelectionMode() == DateSelectionMode.BETWEEN_DATE_SELECTED, separator, getInputValidationListener()); 306 | this.defaultPlaceholder = null; 307 | } 308 | this.dateFormatPattern = format; 309 | } 310 | } 311 | 312 | public boolean isUsePanelOption() { 313 | return usePanelOption; 314 | } 315 | 316 | public void setUsePanelOption(boolean usePanelOption) { 317 | if (this.usePanelOption != usePanelOption) { 318 | this.usePanelOption = usePanelOption; 319 | if (usePanelOption) { 320 | if (panelDateOption == null) { 321 | panelDateOption = new PanelDateOption(this); 322 | panelDateOption.installDateOptionLabel(); 323 | } 324 | add(panelDateOption, "dock east,gap 0 10 10 10"); 325 | repaint(); 326 | revalidate(); 327 | } else { 328 | if (panelDateOption != null) { 329 | remove(panelDateOption); 330 | panelDateOption = null; 331 | repaint(); 332 | revalidate(); 333 | } 334 | } 335 | } 336 | } 337 | 338 | public PanelDateOptionLabel getPanelDateOptionLabel() { 339 | return panelDateOptionLabel; 340 | } 341 | 342 | public void setPanelDateOptionLabel(PanelDateOptionLabel panelDateOptionLabel) { 343 | if (panelDateOptionLabel == null) { 344 | throw new IllegalArgumentException("panelDateOptionLabel can't be null"); 345 | } 346 | this.panelDateOptionLabel = panelDateOptionLabel; 347 | if (panelDateOption != null) { 348 | panelDateOption.installDateOptionLabel(); 349 | } 350 | } 351 | 352 | public boolean isCloseAfterSelected() { 353 | return closeAfterSelected; 354 | } 355 | 356 | public void setCloseAfterSelected(boolean closeAfterSelected) { 357 | this.closeAfterSelected = closeAfterSelected; 358 | } 359 | 360 | public boolean isAnimationEnabled() { 361 | return animationEnabled; 362 | } 363 | 364 | public void setAnimationEnabled(boolean animationEnabled) { 365 | this.animationEnabled = animationEnabled; 366 | } 367 | 368 | public boolean isStartWeekOnMonday() { 369 | return startWeekOnMonday; 370 | } 371 | 372 | public void setStartWeekOnMonday(boolean startWeekOnMonday) { 373 | if (this.startWeekOnMonday != startWeekOnMonday) { 374 | this.startWeekOnMonday = startWeekOnMonday; 375 | if (selectionState == SelectionState.DATE && panelDate != null) { 376 | // update the panel date 377 | panelDate.load(); 378 | panelDate.repaint(); 379 | panelDate.revalidate(); 380 | } 381 | } 382 | } 383 | 384 | public float getSelectionArc() { 385 | return selectionArc; 386 | } 387 | 388 | public void setSelectionArc(float selectionArc) { 389 | if (this.selectionArc != selectionArc) { 390 | this.selectionArc = selectionArc; 391 | updateSelected(); 392 | } 393 | } 394 | 395 | public void clearSelectedDate() { 396 | dateSelectionModel.setSelectDate(null, null); 397 | updateSelected(); 398 | } 399 | 400 | public boolean isDateSelected() { 401 | if (getDateSelectionMode() == DateSelectionMode.SINGLE_DATE_SELECTED) { 402 | return dateSelectionModel.getDate() != null; 403 | } else { 404 | return dateSelectionModel.getDate() != null && dateSelectionModel.getToDate() != null; 405 | } 406 | } 407 | 408 | public LocalDate getSelectedDate() { 409 | SingleDate date = dateSelectionModel.getDate(); 410 | if (date != null) { 411 | return date.toLocalDate(); 412 | } 413 | return null; 414 | } 415 | 416 | public LocalDate[] getSelectedDateRange() { 417 | SingleDate from = dateSelectionModel.getDate(); 418 | if (from != null) { 419 | LocalDate[] dates = new LocalDate[2]; 420 | dates[0] = from.toLocalDate(); 421 | SingleDate to = dateSelectionModel.getToDate(); 422 | if (to != null) { 423 | dates[1] = to.toLocalDate(); 424 | return dates; 425 | } 426 | } 427 | return null; 428 | } 429 | 430 | public String getSelectedDateAsString() { 431 | if (isDateSelected()) { 432 | if (getDateSelectionMode() == DateSelectionMode.SINGLE_DATE_SELECTED) { 433 | return format.format(getSelectedDate()); 434 | } else { 435 | LocalDate[] dates = getSelectedDateRange(); 436 | return format.format(dates[0]) + separator + format.format(dates[1]); 437 | } 438 | } else { 439 | return null; 440 | } 441 | } 442 | 443 | public void slideTo(LocalDate date) { 444 | int m = date.getMonthValue() - 1; 445 | int y = date.getYear(); 446 | if (year != y || month != m) { 447 | if (year < y || (year <= y && month < m)) { 448 | panelSlider.addSlide(createPanelDate(m, y), getSliderTransition(SimpleTransition.SliderType.FORWARD)); 449 | } else { 450 | panelSlider.addSlide(createPanelDate(m, y), getSliderTransition(SimpleTransition.SliderType.BACK)); 451 | } 452 | month = m; 453 | year = y; 454 | selectionState = SelectionState.DATE; 455 | header.setDate(month, year); 456 | } else { 457 | if (selectionState != SelectionState.DATE) { 458 | panelSlider.addSlide(createPanelDate(m, y), getSliderTransition(SimpleTransition.SliderType.DOWN_TOP)); 459 | selectionState = SelectionState.DATE; 460 | } 461 | } 462 | updateSelected(); 463 | } 464 | 465 | public void addDateSelectionListener(DateSelectionListener listener) { 466 | listenerList.add(DateSelectionListener.class, listener); 467 | } 468 | 469 | public void removeDateSelectionListener(DateSelectionListener listener) { 470 | listenerList.remove(DateSelectionListener.class, listener); 471 | } 472 | 473 | public DefaultDateCellRenderer getDefaultDateCellRenderer() { 474 | return defaultDateCellRenderer; 475 | } 476 | 477 | public void setDefaultDateCellRenderer(DefaultDateCellRenderer defaultDateCellRenderer) { 478 | this.defaultDateCellRenderer = defaultDateCellRenderer; 479 | repaint(); 480 | } 481 | 482 | public Header getHeader() { 483 | return header; 484 | } 485 | 486 | public DateSelectionModel getDateSelectionModel() { 487 | return dateSelectionModel; 488 | } 489 | 490 | public void setDateSelectionModel(DateSelectionModel dateSelectionModel) { 491 | if (dateSelectionModel == null) { 492 | throw new IllegalArgumentException("dateSelectionModel can't be null"); 493 | } 494 | if (this.dateSelectionModel != dateSelectionModel) { 495 | DateSelectionModel old = this.dateSelectionModel; 496 | if (old != null) { 497 | old.removeDatePickerSelectionListener(this); 498 | } 499 | this.dateSelectionModel = dateSelectionModel; 500 | this.dateSelectionModel.addDatePickerSelectionListener(this); 501 | } 502 | } 503 | 504 | @Override 505 | public void dateSelectionModelChanged(DateSelectionModelEvent e) { 506 | if (e.getAction() == DateSelectionModelEvent.DATE) { 507 | verifyDateSelection(); 508 | } 509 | repaint(); 510 | } 511 | 512 | @Override 513 | public void dateControlChanged(DateControlEvent e) { 514 | if (e.getType() == DateControlEvent.BACK) { 515 | setToBack(); 516 | } else if (e.getType() == DateControlEvent.FORWARD) { 517 | setToForward(); 518 | } else if (e.getType() == DateControlEvent.MONTH) { 519 | selectMonth(); 520 | } else if (e.getType() == DateControlEvent.YEAR) { 521 | selectYear(); 522 | } 523 | } 524 | 525 | @Override 526 | public void stateChanged(ChangeEvent e) { 527 | if (e.getSource() == panelMonth) { 528 | this.month = panelMonth.getSelectedMonth(); 529 | header.setDate(month, year); 530 | panelSlider.addSlide(createPanelDate(month, year), getSliderTransition(SimpleTransition.SliderType.DOWN_TOP)); 531 | selectionState = SelectionState.DATE; 532 | } else if (e.getSource() == panelYear) { 533 | this.year = panelYear.getSelectedYear(); 534 | header.setDate(month, year); 535 | panelSlider.addSlide(createPanelMonth(year), getSliderTransition(SimpleTransition.SliderType.DOWN_TOP)); 536 | selectionState = SelectionState.MONTH; 537 | } 538 | } 539 | 540 | protected DateSelectionModel createDefaultDateSelection() { 541 | return new DateSelectionModel(); 542 | } 543 | 544 | protected SliderTransition getSliderTransition(SimpleTransition.SliderType type) { 545 | if (!animationEnabled) { 546 | return null; 547 | } 548 | return SimpleTransition.get(type); 549 | } 550 | 551 | private void updateSelected() { 552 | if (selectionState == SelectionState.DATE) { 553 | panelDate.checkSelection(); 554 | } else if (selectionState == SelectionState.MONTH) { 555 | panelMonth.checkSelection(); 556 | } else if (selectionState == SelectionState.YEAR) { 557 | panelYear.checkSelection(); 558 | } 559 | } 560 | 561 | private void installEditor(JFormattedTextField editor) { 562 | if (editor != null) { 563 | JToolBar toolBar = new JToolBar(); 564 | editorButton = new JButton(editorIcon != null ? editorIcon : new FlatSVGIcon("raven/datetime/icon/calendar.svg", 0.8f)); 565 | toolBar.add(editorButton); 566 | editorButton.addActionListener(e -> { 567 | if (editor.isEnabled()) { 568 | editor.grabFocus(); 569 | showPopup(); 570 | } 571 | }); 572 | InputUtils.useDateInput(editor, dateFormatPattern, getDateSelectionMode() == DateSelectionMode.BETWEEN_DATE_SELECTED, separator, getValueCallback(), getInputValidationListener()); 573 | setEditorValue(); 574 | editor.putClientProperty(FlatClientProperties.TEXT_FIELD_TRAILING_COMPONENT, toolBar); 575 | addDateSelectionListener(getDateSelectionListener()); 576 | } 577 | } 578 | 579 | private void uninstallEditor(JFormattedTextField editor) { 580 | if (editor != null) { 581 | editorButton = null; 582 | InputUtils.removePropertyChange(editor); 583 | if (dateSelectionListener != null) { 584 | removeDateSelectionListener(dateSelectionListener); 585 | } 586 | } 587 | } 588 | 589 | private InputUtils.ValueCallback getValueCallback() { 590 | if (valueCallback == null) { 591 | valueCallback = value -> { 592 | if (value == null && isDateSelected()) { 593 | clearSelectedDate(); 594 | } else { 595 | if (value != null && !value.equals(getSelectedDateAsString())) { 596 | if (getDateSelectionMode() == DateSelectionMode.SINGLE_DATE_SELECTED) { 597 | LocalDate date = InputUtils.stringToDate(format, value.toString()); 598 | if (date != null) { 599 | setSelectedDate(date); 600 | } 601 | } else { 602 | LocalDate[] dates = InputUtils.stringToDate(format, separator, value.toString()); 603 | if (dates != null) { 604 | setSelectedDateRange(dates[0], dates[1]); 605 | } 606 | } 607 | } 608 | } 609 | }; 610 | } 611 | return valueCallback; 612 | } 613 | 614 | private DateSelectionListener getDateSelectionListener() { 615 | if (dateSelectionListener == null) { 616 | dateSelectionListener = dateSelectionEvent -> setEditorValue(); 617 | } 618 | return dateSelectionListener; 619 | } 620 | 621 | private void setEditorValue() { 622 | String value = getSelectedDateAsString(); 623 | if (value != null) { 624 | if (!editor.getText().equalsIgnoreCase(value)) { 625 | editor.setValue(value); 626 | } 627 | } else { 628 | editor.setValue(null); 629 | } 630 | } 631 | 632 | private InputValidationListener getInputValidationListener() { 633 | if (inputValidationListener == null) { 634 | inputValidationListener = new InputValidationListener() { 635 | 636 | @Override 637 | public boolean isValidation() { 638 | return dateSelectionModel.getDateSelectionAble() != null; 639 | } 640 | 641 | @Override 642 | public void inputChanged(boolean status) { 643 | checkValidation(status); 644 | } 645 | 646 | @Override 647 | public boolean checkSelectionAble(LocalDate date) { 648 | if (dateSelectionModel.getDateSelectionAble() == null) return true; 649 | return dateSelectionModel.getDateSelectionAble().isDateSelectedAble(date); 650 | } 651 | }; 652 | } 653 | return inputValidationListener; 654 | } 655 | 656 | @Override 657 | protected String getDefaultPlaceholder() { 658 | if (defaultPlaceholder == null) { 659 | if (getDateSelectionMode() == DateSelectionMode.BETWEEN_DATE_SELECTED) { 660 | String d = InputUtils.datePatternToInputFormat(dateFormatPattern, "-"); 661 | defaultPlaceholder = d + separator + d; 662 | } else { 663 | defaultPlaceholder = InputUtils.datePatternToInputFormat(dateFormatPattern, "-"); 664 | } 665 | } 666 | return defaultPlaceholder; 667 | } 668 | 669 | @Override 670 | protected void popupOpen() { 671 | toDateSelectionView(); 672 | } 673 | 674 | private void verifyDateSelection() { 675 | if (getDateSelectionMode() == DateSelectionMode.BETWEEN_DATE_SELECTED) { 676 | SingleDate fromDate = dateSelectionModel.getDate(); 677 | SingleDate toDate = dateSelectionModel.getToDate(); 678 | if ((fromDate == null && toDate != null) || (fromDate != null && toDate == null)) { 679 | return; 680 | } 681 | } 682 | if (isCloseAfterSelected()) { 683 | closePopup(); 684 | } 685 | fireDateSelectionChanged(new DateSelectionEvent(this)); 686 | if (panelDateOption != null) { 687 | panelDateOption.setSelectedCustom(); 688 | } 689 | } 690 | 691 | public void fireDateSelectionChanged(DateSelectionEvent event) { 692 | Object[] listeners = listenerList.getListenerList(); 693 | for (int i = listeners.length - 2; i >= 0; i -= 2) { 694 | if (listeners[i] == DateSelectionListener.class) { 695 | ((DateSelectionListener) listeners[i + 1]).dateSelected(event); 696 | } 697 | } 698 | } 699 | 700 | private void setPanelDate(PanelDate panelDate) { 701 | this.panelDate = panelDate; 702 | } 703 | 704 | private void setPanelMonth(PanelMonth panelMonth) { 705 | PanelMonth old = this.panelMonth; 706 | if (old != null) { 707 | old.removeChangeListener(this); 708 | } 709 | this.panelMonth = panelMonth; 710 | this.panelMonth.addChangeListener(this); 711 | } 712 | 713 | private void setPanelYear(PanelYear panelYear) { 714 | PanelYear old = this.panelYear; 715 | if (old != null) { 716 | old.removeChangeListener(this); 717 | } 718 | this.panelYear = panelYear; 719 | this.panelYear.addChangeListener(this); 720 | } 721 | 722 | private PanelDate createPanelDate(int month, int year) { 723 | PanelDate panelDate = new PanelDate(this, month, year); 724 | setPanelDate(panelDate); 725 | return panelDate; 726 | } 727 | 728 | private PanelMonth createPanelMonth(int year) { 729 | PanelMonth panelMonth = new PanelMonth(this, year); 730 | setPanelMonth(panelMonth); 731 | return panelMonth; 732 | } 733 | 734 | private PanelYear createPanelYear(int year) { 735 | PanelYear panelYear = new PanelYear(this, year); 736 | setPanelYear(panelYear); 737 | return panelYear; 738 | } 739 | 740 | public static void setDefaultWeekdays(String[] defaultWeekdays) { 741 | if (defaultWeekdays == null) { 742 | DatePicker.defaultWeekdays = null; 743 | } else { 744 | DatePicker.defaultWeekdays = Arrays.copyOf(defaultWeekdays, defaultWeekdays.length); 745 | } 746 | } 747 | 748 | public static String[] getDefaultWeekdays() { 749 | if (defaultWeekdays == null) { 750 | return null; 751 | } 752 | return Arrays.copyOf(defaultWeekdays, defaultWeekdays.length); 753 | } 754 | 755 | public enum DateSelectionMode { 756 | SINGLE_DATE_SELECTED, BETWEEN_DATE_SELECTED 757 | } 758 | 759 | private enum SelectionState { 760 | DATE, MONTH, YEAR 761 | } 762 | } 763 | -------------------------------------------------------------------------------- /src/main/java/raven/datetime/DateSelectionAble.java: -------------------------------------------------------------------------------- 1 | package raven.datetime; 2 | 3 | import java.time.LocalDate; 4 | 5 | public interface DateSelectionAble { 6 | 7 | boolean isDateSelectedAble(LocalDate date); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/raven/datetime/PanelDateOption.java: -------------------------------------------------------------------------------- 1 | package raven.datetime; 2 | 3 | import com.formdev.flatlaf.FlatClientProperties; 4 | import net.miginfocom.swing.MigLayout; 5 | 6 | import javax.swing.*; 7 | import java.awt.*; 8 | import java.time.LocalDate; 9 | import java.util.List; 10 | 11 | public class PanelDateOption extends JPanel { 12 | 13 | private final DatePicker datePicker; 14 | private boolean disableChange; 15 | 16 | public PanelDateOption(DatePicker datePicker) { 17 | this.datePicker = datePicker; 18 | init(); 19 | } 20 | 21 | private void init() { 22 | putClientProperty(FlatClientProperties.STYLE, "" + 23 | "background:null"); 24 | buttonGroup = new ButtonGroup(); 25 | } 26 | 27 | public void installDateOptionLabel() { 28 | removeAll(); 29 | PanelDateOptionLabel panelDateOptionLabel = datePicker.getPanelDateOptionLabel(); 30 | if (panelDateOptionLabel == null) { 31 | panelDateOptionLabel = createDefaultPanelDateOptionLabel(); 32 | } 33 | String layoutRowConstraints = ""; 34 | List items = panelDateOptionLabel.getListItems(); 35 | for (int i = 0; i < items.size(); i++) { 36 | PanelDateOptionLabel.Item item = items.get(i); 37 | add(createButton(item.getLabel(), item.getCallback())); 38 | if (item.getCallback() == null) { 39 | layoutRowConstraints += "push"; 40 | } else { 41 | layoutRowConstraints += "[]"; 42 | } 43 | } 44 | layoutRowConstraints += "[]"; 45 | setLayout(new MigLayout("wrap,insets 5,fillx", "[fill]", layoutRowConstraints)); 46 | add(new JSeparator(SwingConstants.VERTICAL), "dock west,height 100%", 0); 47 | repaint(); 48 | revalidate(); 49 | } 50 | 51 | private JToggleButton createButton(String name, PanelDateOptionLabel.LabelCallback callback) { 52 | JToggleButton button = new JToggleButton(name); 53 | button.setHorizontalAlignment(SwingConstants.LEADING); 54 | if (callback == null) { 55 | button.setName("custom"); 56 | } 57 | button.addActionListener(e -> { 58 | disableChange = true; 59 | boolean isEnable = datePicker.isEnabled(); 60 | if (callback == null) { 61 | if (isEnable) { 62 | datePicker.clearSelectedDate(); 63 | } 64 | } else { 65 | LocalDate[] dates = callback.getDate(); 66 | if (dates.length == 0) { 67 | throw new IllegalArgumentException("Date option is empty so can't be select"); 68 | } 69 | boolean singleDate = dates.length == 1 || datePicker.getDateSelectionMode() == DatePicker.DateSelectionMode.SINGLE_DATE_SELECTED; 70 | if (isEnable) { 71 | if (singleDate) { 72 | datePicker.setSelectedDate(dates[0]); 73 | } else { 74 | datePicker.setSelectedDateRange(dates[0], dates[1]); 75 | } 76 | } else { 77 | datePicker.slideTo(dates[0]); 78 | } 79 | } 80 | }); 81 | button.putClientProperty(FlatClientProperties.STYLE, "" + 82 | "arc:10;" + 83 | "borderWidth:0;" + 84 | "focusWidth:0;" + 85 | "innerFocusWidth:0;" + 86 | "margin:4,10,4,10;" + 87 | "background:null"); 88 | buttonGroup.add(button); 89 | return button; 90 | } 91 | 92 | public void setSelectedCustom() { 93 | if (!disableChange) { 94 | JToggleButton customButton = null; 95 | for (Component com : getComponents()) { 96 | String name = com.getName(); 97 | if (name != null && name.equals("custom")) { 98 | customButton = (JToggleButton) com; 99 | break; 100 | } 101 | } 102 | if (customButton != null) { 103 | customButton.setSelected(true); 104 | } 105 | } 106 | disableChange = false; 107 | } 108 | 109 | private PanelDateOptionLabel createDefaultPanelDateOptionLabel() { 110 | PanelDateOptionLabel defaultPanelDateOptionLabel = new PanelDateOptionLabel(); 111 | defaultPanelDateOptionLabel.add("Today", PanelDateOptionLabel.LabelCallback.TODAY); 112 | defaultPanelDateOptionLabel.add("Yesterday", PanelDateOptionLabel.LabelCallback.YESTERDAY); 113 | defaultPanelDateOptionLabel.add("Last 7 Days", PanelDateOptionLabel.LabelCallback.LAST_7_DAYS); 114 | defaultPanelDateOptionLabel.add("Last 30 Days", PanelDateOptionLabel.LabelCallback.LAST_30_DAYS); 115 | defaultPanelDateOptionLabel.add("This Month", PanelDateOptionLabel.LabelCallback.THIS_MONTH); 116 | defaultPanelDateOptionLabel.add("Last Month", PanelDateOptionLabel.LabelCallback.LAST_MONTH); 117 | defaultPanelDateOptionLabel.add("Last Year", PanelDateOptionLabel.LabelCallback.LAST_YEAR); 118 | defaultPanelDateOptionLabel.add("Custom", PanelDateOptionLabel.LabelCallback.CUSTOM); 119 | 120 | return defaultPanelDateOptionLabel; 121 | } 122 | 123 | private ButtonGroup buttonGroup; 124 | } 125 | -------------------------------------------------------------------------------- /src/main/java/raven/datetime/PanelDateOptionLabel.java: -------------------------------------------------------------------------------- 1 | package raven.datetime; 2 | 3 | import java.time.LocalDate; 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | 7 | public class PanelDateOptionLabel { 8 | 9 | private final List listItems = new ArrayList<>(); 10 | 11 | public PanelDateOptionLabel() { 12 | } 13 | 14 | public void add(String label, LabelCallback callback) { 15 | listItems.add(new Item(label, callback)); 16 | } 17 | 18 | public List getListItems() { 19 | return listItems; 20 | } 21 | 22 | private void remove(int index) { 23 | listItems.remove(index); 24 | } 25 | 26 | private void clear() { 27 | listItems.clear(); 28 | } 29 | 30 | public static class Item { 31 | 32 | public String getLabel() { 33 | return label; 34 | } 35 | 36 | public LabelCallback getCallback() { 37 | return callback; 38 | } 39 | 40 | public Item(String label, LabelCallback callback) { 41 | this.label = label; 42 | this.callback = callback; 43 | } 44 | 45 | private final String label; 46 | private final LabelCallback callback; 47 | } 48 | 49 | public interface LabelCallback { 50 | 51 | LabelCallback TODAY = () -> new LocalDate[]{LocalDate.now()}; 52 | LabelCallback YESTERDAY = () -> new LocalDate[]{LocalDate.now().minusDays(1)}; 53 | LabelCallback LAST_7_DAYS = () -> new LocalDate[]{LocalDate.now().minusDays(7), LocalDate.now()}; 54 | LabelCallback LAST_30_DAYS = () -> new LocalDate[]{LocalDate.now().minusDays(30), LocalDate.now()}; 55 | LabelCallback THIS_MONTH = () -> { 56 | LocalDate now = LocalDate.now(); 57 | return new LocalDate[]{now.withDayOfMonth(1), now.withDayOfMonth(now.lengthOfMonth())}; 58 | }; 59 | LabelCallback LAST_MONTH = () -> { 60 | LocalDate lastMonth = LocalDate.now().minusMonths(1); 61 | return new LocalDate[]{lastMonth.withDayOfMonth(1), lastMonth.withDayOfMonth(lastMonth.lengthOfMonth())}; 62 | }; 63 | LabelCallback LAST_YEAR = () -> { 64 | LocalDate lastYear = LocalDate.now().minusYears(1).withDayOfYear(1); 65 | return new LocalDate[]{lastYear, lastYear.withDayOfYear(lastYear.lengthOfYear())}; 66 | }; 67 | 68 | LabelCallback CUSTOM = null; 69 | 70 | LocalDate[] getDate(); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/raven/datetime/TimePicker.java: -------------------------------------------------------------------------------- 1 | package raven.datetime; 2 | 3 | import com.formdev.flatlaf.FlatClientProperties; 4 | import com.formdev.flatlaf.extras.FlatSVGIcon; 5 | import net.miginfocom.swing.MigLayout; 6 | import raven.datetime.component.PanelPopupEditor; 7 | import raven.datetime.component.time.Header; 8 | import raven.datetime.component.time.PanelClock; 9 | import raven.datetime.component.time.TimeSelectionModel; 10 | import raven.datetime.component.time.event.TimeActionListener; 11 | import raven.datetime.component.time.event.TimeSelectionModelEvent; 12 | import raven.datetime.component.time.event.TimeSelectionModelListener; 13 | import raven.datetime.event.TimeSelectionEvent; 14 | import raven.datetime.event.TimeSelectionListener; 15 | import raven.datetime.util.InputUtils; 16 | import raven.datetime.util.InputValidationListener; 17 | 18 | import javax.swing.*; 19 | import java.awt.*; 20 | import java.time.LocalTime; 21 | import java.time.format.DateTimeFormatter; 22 | import java.util.Locale; 23 | 24 | public class TimePicker extends PanelPopupEditor implements TimeSelectionModelListener { 25 | 26 | private final DateTimeFormatter format12h = DateTimeFormatter.ofPattern("hh:mm a", Locale.ENGLISH); 27 | private final DateTimeFormatter format24h = DateTimeFormatter.ofPattern("HH:mm", Locale.ENGLISH); 28 | private TimeSelectionModel timeSelectionModel; 29 | private TimeSelectionListener timeSelectionListener; 30 | private InputValidationListener inputValidationListener; 31 | private InputUtils.ValueCallback valueCallback; 32 | private Icon editorIcon; 33 | private MigLayout layout; 34 | private int orientation = SwingConstants.VERTICAL; 35 | private Color color; 36 | private JButton editorButton; 37 | private LocalTime oldSelectedTime; 38 | 39 | private Header header; 40 | private PanelClock panelClock; 41 | 42 | public TimePicker() { 43 | this(null); 44 | } 45 | 46 | public TimePicker(TimeSelectionModel model) { 47 | init(model); 48 | } 49 | 50 | private void init(TimeSelectionModel model) { 51 | putClientProperty(FlatClientProperties.STYLE, "" + 52 | "[light]background:darken($Panel.background,2%);" + 53 | "[dark]background:lighten($Panel.background,2%);"); 54 | layout = new MigLayout( 55 | "wrap,fill,insets 3", 56 | "fill", 57 | "fill"); 58 | setLayout(layout); 59 | 60 | if (model == null) { 61 | model = createDefaultTimeSelection(); 62 | } 63 | setTimeSelectionModel(model); 64 | 65 | header = new Header(this, new HeaderActionListener()); 66 | panelClock = new PanelClock(this, new ClockActionListener()); 67 | add(header, "width 120:120"); 68 | add(panelClock, "width 230:230,height 230:230"); 69 | } 70 | 71 | public int getOrientation() { 72 | return orientation; 73 | } 74 | 75 | public void setOrientation(int orientation) { 76 | if (this.orientation != orientation) { 77 | String c = orientation == SwingConstants.VERTICAL ? "wrap," : ""; 78 | layout.setLayoutConstraints(c + "fill,insets 3"); 79 | this.orientation = orientation; 80 | header.setOrientation(orientation); 81 | revalidate(); 82 | } 83 | } 84 | 85 | public void setEditor(JFormattedTextField editor) { 86 | if (editor != this.editor) { 87 | if (this.editor != null) { 88 | uninstallEditor(this.editor); 89 | } 90 | this.editor = editor; 91 | if (editor != null) { 92 | installEditor(editor); 93 | if (editorValidation) { 94 | validChanged(editor, isValid); 95 | } else { 96 | validChanged(editor, true); 97 | } 98 | } 99 | } 100 | } 101 | 102 | public boolean is24HourView() { 103 | return panelClock.isUse24hour(); 104 | } 105 | 106 | public void set24HourView(boolean hour24) { 107 | if (panelClock.isUse24hour() != hour24) { 108 | panelClock.setUse24hour(hour24); 109 | header.setUse24hour(hour24); 110 | panelClock.updateClock(); 111 | header.updateHeader(); 112 | repaint(); 113 | if (editor != null) { 114 | InputUtils.changeTimeFormatted(editor, hour24, getInputValidationListener()); 115 | this.defaultPlaceholder = null; 116 | setEditorValue(); 117 | } 118 | } 119 | } 120 | 121 | public Color getColor() { 122 | return color; 123 | } 124 | 125 | public void setColor(Color color) { 126 | this.color = color; 127 | header.setColor(color); 128 | panelClock.setColor(color); 129 | } 130 | 131 | public Icon getEditorIcon() { 132 | return editorIcon; 133 | } 134 | 135 | public void setEditorIcon(Icon editorIcon) { 136 | this.editorIcon = editorIcon; 137 | if (editorButton != null) { 138 | editorButton.setIcon(editorIcon); 139 | } 140 | } 141 | 142 | /** 143 | * Set time to current local time 144 | */ 145 | public void now() { 146 | setSelectedTime(LocalTime.now()); 147 | } 148 | 149 | public void setSelectedTime(LocalTime time) { 150 | if (time == null) { 151 | clearSelectedTime(); 152 | } else { 153 | int hour = time.getHour(); 154 | int minute = time.getMinute(); 155 | timeSelectionModel.set(hour, minute); 156 | } 157 | } 158 | 159 | public void clearSelectedTime() { 160 | timeSelectionModel.set(-1, -1); 161 | } 162 | 163 | public boolean isTimeSelected() { 164 | return timeSelectionModel.isSelected(); 165 | } 166 | 167 | public LocalTime getSelectedTime() { 168 | return timeSelectionModel.getTime(); 169 | } 170 | 171 | public String getSelectedTimeAsString() { 172 | if (isTimeSelected()) { 173 | if (panelClock.isUse24hour()) { 174 | return format24h.format(getSelectedTime()); 175 | } else { 176 | return format12h.format(getSelectedTime()); 177 | } 178 | } else { 179 | return null; 180 | } 181 | } 182 | 183 | protected TimeSelectionModel createDefaultTimeSelection() { 184 | return new TimeSelectionModel(); 185 | } 186 | 187 | public TimeSelectionAble getTimeSelectionAble() { 188 | return timeSelectionModel.getTimeSelectionAble(); 189 | } 190 | 191 | public void setTimeSelectionAble(TimeSelectionAble timeSelectionAble) { 192 | timeSelectionModel.setTimeSelectionAble(timeSelectionAble); 193 | commitEdit(); 194 | } 195 | 196 | public TimeSelectionModel getTimeSelectionModel() { 197 | return timeSelectionModel; 198 | } 199 | 200 | public void setTimeSelectionModel(TimeSelectionModel timeSelectionModel) { 201 | if (timeSelectionModel == null) { 202 | throw new IllegalArgumentException("timeSelectionModel can't be null"); 203 | } 204 | if (this.timeSelectionModel != timeSelectionModel) { 205 | TimeSelectionModel old = this.timeSelectionModel; 206 | if (old != null) { 207 | old.removeTimePickerSelectionListener(this); 208 | } 209 | this.timeSelectionModel = timeSelectionModel; 210 | this.timeSelectionModel.addTimePickerSelectionListener(this); 211 | } 212 | } 213 | 214 | public boolean isHourSelectionView() { 215 | return panelClock.isHourSelectionView(); 216 | } 217 | 218 | public void setHourSelectionView(boolean hourSelectionView) { 219 | header.setHourSelectionView(hourSelectionView); 220 | panelClock.setHourSelectionViewImmediately(hourSelectionView); 221 | } 222 | 223 | public void addTimeSelectionListener(TimeSelectionListener listener) { 224 | listenerList.add(TimeSelectionListener.class, listener); 225 | } 226 | 227 | public void removeTimeSelectionListener(TimeSelectionListener listener) { 228 | listenerList.remove(TimeSelectionListener.class, listener); 229 | } 230 | 231 | private void installEditor(JFormattedTextField editor) { 232 | JToolBar toolBar = new JToolBar(); 233 | editorButton = new JButton(editorIcon != null ? editorIcon : new FlatSVGIcon("raven/datetime/icon/clock.svg", 0.8f)); 234 | toolBar.add(editorButton); 235 | editorButton.addActionListener(e -> { 236 | if (editor.isEnabled()) { 237 | editor.grabFocus(); 238 | showPopup(); 239 | } 240 | }); 241 | InputUtils.useTimeInput(editor, panelClock.isUse24hour(), getValueCallback(), getInputValidationListener()); 242 | setEditorValue(); 243 | editor.putClientProperty(FlatClientProperties.TEXT_FIELD_TRAILING_COMPONENT, toolBar); 244 | addTimeSelectionListener(getTimeSelectionListener()); 245 | } 246 | 247 | private void uninstallEditor(JFormattedTextField editor) { 248 | if (editor != null) { 249 | editorButton = null; 250 | InputUtils.removePropertyChange(editor); 251 | if (timeSelectionListener != null) { 252 | removeTimeSelectionListener(timeSelectionListener); 253 | } 254 | } 255 | } 256 | 257 | private InputUtils.ValueCallback getValueCallback() { 258 | if (valueCallback == null) { 259 | valueCallback = value -> { 260 | if (value == null && isTimeSelected()) { 261 | clearSelectedTime(); 262 | } else { 263 | if (value != null && !value.equals(getSelectedTimeAsString())) { 264 | LocalTime time = InputUtils.stringToTime(panelClock.isUse24hour(), value.toString()); 265 | if (time != null) { 266 | setSelectedTime(time); 267 | } 268 | } 269 | } 270 | }; 271 | } 272 | return valueCallback; 273 | } 274 | 275 | private TimeSelectionListener getTimeSelectionListener() { 276 | if (timeSelectionListener == null) { 277 | timeSelectionListener = timeSelectionEvent -> setEditorValue(); 278 | } 279 | return timeSelectionListener; 280 | } 281 | 282 | private void setEditorValue() { 283 | String value = getSelectedTimeAsString(); 284 | if (value != null) { 285 | if (!editor.getText().equalsIgnoreCase(value)) { 286 | editor.setValue(value); 287 | } 288 | } else { 289 | editor.setValue(null); 290 | } 291 | } 292 | 293 | private InputValidationListener getInputValidationListener() { 294 | if (inputValidationListener == null) { 295 | inputValidationListener = new InputValidationListener() { 296 | 297 | @Override 298 | public boolean isValidation() { 299 | return timeSelectionModel.getTimeSelectionAble() != null; 300 | } 301 | 302 | @Override 303 | public void inputChanged(boolean status) { 304 | checkValidation(status); 305 | } 306 | 307 | @Override 308 | public boolean checkSelectionAble(LocalTime time) { 309 | int hour = time.getHour(); 310 | int minute = time.getMinute(); 311 | return timeSelectionModel.checkSelection(hour, minute); 312 | } 313 | }; 314 | } 315 | return inputValidationListener; 316 | } 317 | 318 | @Override 319 | protected String getDefaultPlaceholder() { 320 | if (defaultPlaceholder == null) { 321 | String pattern; 322 | if (is24HourView()) { 323 | pattern = "--:--"; 324 | } else { 325 | pattern = "--:-- --"; 326 | } 327 | defaultPlaceholder = pattern; 328 | } 329 | return defaultPlaceholder; 330 | } 331 | 332 | @Override 333 | protected void popupOpen() { 334 | setHourSelectionView(true); 335 | } 336 | 337 | private void verifyTimeSelection() { 338 | LocalTime time = getSelectedTime(); 339 | if ((time == null && oldSelectedTime == null)) { 340 | return; 341 | } else if (time != null && oldSelectedTime != null) { 342 | if (time.equals(oldSelectedTime)) { 343 | return; 344 | } 345 | } 346 | oldSelectedTime = time; 347 | fireTimeSelectionChanged(new TimeSelectionEvent(this)); 348 | } 349 | 350 | public void fireTimeSelectionChanged(TimeSelectionEvent event) { 351 | Object[] listeners = listenerList.getListenerList(); 352 | for (int i = listeners.length - 2; i >= 0; i -= 2) { 353 | if (listeners[i] == TimeSelectionListener.class) { 354 | ((TimeSelectionListener) listeners[i + 1]).timeSelected(event); 355 | } 356 | } 357 | } 358 | 359 | @Override 360 | public void setEnabled(boolean enabled) { 361 | super.setEnabled(enabled); 362 | header.setEnabled(enabled); 363 | panelClock.setEnabled(enabled); 364 | } 365 | 366 | public Header getHeader() { 367 | return header; 368 | } 369 | 370 | @Override 371 | public void timeSelectionModelChanged(TimeSelectionModelEvent e) { 372 | // check if user clear the time selection. so we set to the hour selection view 373 | if (!timeSelectionModel.isSelected() && e.getAction() == TimeSelectionModelEvent.HOUR_MINUTE) { 374 | panelClock.setHourSelectionView(true); 375 | header.setHourSelectionView(true); 376 | } 377 | panelClock.updateClock(); 378 | header.updateHeader(); 379 | repaint(); 380 | verifyTimeSelection(); 381 | } 382 | 383 | private class ClockActionListener implements TimeActionListener { 384 | 385 | @Override 386 | public void selectionViewChanged(boolean isHourSelectionView) { 387 | if (isHourSelectionView) { 388 | panelClock.setHourSelectionView(false); 389 | header.setHourSelectionView(false); 390 | } 391 | } 392 | } 393 | 394 | private class HeaderActionListener implements TimeActionListener { 395 | 396 | @Override 397 | public void selectionViewChanged(boolean isHourSelectionView) { 398 | panelClock.setHourSelectionView(isHourSelectionView); 399 | } 400 | } 401 | } 402 | -------------------------------------------------------------------------------- /src/main/java/raven/datetime/TimeSelectionAble.java: -------------------------------------------------------------------------------- 1 | package raven.datetime; 2 | 3 | import java.time.LocalTime; 4 | 5 | public interface TimeSelectionAble { 6 | 7 | boolean isTimeSelectedAble(LocalTime time, boolean hourView); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/raven/datetime/component/PanelPopupEditor.java: -------------------------------------------------------------------------------- 1 | package raven.datetime.component; 2 | 3 | import com.formdev.flatlaf.FlatClientProperties; 4 | import raven.datetime.util.Utils; 5 | 6 | import javax.swing.*; 7 | import java.awt.*; 8 | import java.text.ParseException; 9 | 10 | public abstract class PanelPopupEditor extends JPanel { 11 | 12 | protected JFormattedTextField editor; 13 | protected JPopupMenu popupMenu; 14 | 15 | protected boolean editorValidation = true; 16 | protected boolean isValid; 17 | protected boolean validationOnNull; 18 | protected String defaultPlaceholder; 19 | protected Point popupSpace = new Point(1, 1); 20 | 21 | protected LookAndFeel oldThemes = UIManager.getLookAndFeel(); 22 | 23 | public PanelPopupEditor() { 24 | } 25 | 26 | public void showPopup() { 27 | showPopup(editor); 28 | } 29 | 30 | public void showPopup(Component component) { 31 | if (popupMenu == null) { 32 | popupMenu = new JPopupMenu(); 33 | popupMenu.putClientProperty(FlatClientProperties.STYLE, "" + 34 | "borderInsets:1,1,1,1"); 35 | popupMenu.add(this); 36 | } 37 | if (UIManager.getLookAndFeel() != oldThemes) { 38 | // component in popup not update UI when change themes 39 | // so need to update when popup show 40 | SwingUtilities.updateComponentTreeUI(popupMenu); 41 | oldThemes = UIManager.getLookAndFeel(); 42 | } 43 | Point point = Utils.adjustPopupLocation(popupMenu, component, popupSpace); 44 | popupOpen(); 45 | popupMenu.show(component, point.x, point.y); 46 | } 47 | 48 | public void closePopup() { 49 | if (popupMenu != null) { 50 | popupMenu.setVisible(false); 51 | repaint(); 52 | } 53 | } 54 | 55 | public boolean isEditorValidation() { 56 | return editorValidation; 57 | } 58 | 59 | public void setEditorValidation(boolean editorValidation) { 60 | if (this.editorValidation != editorValidation) { 61 | this.editorValidation = editorValidation; 62 | if (editor != null) { 63 | if (editorValidation) { 64 | validChanged(editor, isValid); 65 | } else { 66 | validChanged(editor, true); 67 | } 68 | } 69 | } 70 | } 71 | 72 | public boolean isValidationOnNull() { 73 | return validationOnNull; 74 | } 75 | 76 | public void setValidationOnNull(boolean validationOnNull) { 77 | if (this.validationOnNull != validationOnNull) { 78 | this.validationOnNull = validationOnNull; 79 | commitEdit(); 80 | } 81 | } 82 | 83 | protected void checkValidation(boolean status) { 84 | boolean valid = status || isEditorValidationOnNull(); 85 | if (isValid != valid) { 86 | isValid = valid; 87 | if (editor != null) { 88 | if (editorValidation) { 89 | validChanged(editor, valid); 90 | } 91 | } 92 | } 93 | } 94 | 95 | protected void validChanged(JFormattedTextField editor, boolean isValid) { 96 | String style = isValid ? null : FlatClientProperties.OUTLINE_ERROR; 97 | editor.putClientProperty(FlatClientProperties.OUTLINE, style); 98 | } 99 | 100 | protected boolean isEditorValidationOnNull() { 101 | if (validationOnNull) { 102 | return false; 103 | } 104 | return editor != null && editor.getText().equals(getDefaultPlaceholder()); 105 | } 106 | 107 | protected void commitEdit() { 108 | if (editor != null && editorValidation) { 109 | try { 110 | editor.commitEdit(); 111 | } catch (ParseException ignored) { 112 | } 113 | } 114 | } 115 | 116 | public JFormattedTextField getEditor() { 117 | return editor; 118 | } 119 | 120 | public Point getPopupSpace() { 121 | return popupSpace; 122 | } 123 | 124 | public void setPopupSpace(Point popupSpace) { 125 | this.popupSpace = popupSpace; 126 | } 127 | 128 | protected abstract String getDefaultPlaceholder(); 129 | 130 | protected abstract void popupOpen(); 131 | } 132 | -------------------------------------------------------------------------------- /src/main/java/raven/datetime/component/date/ButtonDate.java: -------------------------------------------------------------------------------- 1 | package raven.datetime.component.date; 2 | 3 | import com.formdev.flatlaf.FlatClientProperties; 4 | import com.formdev.flatlaf.ui.FlatUIUtils; 5 | import raven.datetime.DatePicker; 6 | 7 | import javax.swing.*; 8 | import java.awt.*; 9 | import java.awt.event.MouseAdapter; 10 | import java.awt.event.MouseEvent; 11 | 12 | public class ButtonDate extends JButton { 13 | 14 | private final DatePicker datePicker; 15 | private final SingleDate date; 16 | private final int rowIndex; 17 | private boolean press; 18 | private boolean hover; 19 | 20 | public ButtonDate(DatePicker datePicker, SingleDate date, boolean enable, int rowIndex) { 21 | this.datePicker = datePicker; 22 | this.date = date; 23 | this.rowIndex = rowIndex; 24 | setText(date.getDay() + ""); 25 | init(enable); 26 | } 27 | 28 | private void init(boolean enable) { 29 | setContentAreaFilled(false); 30 | addActionListener(e -> { 31 | if (datePicker.isEnabled()) { 32 | datePicker.getDateSelectionModel().selectDate(date); 33 | hover = false; 34 | PanelDate panelDate = (PanelDate) getParent(); 35 | panelDate.checkSelection(); 36 | } 37 | }); 38 | addMouseListener(new MouseAdapter() { 39 | @Override 40 | public void mouseClicked(MouseEvent e) { 41 | if (datePicker.isEnabled() && isEnabled() && datePicker.getDateSelectionMode() == DatePicker.DateSelectionMode.SINGLE_DATE_SELECTED) { 42 | if (e.getClickCount() == 2 && SwingUtilities.isLeftMouseButton(e)) { 43 | datePicker.closePopup(); 44 | } 45 | } 46 | } 47 | 48 | @Override 49 | public void mousePressed(MouseEvent e) { 50 | if (datePicker.isEnabled() && SwingUtilities.isLeftMouseButton(e)) { 51 | press = true; 52 | } 53 | } 54 | 55 | @Override 56 | public void mouseReleased(MouseEvent e) { 57 | if (datePicker.isEnabled() && SwingUtilities.isLeftMouseButton(e)) { 58 | press = false; 59 | } 60 | } 61 | 62 | @Override 63 | public void mouseEntered(MouseEvent e) { 64 | if (datePicker.isEnabled()) { 65 | hover = true; 66 | if (datePicker.getDateSelectionModel().getDateSelectionMode() == DatePicker.DateSelectionMode.BETWEEN_DATE_SELECTED && datePicker.getDateSelectionModel().getDate() != null) { 67 | datePicker.getDateSelectionModel().setHoverDate(date); 68 | } 69 | } 70 | } 71 | 72 | @Override 73 | public void mouseExited(MouseEvent e) { 74 | if (datePicker.isEnabled()) { 75 | hover = false; 76 | } 77 | } 78 | }); 79 | if (enable) { 80 | putClientProperty(FlatClientProperties.STYLE, "" + 81 | "margin:7,7,7,7;" + 82 | "focusWidth:2;" + 83 | "selectedForeground:contrast($Component.accentColor,$Button.background,#fff)"); 84 | } else { 85 | putClientProperty(FlatClientProperties.STYLE, "" + 86 | "margin:7,7,7,7;" + 87 | "focusWidth:2;" + 88 | "selectedForeground:contrast($Component.accentColor,$Button.background,#fff);" + 89 | "foreground:$Button.disabledText"); 90 | } 91 | } 92 | 93 | @Override 94 | protected void paintComponent(Graphics g) { 95 | DefaultDateCellRenderer dateCellRenderer = datePicker.getDefaultDateCellRenderer(); 96 | if (dateCellRenderer != null) { 97 | Graphics2D g2 = (Graphics2D) g.create(); 98 | try { 99 | FlatUIUtils.setRenderingHints(g2); 100 | dateCellRenderer.paint(g2, datePicker, this, date, getWidth(), getHeight()); 101 | } finally { 102 | g2.dispose(); 103 | } 104 | } 105 | super.paintComponent(g); 106 | } 107 | 108 | public boolean isDateSelected() { 109 | DateSelectionModel dateSelectionModel = datePicker.getDateSelectionModel(); 110 | if (dateSelectionModel.getDateSelectionMode() == DatePicker.DateSelectionMode.SINGLE_DATE_SELECTED) { 111 | return date.same(dateSelectionModel.getDate()); 112 | } else { 113 | return date.same(dateSelectionModel.getDate()) || date.same(dateSelectionModel.getToDate()); 114 | } 115 | } 116 | 117 | public SingleDate getDate() { 118 | return date; 119 | } 120 | 121 | public boolean isPress() { 122 | return press; 123 | } 124 | 125 | public boolean isHover() { 126 | return hover; 127 | } 128 | 129 | public int getRowIndex() { 130 | return rowIndex; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/main/java/raven/datetime/component/date/ButtonMonthYear.java: -------------------------------------------------------------------------------- 1 | package raven.datetime.component.date; 2 | 3 | import com.formdev.flatlaf.FlatClientProperties; 4 | import com.formdev.flatlaf.ui.FlatUIUtils; 5 | import com.formdev.flatlaf.util.UIScale; 6 | import raven.datetime.DatePicker; 7 | import raven.datetime.util.Utils; 8 | 9 | import javax.swing.*; 10 | import java.awt.*; 11 | import java.awt.event.MouseAdapter; 12 | import java.awt.event.MouseEvent; 13 | 14 | public class ButtonMonthYear extends JButton { 15 | 16 | private final DatePicker datePicker; 17 | private final int value; 18 | private boolean press; 19 | private boolean hover; 20 | 21 | public ButtonMonthYear(DatePicker datePicker, int value) { 22 | this.datePicker = datePicker; 23 | this.value = value; 24 | init(); 25 | } 26 | 27 | private void init() { 28 | setContentAreaFilled(false); 29 | addMouseListener(new MouseAdapter() { 30 | @Override 31 | public void mousePressed(MouseEvent e) { 32 | if (SwingUtilities.isLeftMouseButton(e)) { 33 | press = true; 34 | } 35 | } 36 | 37 | @Override 38 | public void mouseReleased(MouseEvent e) { 39 | if (SwingUtilities.isLeftMouseButton(e)) { 40 | press = false; 41 | } 42 | } 43 | 44 | @Override 45 | public void mouseEntered(MouseEvent e) { 46 | hover = true; 47 | } 48 | 49 | @Override 50 | public void mouseExited(MouseEvent e) { 51 | hover = false; 52 | } 53 | }); 54 | putClientProperty(FlatClientProperties.STYLE, "" + 55 | "margin:6,6,6,6;" + 56 | "selectedForeground:contrast($Component.accentColor,$Button.background,#fff);"); 57 | } 58 | 59 | public int getValue() { 60 | return value; 61 | } 62 | 63 | @Override 64 | protected void paintComponent(Graphics g) { 65 | Graphics2D g2 = (Graphics2D) g.create(); 66 | FlatUIUtils.setRenderingHints(g2); 67 | int border = UIScale.scale(6); 68 | float arc = UIScale.scale(datePicker.getSelectionArc()); 69 | int width = getWidth() - border; 70 | int height = getHeight() - border; 71 | int x = (getWidth() - width) / 2; 72 | int y = (getHeight() - height) / 2; 73 | g2.setColor(getColor()); 74 | FlatUIUtils.paintComponentBackground(g2, x, y, width, height, 0, arc); 75 | g2.dispose(); 76 | super.paintComponent(g); 77 | } 78 | 79 | protected Color getColor() { 80 | Color color = isSelected() ? getAccentColor() : FlatUIUtils.getParentBackground(this); 81 | return Utils.getColor(color, press, hover); 82 | } 83 | 84 | protected Color getAccentColor() { 85 | if (datePicker.getColor() != null) { 86 | return datePicker.getColor(); 87 | } 88 | return UIManager.getColor("Component.accentColor"); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/main/java/raven/datetime/component/date/DateSelectionModel.java: -------------------------------------------------------------------------------- 1 | package raven.datetime.component.date; 2 | 3 | import raven.datetime.DatePicker; 4 | import raven.datetime.DateSelectionAble; 5 | import raven.datetime.component.date.event.DateSelectionModelEvent; 6 | import raven.datetime.component.date.event.DateSelectionModelListener; 7 | 8 | import javax.swing.event.EventListenerList; 9 | 10 | public class DateSelectionModel { 11 | 12 | protected EventListenerList listenerList = new EventListenerList(); 13 | private DatePicker.DateSelectionMode dateSelectionMode; 14 | private DateSelectionAble dateSelectionAble; 15 | private SingleDate date; 16 | private SingleDate toDate; 17 | private SingleDate hoverDate; 18 | 19 | public DateSelectionModel() { 20 | this(DatePicker.DateSelectionMode.SINGLE_DATE_SELECTED); 21 | } 22 | 23 | public DateSelectionModel(DatePicker.DateSelectionMode dateSelectionMode) { 24 | this(dateSelectionMode, null); 25 | } 26 | 27 | public DateSelectionModel(DateSelectionAble dateSelectionAble) { 28 | this(DatePicker.DateSelectionMode.SINGLE_DATE_SELECTED, dateSelectionAble); 29 | } 30 | 31 | public DateSelectionModel(DatePicker.DateSelectionMode dateSelectionMode, DateSelectionAble dateSelectionAble) { 32 | this.dateSelectionMode = dateSelectionMode; 33 | this.dateSelectionAble = dateSelectionAble; 34 | } 35 | 36 | public SingleDate getDate() { 37 | return date; 38 | } 39 | 40 | public void setDate(SingleDate date) { 41 | if (equalsDate(this.date, date)) { 42 | return; 43 | } 44 | if (!checkSelection(date)) { 45 | return; 46 | } 47 | this.date = date; 48 | fireDatePickerChanged(new DateSelectionModelEvent(this, DateSelectionModelEvent.DATE)); 49 | } 50 | 51 | public SingleDate getToDate() { 52 | return toDate; 53 | } 54 | 55 | public void setToDate(SingleDate toDate) { 56 | if (equalsDate(this.toDate, toDate)) { 57 | return; 58 | } 59 | if (!checkSelection(toDate)) { 60 | return; 61 | } 62 | this.toDate = toDate; 63 | } 64 | 65 | public SingleDate getHoverDate() { 66 | return hoverDate; 67 | } 68 | 69 | public void setHoverDate(SingleDate hoverDate) { 70 | if (equalsDate(this.hoverDate, hoverDate)) { 71 | return; 72 | } 73 | this.hoverDate = hoverDate; 74 | fireDatePickerChanged(new DateSelectionModelEvent(this, DateSelectionModelEvent.BETWEEN_DATE_HOVER)); 75 | } 76 | 77 | public void setSelectDate(SingleDate from, SingleDate to) { 78 | if (equalsDate(this.date, from) && equalsDate(this.toDate, to)) { 79 | return; 80 | } 81 | if (!checkSelection(from) || !checkSelection(to)) { 82 | return; 83 | } 84 | date = from; 85 | toDate = to; 86 | fireDatePickerChanged(new DateSelectionModelEvent(this, DateSelectionModelEvent.DATE)); 87 | } 88 | 89 | protected void selectDate(SingleDate date) { 90 | if (!checkSelection(date)) { 91 | return; 92 | } 93 | if (dateSelectionMode == DatePicker.DateSelectionMode.SINGLE_DATE_SELECTED) { 94 | setDate(date); 95 | } else { 96 | if (getDate() == null || getToDate() != null) { 97 | this.date = date; 98 | hoverDate = date; 99 | if (getToDate() != null) { 100 | this.toDate = null; 101 | } 102 | fireDatePickerChanged(new DateSelectionModelEvent(this, DateSelectionModelEvent.DATE)); 103 | } else { 104 | this.toDate = date; 105 | invertIfNecessaryToDate(date); 106 | fireDatePickerChanged(new DateSelectionModelEvent(this, DateSelectionModelEvent.DATE)); 107 | } 108 | } 109 | } 110 | 111 | public void setDateSelectionAble(DateSelectionAble dateSelectionAble) { 112 | this.dateSelectionAble = dateSelectionAble; 113 | } 114 | 115 | public DateSelectionAble getDateSelectionAble() { 116 | return dateSelectionAble; 117 | } 118 | 119 | public void addDatePickerSelectionListener(DateSelectionModelListener listener) { 120 | listenerList.add(DateSelectionModelListener.class, listener); 121 | } 122 | 123 | public void removeDatePickerSelectionListener(DateSelectionModelListener listener) { 124 | listenerList.remove(DateSelectionModelListener.class, listener); 125 | } 126 | 127 | public void fireDatePickerChanged(DateSelectionModelEvent event) { 128 | Object[] listeners = listenerList.getListenerList(); 129 | for (int i = listeners.length - 2; i >= 0; i -= 2) { 130 | if (listeners[i] == DateSelectionModelListener.class) { 131 | ((DateSelectionModelListener) listeners[i + 1]).dateSelectionModelChanged(event); 132 | } 133 | } 134 | } 135 | 136 | public boolean equalsDate(SingleDate date1, SingleDate date2) { 137 | // both are null 138 | if (date1 == null && date2 == null) { 139 | return true; 140 | } 141 | // only one is null 142 | if (date1 == null || date2 == null) { 143 | return false; 144 | } 145 | return date1.same(date2); 146 | } 147 | 148 | public DatePicker.DateSelectionMode getDateSelectionMode() { 149 | return dateSelectionMode; 150 | } 151 | 152 | public void setDateSelectionMode(DatePicker.DateSelectionMode dateSelectionMode) { 153 | this.dateSelectionMode = dateSelectionMode; 154 | } 155 | 156 | private boolean checkSelection(SingleDate date) { 157 | if (dateSelectionAble != null) { 158 | return date == null || dateSelectionAble.isDateSelectedAble(date.toLocalDate()); 159 | } 160 | return true; 161 | } 162 | 163 | private void invertIfNecessaryToDate(SingleDate date) { 164 | if (this.date.toLocalDate().isAfter(date.toLocalDate())) { 165 | this.toDate = this.date; 166 | this.date = date; 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/main/java/raven/datetime/component/date/DefaultDateCellRenderer.java: -------------------------------------------------------------------------------- 1 | package raven.datetime.component.date; 2 | 3 | import com.formdev.flatlaf.FlatLaf; 4 | import com.formdev.flatlaf.ui.FlatUIUtils; 5 | import com.formdev.flatlaf.util.ColorFunctions; 6 | import com.formdev.flatlaf.util.UIScale; 7 | import raven.datetime.DatePicker; 8 | import raven.datetime.util.Utils; 9 | 10 | import javax.swing.*; 11 | import java.awt.*; 12 | import java.awt.geom.Area; 13 | import java.awt.geom.Rectangle2D; 14 | 15 | public class DefaultDateCellRenderer { 16 | 17 | public void paint(Graphics2D g2, DatePicker datePicker, ButtonDate component, SingleDate date, float width, float height) { 18 | 19 | // paint selection 20 | float arc = getArc(datePicker); 21 | boolean isSelected = component.isDateSelected(); 22 | paintDateSelection(g2, datePicker, component, isSelected, width, height, arc); 23 | 24 | // paint date between selected 25 | DateSelectionModel dateSelectionModel = datePicker.getDateSelectionModel(); 26 | if (dateSelectionModel.getDateSelectionMode() == DatePicker.DateSelectionMode.BETWEEN_DATE_SELECTED && dateSelectionModel.getDate() != null) { 27 | paintBetweenDateSelection(g2, datePicker, component, date, width, height, arc); 28 | } 29 | if (date.same(new SingleDate())) { 30 | paintCurrentDate(g2, datePicker, component, date, isSelected, width, height, arc); 31 | } 32 | } 33 | 34 | /** 35 | * Paint cell selection or cell focused 36 | */ 37 | protected void paintDateSelection(Graphics2D g2, DatePicker datePicker, ButtonDate component, boolean isSelected, float width, float height, float arc) { 38 | Rectangle2D.Float rec = getRectangle(width, height); 39 | g2.setColor(getSelectionColor(datePicker, component, isSelected)); 40 | g2.fill(FlatUIUtils.createComponentRectangle(rec.x, rec.y, rec.width, rec.height, arc)); 41 | } 42 | 43 | protected void paintBetweenDateSelection(Graphics2D g2, DatePicker datePicker, ButtonDate component, SingleDate date, float width, float height, float arc) { 44 | g2.setColor(getBetweenDateColor(datePicker, component)); 45 | DateSelectionModel dateSelectionModel = datePicker.getDateSelectionModel(); 46 | Rectangle2D.Float rec = getRectangle(width, height); 47 | int rowIndex = component.getRowIndex(); 48 | if (date.between(dateSelectionModel.getDate(), getToDate(datePicker))) { 49 | if (rowIndex == 0) { 50 | g2.fill(createShape(rec.x, rec.y, width, rec.height, arc, true, true)); 51 | } else if (rowIndex == 6) { 52 | g2.fill(createShape(rec.x, rec.y, width, rec.height, arc, false, true)); 53 | } else { 54 | g2.fill(new Rectangle2D.Float(0, rec.y, width, rec.height)); 55 | } 56 | } 57 | if (!dateSelectionModel.getDate().same(getToDate(datePicker))) { 58 | boolean right = dateSelectionModel.getDate().before(getToDate(datePicker)); 59 | if (date.same(dateSelectionModel.getDate())) { 60 | if ((right && rowIndex != 6) || !right && rowIndex != 0) { 61 | g2.fill(createShape(rec.x, rec.y, width, rec.height, arc, right, false)); 62 | } 63 | } 64 | if (date.same(getToDate(datePicker))) { 65 | if ((right && rowIndex != 0) || (!right && rowIndex != 6)) { 66 | g2.fill(createShape(rec.x, rec.y, width, rec.height, arc, !right, !component.isHover() && dateSelectionModel.getToDate() == null)); 67 | } 68 | } 69 | } 70 | } 71 | 72 | protected void paintCurrentDate(Graphics2D g2, DatePicker datePicker, ButtonDate component, SingleDate date, boolean isSelected, float width, float height, float arc) { 73 | Rectangle2D.Float rec = getRectangle(width, height); 74 | float fw = getSelectedFocusWidth(); 75 | float space = UIScale.scale(isSelected ? fw : 0); 76 | if (space > 0) { 77 | rec.x -= space; 78 | rec.y -= space; 79 | rec.width += space * 2f; 80 | rec.height += space * 2f; 81 | } 82 | Area area = new Area(FlatUIUtils.createComponentRectangle(rec.x, rec.y, rec.width, rec.height, arc)); 83 | if (isSelected) { 84 | float s = UIScale.scale(fw + 1f); 85 | area.subtract(new Area(FlatUIUtils.createComponentRectangle(rec.x + s, rec.x + s, rec.width - s * 2f, rec.height - s * 2f, arc - s))); 86 | } else { 87 | float s = UIScale.scale(fw); 88 | area.subtract(new Area(FlatUIUtils.createComponentRectangle(rec.x + s, rec.y + s, rec.width - s * 2f, rec.height - s * 2f, arc - s))); 89 | } 90 | Color accentColor = getAccentColor(datePicker); 91 | g2.setColor(isSelected ? getBorderColor(component, accentColor) : accentColor); 92 | g2.fill(area); 93 | } 94 | 95 | private SingleDate getToDate(DatePicker datePicker) { 96 | DateSelectionModel dateSelectionModel = datePicker.getDateSelectionModel(); 97 | return dateSelectionModel.getToDate() != null ? dateSelectionModel.getToDate() : dateSelectionModel.getHoverDate(); 98 | } 99 | 100 | protected Shape createShape(float x, float y, float width, float size, float arc, boolean right, boolean add) { 101 | Area area; 102 | if (right) { 103 | area = new Area(new Rectangle2D.Float(width / 2, y, width / 2, size)); 104 | area.subtract(new Area(FlatUIUtils.createComponentRectangle(x, y, size, size, arc))); 105 | } else { 106 | area = new Area(new Rectangle2D.Float(0, y, width / 2, size)); 107 | } 108 | if (add) { 109 | area.add(new Area(FlatUIUtils.createComponentRectangle(x, y, size, size, arc))); 110 | } else { 111 | area.subtract(new Area(FlatUIUtils.createComponentRectangle(x, y, size, size, arc))); 112 | } 113 | return area; 114 | } 115 | 116 | protected Color getBorderColor(ButtonDate component, Color color) { 117 | return ColorFunctions.mix(color, component.getParent().getBackground(), 0.45f); 118 | } 119 | 120 | protected Color getBetweenDateColor(DatePicker datePicker, ButtonDate component) { 121 | Color color = FlatUIUtils.getParentBackground(component); 122 | if (datePicker.getDateSelectionModel().getToDate() != null) { 123 | return ColorFunctions.mix(color, getAccentColor(datePicker), 0.9f); 124 | } 125 | return FlatLaf.isLafDark() ? ColorFunctions.lighten(color, 0.03f) : ColorFunctions.darken(color, 0.03f); 126 | } 127 | 128 | /** 129 | * Get selection color, focused color or isPress and isHover color 130 | */ 131 | protected Color getSelectionColor(DatePicker datePicker, ButtonDate component, boolean isSelected) { 132 | // use component parent background as the focused color 133 | Color color = FlatUIUtils.getParentBackground(component); 134 | if (isSelected) { 135 | // use accent color as selection color 136 | color = getAccentColor(datePicker); 137 | } 138 | return Utils.getColor(color, component.isPress(), component.isHover()); 139 | } 140 | 141 | protected Color getAccentColor(DatePicker datePicker) { 142 | if (datePicker.getColor() != null) { 143 | return datePicker.getColor(); 144 | } 145 | return UIManager.getColor("Component.accentColor"); 146 | } 147 | 148 | protected float getCellPadding() { 149 | return 7f; 150 | } 151 | 152 | protected float getSelectedFocusWidth() { 153 | return 2f; 154 | } 155 | 156 | protected float getArc(DatePicker datePicker) { 157 | return datePicker.getSelectionArc(); 158 | } 159 | 160 | protected Rectangle2D.Float getRectangle(float width, float height) { 161 | float size = Math.min(width, height) - UIScale.scale(getCellPadding()); 162 | float x = (width - size) / 2f; 163 | float y = (height - size) / 2f; 164 | return new Rectangle2D.Float(x, y, size, size); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/main/java/raven/datetime/component/date/Header.java: -------------------------------------------------------------------------------- 1 | package raven.datetime.component.date; 2 | 3 | import com.formdev.flatlaf.FlatClientProperties; 4 | import com.formdev.flatlaf.extras.FlatSVGIcon; 5 | import net.miginfocom.swing.MigLayout; 6 | import raven.datetime.component.date.event.DateControlEvent; 7 | import raven.datetime.component.date.event.DateControlListener; 8 | 9 | import javax.swing.*; 10 | import java.text.DateFormatSymbols; 11 | 12 | public class Header extends JPanel { 13 | 14 | protected JButton buttonMonth; 15 | protected JButton buttonYear; 16 | 17 | protected Icon backIcon; 18 | 19 | protected Icon forwardIcon; 20 | 21 | public Header() { 22 | this(10, 2023); 23 | } 24 | 25 | public Header(int month, int year) { 26 | init(month, year); 27 | } 28 | 29 | private void init(int month, int year) { 30 | putClientProperty(FlatClientProperties.STYLE, "" + 31 | "background:null"); 32 | setLayout(new MigLayout("fill,insets 3", "[]push[][]push[]", "fill")); 33 | 34 | JButton cmdBack = createButton(); 35 | JButton cmdNext = createButton(); 36 | 37 | backIcon = createDefaultBackIcon(); 38 | forwardIcon = createDefaultForwardIcon(); 39 | cmdBack.setIcon(backIcon); 40 | cmdNext.setIcon(forwardIcon); 41 | 42 | buttonMonth = createButton(); 43 | buttonYear = createButton(); 44 | 45 | cmdBack.addActionListener(e -> fireDateControlChanged(new DateControlEvent(this, DateControlEvent.DAY_STATE, DateControlEvent.BACK))); 46 | cmdNext.addActionListener(e -> fireDateControlChanged(new DateControlEvent(this, DateControlEvent.DAY_STATE, DateControlEvent.FORWARD))); 47 | buttonMonth.addActionListener(e -> fireDateControlChanged(new DateControlEvent(this, DateControlEvent.DAY_STATE, DateControlEvent.MONTH))); 48 | buttonYear.addActionListener(e -> fireDateControlChanged(new DateControlEvent(this, DateControlEvent.DAY_STATE, DateControlEvent.YEAR))); 49 | 50 | add(cmdBack); 51 | add(buttonMonth); 52 | add(buttonYear); 53 | add(cmdNext); 54 | setDate(month, year); 55 | } 56 | 57 | protected JButton createButton() { 58 | JButton button = new JButton(); 59 | button.putClientProperty(FlatClientProperties.STYLE, "" + 60 | "background:null;" + 61 | "arc:10;" + 62 | "borderWidth:0;" + 63 | "focusWidth:0;" + 64 | "innerFocusWidth:0;" + 65 | "margin:0,5,0,5"); 66 | return button; 67 | } 68 | 69 | protected Icon createDefaultBackIcon() { 70 | return new FlatSVGIcon("raven/datetime/icon/back.svg"); 71 | } 72 | 73 | protected Icon createDefaultForwardIcon() { 74 | return new FlatSVGIcon("raven/datetime/icon/forward.svg"); 75 | } 76 | 77 | public void addDateControlListener(DateControlListener listener) { 78 | listenerList.add(DateControlListener.class, listener); 79 | } 80 | 81 | public void removeDateControlListener(DateControlListener listener) { 82 | listenerList.remove(DateControlListener.class, listener); 83 | } 84 | 85 | public void fireDateControlChanged(DateControlEvent event) { 86 | Object[] listeners = listenerList.getListenerList(); 87 | for (int i = listeners.length - 2; i >= 0; i -= 2) { 88 | if (listeners[i] == DateControlListener.class) { 89 | ((DateControlListener) listeners[i + 1]).dateControlChanged(event); 90 | } 91 | } 92 | } 93 | 94 | public void setDate(int month, int year) { 95 | buttonMonth.setText(DateFormatSymbols.getInstance().getMonths()[month]); 96 | buttonYear.setText(year + ""); 97 | } 98 | 99 | public Icon getBackIcon() { 100 | return backIcon; 101 | } 102 | 103 | public void setBackIcon(Icon backIcon) { 104 | this.backIcon = backIcon; 105 | } 106 | 107 | public Icon getForwardIcon() { 108 | return forwardIcon; 109 | } 110 | 111 | public void setForwardIcon(Icon forwardIcon) { 112 | this.forwardIcon = forwardIcon; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/main/java/raven/datetime/component/date/PanelDate.java: -------------------------------------------------------------------------------- 1 | package raven.datetime.component.date; 2 | 3 | import com.formdev.flatlaf.FlatClientProperties; 4 | import net.miginfocom.swing.MigLayout; 5 | import raven.datetime.DatePicker; 6 | 7 | import javax.swing.*; 8 | import java.awt.*; 9 | import java.text.DateFormatSymbols; 10 | import java.util.Calendar; 11 | 12 | public class PanelDate extends JPanel { 13 | 14 | private final DatePicker datePicker; 15 | private final int month; 16 | private final int year; 17 | 18 | public PanelDate(DatePicker datePicker, int month, int year) { 19 | this.datePicker = datePicker; 20 | this.month = month; 21 | this.year = year; 22 | init(); 23 | } 24 | 25 | private void init() { 26 | putClientProperty(FlatClientProperties.STYLE, "" + 27 | "background:null;"); 28 | setLayout(new MigLayout( 29 | "novisualpadding,wrap 7,insets 3,gap 0,al center center", 30 | "[fill]", 31 | "[fill]10[fill][fill]")); 32 | load(); 33 | } 34 | 35 | public void load() { 36 | removeAll(); 37 | createDateHeader(); 38 | final int col = 7; 39 | final int row = 6; 40 | final int t = col * row; 41 | Calendar calendar = Calendar.getInstance(); 42 | calendar.set(Calendar.YEAR, year); 43 | calendar.set(Calendar.MONTH, month); 44 | calendar.set(Calendar.DATE, 1); 45 | int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK); 46 | if (datePicker.isStartWeekOnMonday() && dayOfWeek == Calendar.SUNDAY) { 47 | dayOfWeek = 8; 48 | } 49 | int startDay = dayOfWeek - (datePicker.isStartWeekOnMonday() ? 2 : 1); 50 | calendar.add(Calendar.DATE, -startDay); 51 | int rowIndex = 0; 52 | for (int i = 1; i <= t; i++) { 53 | SingleDate singleDate = new SingleDate(calendar); 54 | boolean selectable = datePicker.getDateSelectionModel().getDateSelectionAble() == null || datePicker.getDateSelectionModel().getDateSelectionAble().isDateSelectedAble(singleDate.toLocalDate()); 55 | boolean enable = calendar.get(Calendar.MONTH) == month && calendar.get(Calendar.YEAR) == year; 56 | JButton button = createButton(new SingleDate(calendar), enable, rowIndex); 57 | if (!selectable) { 58 | button.setEnabled(false); 59 | } 60 | add(button); 61 | calendar.add(Calendar.DATE, 1); 62 | if (rowIndex == 6) { 63 | rowIndex = 0; 64 | } else { 65 | rowIndex++; 66 | } 67 | } 68 | checkSelection(); 69 | } 70 | 71 | protected void createDateHeader() { 72 | String[] weekdays = DatePicker.getDefaultWeekdays(); 73 | if (weekdays == null) { 74 | weekdays = DateFormatSymbols.getInstance().getShortWeekdays(); 75 | } 76 | // swap monday to the start day of week 77 | if (datePicker.isStartWeekOnMonday()) { 78 | String sunday = weekdays[1]; 79 | for (int i = 2; i < weekdays.length; i++) { 80 | weekdays[i - 1] = weekdays[i]; 81 | } 82 | weekdays[weekdays.length - 1] = sunday; 83 | } 84 | for (String week : weekdays) { 85 | if (!week.isEmpty()) { 86 | add(createLabel(week)); 87 | } 88 | } 89 | } 90 | 91 | private JLabel createLabel(String text) { 92 | JLabel label = new JLabel(text, JLabel.CENTER); 93 | label.putClientProperty(FlatClientProperties.STYLE, "" + 94 | "[light]foreground:lighten($Label.foreground,30%);" + 95 | "[dark]foreground:darken($Label.foreground,30%)"); 96 | return label; 97 | } 98 | 99 | protected JButton createButton(SingleDate date, boolean enable, int rowIndex) { 100 | ButtonDate button = new ButtonDate(datePicker, date, enable, rowIndex); 101 | if (button.isDateSelected()) { 102 | button.setSelected(true); 103 | } 104 | return button; 105 | } 106 | 107 | public void checkSelection() { 108 | for (int i = 0; i < getComponentCount(); i++) { 109 | Component com = getComponent(i); 110 | if (com instanceof ButtonDate) { 111 | ButtonDate buttonDate = (ButtonDate) com; 112 | if (datePicker.getDateSelectionModel().getDateSelectionMode() == DatePicker.DateSelectionMode.SINGLE_DATE_SELECTED) { 113 | buttonDate.setSelected(buttonDate.getDate().same(datePicker.getDateSelectionModel().getDate())); 114 | } else { 115 | buttonDate.setSelected(buttonDate.getDate().same(datePicker.getDateSelectionModel().getDate()) || buttonDate.getDate().same(datePicker.getDateSelectionModel().getToDate())); 116 | } 117 | } 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/main/java/raven/datetime/component/date/PanelMonth.java: -------------------------------------------------------------------------------- 1 | package raven.datetime.component.date; 2 | 3 | import com.formdev.flatlaf.FlatClientProperties; 4 | import net.miginfocom.swing.MigLayout; 5 | import raven.datetime.DatePicker; 6 | 7 | import javax.swing.*; 8 | import javax.swing.event.ChangeEvent; 9 | import javax.swing.event.ChangeListener; 10 | import java.awt.*; 11 | import java.text.DateFormatSymbols; 12 | 13 | public class PanelMonth extends JPanel { 14 | 15 | private final DatePicker datePicker; 16 | private final int year; 17 | private int selectedMonth = -1; 18 | 19 | public PanelMonth(DatePicker datePicker, int year) { 20 | this.datePicker = datePicker; 21 | this.year = year; 22 | init(); 23 | } 24 | 25 | private void init() { 26 | putClientProperty(FlatClientProperties.STYLE, "" + 27 | "background:null"); 28 | setLayout(new MigLayout( 29 | "novisualpadding,wrap 3,insets 0,fillx,gap 0,al center center", 30 | "fill,sg main", 31 | "fill")); 32 | 33 | final int count = 12; 34 | for (int i = 0; i < count; i++) { 35 | final int month = i; 36 | ButtonMonthYear button = new ButtonMonthYear(datePicker, i); 37 | button.setText(DateFormatSymbols.getInstance().getMonths()[i]); 38 | if (checkSelected(month + 1)) { 39 | button.setSelected(true); 40 | } 41 | button.addActionListener(e -> { 42 | this.selectedMonth = month; 43 | fireMonthChanged(new ChangeEvent(this)); 44 | }); 45 | add(button); 46 | } 47 | } 48 | 49 | public boolean checkSelected(int month) { 50 | DateSelectionModel dateSelectionModel = datePicker.getDateSelectionModel(); 51 | if (dateSelectionModel.getDateSelectionMode() == DatePicker.DateSelectionMode.SINGLE_DATE_SELECTED) { 52 | return dateSelectionModel.getDate() != null && year == dateSelectionModel.getDate().getYear() && month == dateSelectionModel.getDate().getMonth(); 53 | } else { 54 | return (dateSelectionModel.getDate() != null && year == dateSelectionModel.getDate().getYear() && month == dateSelectionModel.getDate().getMonth()) || 55 | (dateSelectionModel.getToDate() != null && year == dateSelectionModel.getToDate().getYear() && month == dateSelectionModel.getToDate().getMonth()); 56 | } 57 | } 58 | 59 | public void checkSelection() { 60 | for (int i = 0; i < getComponentCount(); i++) { 61 | Component com = getComponent(i); 62 | if (com instanceof ButtonMonthYear) { 63 | ButtonMonthYear button = (ButtonMonthYear) com; 64 | button.setSelected(checkSelected(button.getValue() + 1)); 65 | } 66 | } 67 | } 68 | 69 | public void addChangeListener(ChangeListener listener) { 70 | listenerList.add(ChangeListener.class, listener); 71 | } 72 | 73 | public void removeChangeListener(ChangeListener listener) { 74 | listenerList.remove(ChangeListener.class, listener); 75 | } 76 | 77 | public void fireMonthChanged(ChangeEvent event) { 78 | Object[] listeners = listenerList.getListenerList(); 79 | for (int i = listeners.length - 2; i >= 0; i -= 2) { 80 | if (listeners[i] == ChangeListener.class) { 81 | ((ChangeListener) listeners[i + 1]).stateChanged(event); 82 | } 83 | } 84 | } 85 | 86 | public int getSelectedMonth() { 87 | return selectedMonth; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/raven/datetime/component/date/PanelYear.java: -------------------------------------------------------------------------------- 1 | package raven.datetime.component.date; 2 | 3 | import com.formdev.flatlaf.FlatClientProperties; 4 | import net.miginfocom.swing.MigLayout; 5 | import raven.datetime.DatePicker; 6 | 7 | import javax.swing.*; 8 | import javax.swing.event.ChangeEvent; 9 | import javax.swing.event.ChangeListener; 10 | import java.awt.*; 11 | 12 | public class PanelYear extends JPanel { 13 | 14 | public static final int YEAR_CELL = 28; 15 | private final DatePicker datePicker; 16 | private final int year; 17 | private int selectedYear = -1; 18 | 19 | public PanelYear(DatePicker datePicker, int year) { 20 | this.datePicker = datePicker; 21 | this.year = year; 22 | init(); 23 | } 24 | 25 | private void init() { 26 | putClientProperty(FlatClientProperties.STYLE, "" + 27 | "background:null"); 28 | setLayout(new MigLayout( 29 | "novisualpadding,wrap 4,insets 0,fillx,gap 0,al center center", 30 | "fill,sg main", 31 | "fill")); 32 | 33 | final int count = YEAR_CELL; 34 | for (int i = 0; i < count; i++) { 35 | final int y = getStartYear(year) + i; 36 | ButtonMonthYear button = new ButtonMonthYear(datePicker, y); 37 | button.setText(y + ""); 38 | if (checkSelected(y)) { 39 | button.setSelected(true); 40 | } 41 | button.addActionListener(e -> { 42 | this.selectedYear = y; 43 | fireYearChanged(new ChangeEvent(this)); 44 | }); 45 | add(button); 46 | } 47 | checkSelection(); 48 | } 49 | 50 | private int getStartYear(int year) { 51 | int initYear = 1900; 52 | int currentYear = year; 53 | int yearsPerPage = YEAR_CELL; 54 | int yearsPassed = currentYear - initYear; 55 | int pages = yearsPassed / yearsPerPage; 56 | int startingYearOnPage = initYear + (pages * yearsPerPage); 57 | return startingYearOnPage; 58 | } 59 | 60 | protected boolean checkSelected(int year) { 61 | DateSelectionModel dateSelectionModel = datePicker.getDateSelectionModel(); 62 | if (dateSelectionModel.getDateSelectionMode() == DatePicker.DateSelectionMode.SINGLE_DATE_SELECTED) { 63 | return dateSelectionModel.getDate() != null && year == dateSelectionModel.getDate().getYear(); 64 | } else { 65 | return (dateSelectionModel.getDate() != null && year == dateSelectionModel.getDate().getYear()) || 66 | (dateSelectionModel.getToDate() != null && year == dateSelectionModel.getToDate().getYear()); 67 | } 68 | } 69 | 70 | public void checkSelection() { 71 | for (int i = 0; i < getComponentCount(); i++) { 72 | Component com = getComponent(i); 73 | if (com instanceof ButtonMonthYear) { 74 | ButtonMonthYear button = (ButtonMonthYear) com; 75 | button.setSelected(checkSelected(button.getValue())); 76 | } 77 | } 78 | } 79 | 80 | public int getYear() { 81 | return year; 82 | } 83 | 84 | public void addChangeListener(ChangeListener listener) { 85 | listenerList.add(ChangeListener.class, listener); 86 | } 87 | 88 | public void removeChangeListener(ChangeListener listener) { 89 | listenerList.remove(ChangeListener.class, listener); 90 | } 91 | 92 | public void fireYearChanged(ChangeEvent event) { 93 | Object[] listeners = listenerList.getListenerList(); 94 | for (int i = listeners.length - 2; i >= 0; i -= 2) { 95 | if (listeners[i] == ChangeListener.class) { 96 | ((ChangeListener) listeners[i + 1]).stateChanged(event); 97 | } 98 | } 99 | } 100 | 101 | public int getSelectedYear() { 102 | return selectedYear; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/main/java/raven/datetime/component/date/SingleDate.java: -------------------------------------------------------------------------------- 1 | package raven.datetime.component.date; 2 | 3 | import java.time.LocalDate; 4 | import java.util.Calendar; 5 | 6 | public class SingleDate { 7 | 8 | private int day; 9 | private int month; 10 | private int year; 11 | 12 | public SingleDate() { 13 | this(LocalDate.now()); 14 | } 15 | 16 | public SingleDate(LocalDate date) { 17 | this.day = date.getDayOfMonth(); 18 | this.month = date.getMonthValue(); 19 | this.year = date.getYear(); 20 | } 21 | 22 | public SingleDate(Calendar calendar) { 23 | this(calendar.get(Calendar.DATE), calendar.get(Calendar.MONTH) + 1, calendar.get(Calendar.YEAR)); 24 | } 25 | 26 | public SingleDate(int day, int month, int year) { 27 | this.day = day; 28 | this.month = month; 29 | this.year = year; 30 | } 31 | 32 | public int getDay() { 33 | return day; 34 | } 35 | 36 | public void setDay(int day) { 37 | this.day = day; 38 | } 39 | 40 | public int getMonth() { 41 | return month; 42 | } 43 | 44 | public void setMonth(int month) { 45 | this.month = month; 46 | } 47 | 48 | public int getYear() { 49 | return year; 50 | } 51 | 52 | public void setYear(int year) { 53 | this.year = year; 54 | } 55 | 56 | public boolean same(SingleDate date) { 57 | return date != null && year == date.year && month == date.month && day == date.day; 58 | } 59 | 60 | public boolean after(SingleDate date) { 61 | if (date == null) { 62 | return false; 63 | } 64 | if (year > date.year) { 65 | return true; 66 | } else if (year < date.year) { 67 | return false; 68 | } else { 69 | if (month > date.month) { 70 | return true; 71 | } else if (month < date.month) { 72 | return false; 73 | } else { 74 | return day > date.day; 75 | } 76 | } 77 | } 78 | 79 | public boolean before(SingleDate date) { 80 | if (date == null) { 81 | return false; 82 | } 83 | if (year < date.year) { 84 | return true; 85 | } else if (year > date.year) { 86 | return false; 87 | } else { 88 | if (month < date.month) { 89 | return true; 90 | } else if (month > date.month) { 91 | return false; 92 | } else { 93 | return day < date.day; 94 | } 95 | } 96 | } 97 | 98 | public boolean between(SingleDate from, SingleDate to) { 99 | if (from.before(to)) { 100 | return after(from) && before(to); 101 | } else { 102 | return after(to) && before(from); 103 | } 104 | } 105 | 106 | public LocalDate toLocalDate() { 107 | return LocalDate.of(year, month, day); 108 | } 109 | 110 | @Override 111 | public String toString() { 112 | return day + "/" + month + "/" + year; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/main/java/raven/datetime/component/date/event/DateControlEvent.java: -------------------------------------------------------------------------------- 1 | package raven.datetime.component.date.event; 2 | 3 | import java.util.EventObject; 4 | 5 | public class DateControlEvent extends EventObject { 6 | 7 | public static final int DAY_STATE = 1; 8 | public static final int MONTH_STATE = 2; 9 | public static final int YEAR_STATE = 3; 10 | 11 | public static final int BACK = 10; 12 | public static final int FORWARD = 11; 13 | public static final int MONTH = 12; 14 | public static final int YEAR = 13; 15 | 16 | protected int state; 17 | protected int type; 18 | 19 | public DateControlEvent(Object source, int state, int type) { 20 | super(source); 21 | this.state = state; 22 | this.type = type; 23 | } 24 | 25 | public int getState() { 26 | return state; 27 | } 28 | 29 | public int getType() { 30 | return type; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/raven/datetime/component/date/event/DateControlListener.java: -------------------------------------------------------------------------------- 1 | package raven.datetime.component.date.event; 2 | 3 | import java.util.EventListener; 4 | 5 | public interface DateControlListener extends EventListener { 6 | 7 | void dateControlChanged(DateControlEvent e); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/raven/datetime/component/date/event/DateSelectionModelEvent.java: -------------------------------------------------------------------------------- 1 | package raven.datetime.component.date.event; 2 | 3 | import java.util.EventObject; 4 | 5 | public class DateSelectionModelEvent extends EventObject { 6 | 7 | public static final int BETWEEN_DATE_HOVER = 1; 8 | public static final int DATE = 2; 9 | 10 | protected int action; 11 | 12 | public DateSelectionModelEvent(Object source, int action) { 13 | super(source); 14 | this.action = action; 15 | } 16 | 17 | public int getAction() { 18 | return action; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/raven/datetime/component/date/event/DateSelectionModelListener.java: -------------------------------------------------------------------------------- 1 | package raven.datetime.component.date.event; 2 | 3 | import java.util.EventListener; 4 | 5 | public interface DateSelectionModelListener extends EventListener { 6 | 7 | void dateSelectionModelChanged(DateSelectionModelEvent e); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/raven/datetime/component/time/AnimationChange.java: -------------------------------------------------------------------------------- 1 | package raven.datetime.component.time; 2 | 3 | import com.formdev.flatlaf.util.Animator; 4 | import com.formdev.flatlaf.util.CubicBezierEasing; 5 | 6 | import java.awt.*; 7 | 8 | public class AnimationChange { 9 | 10 | public final Animator animator; 11 | 12 | private final AnimationValue angleValue = new AnimationValue(); 13 | private final AnimationValue marginValue = new AnimationValue(); 14 | 15 | private float angle; 16 | 17 | private float margin; 18 | 19 | public AnimationChange(Component component) { 20 | animator = new Animator(350, v -> { 21 | angle = angleValue.interpolate(v); 22 | margin = marginValue.interpolate(v); 23 | component.repaint(); 24 | }); 25 | animator.setInterpolator(CubicBezierEasing.EASE); 26 | } 27 | 28 | public void start(float angleTarget, float marginTarget, boolean animated) { 29 | if (angle != angleTarget || margin != marginTarget) { 30 | angleValue.set(angle, angleTarget); 31 | marginValue.set(margin, marginTarget); 32 | if (animator.isRunning()) { 33 | animator.stop(); 34 | } 35 | if (animated) { 36 | animator.start(); 37 | } else { 38 | angle = angleValue.interpolate(1f); 39 | margin = marginValue.interpolate(1f); 40 | } 41 | } 42 | } 43 | 44 | public float getAngle() { 45 | return angle; 46 | } 47 | 48 | public float getMargin() { 49 | return margin; 50 | } 51 | 52 | public void set(float angle, float margin) { 53 | if (animator.isRunning()) { 54 | animator.stop(); 55 | } 56 | this.angle = angle; 57 | this.margin = margin; 58 | } 59 | 60 | public boolean isRunning() { 61 | return animator.isRunning(); 62 | } 63 | 64 | private static class AnimationValue { 65 | 66 | private float from; 67 | private float target; 68 | 69 | public void set(float from, float target) { 70 | this.from = from; 71 | this.target = target; 72 | } 73 | 74 | private float interpolate(float fraction) { 75 | return from + ((target - from) * fraction); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/raven/datetime/component/time/Header.java: -------------------------------------------------------------------------------- 1 | package raven.datetime.component.time; 2 | 3 | import com.formdev.flatlaf.FlatClientProperties; 4 | import net.miginfocom.swing.MigLayout; 5 | import raven.datetime.TimePicker; 6 | import raven.datetime.component.time.event.TimeActionListener; 7 | 8 | import javax.swing.*; 9 | import java.awt.*; 10 | import java.text.DateFormatSymbols; 11 | import java.text.DecimalFormat; 12 | import java.util.Locale; 13 | 14 | public class Header extends JPanel { 15 | 16 | private final TimePicker timePicker; 17 | private final TimeActionListener timeActionListener; 18 | private final DecimalFormat format = new DecimalFormat("00"); 19 | private MigLayout layout; 20 | private Color color; 21 | 22 | public Header(TimePicker timePicker, TimeActionListener timeActionListener) { 23 | this.timePicker = timePicker; 24 | this.timeActionListener = timeActionListener; 25 | init(); 26 | } 27 | 28 | private void init() { 29 | layout = new MigLayout("fill,insets 10", "center"); 30 | setLayout(layout); 31 | add(createToolBar(), "id b1"); 32 | add(createAmPm(), "pos b1.x2+rel 0.5al n n"); 33 | } 34 | 35 | public void updateHeader() { 36 | int hour = timePicker.getTimeSelectionModel().getHour(); 37 | int minute = timePicker.getTimeSelectionModel().getMinute(); 38 | 39 | if (hour == -1 && minute == -1) { 40 | buttonAm.setSelected(false); 41 | buttonPm.setSelected(false); 42 | } else { 43 | if (hour >= 12) { 44 | setSelectedAm(false); 45 | } else { 46 | setSelectedAm(true); 47 | } 48 | } 49 | 50 | if (!timePicker.is24HourView()) { 51 | if (hour >= 12) { 52 | hour -= 12; 53 | } 54 | if (hour == 0) { 55 | hour = 12; 56 | } 57 | } 58 | 59 | String hourText = hour == -1 ? "--" : format.format(hour); 60 | String minuteText = minute == -1 ? "--" : format.format(minute); 61 | buttonHour.setText(hourText); 62 | buttonMinute.setText(minuteText); 63 | } 64 | 65 | public void setOrientation(int orientation) { 66 | String c = orientation == SwingConstants.VERTICAL ? "pos b1.x2+rel 0.5al n n" : "pos 0.5al b1.y2+rel n n"; 67 | amPmToolBar.setOrientation(orientation); 68 | layout.setComponentConstraints(amPmToolBar, c); 69 | } 70 | 71 | public boolean isAm() { 72 | return !buttonPm.isSelected(); 73 | } 74 | 75 | public void setHourSelectionView(boolean hourSelectionView) { 76 | if (hourSelectionView) { 77 | buttonHour.setSelected(true); 78 | } else { 79 | buttonMinute.setSelected(true); 80 | } 81 | } 82 | 83 | public void setUse24hour(boolean use24hour) { 84 | amPmToolBar.setVisible(!use24hour); 85 | } 86 | 87 | protected JToolBar createToolBar() { 88 | JToolBar toolBar = new JToolBar(); 89 | toolBar.putClientProperty(FlatClientProperties.STYLE, "" + 90 | "background:null;" + 91 | "hoverButtonGroupBackground:null;"); 92 | buttonHour = createButton(); 93 | buttonMinute = createButton(); 94 | ButtonGroup group = new ButtonGroup(); 95 | group.add(buttonHour); 96 | group.add(buttonMinute); 97 | buttonHour.setSelected(true); 98 | buttonHour.addActionListener(e -> timeActionListener.selectionViewChanged(true)); 99 | buttonMinute.addActionListener(e -> timeActionListener.selectionViewChanged(false)); 100 | toolBar.add(buttonHour); 101 | toolBar.add(createSplit()); 102 | toolBar.add(buttonMinute); 103 | return toolBar; 104 | } 105 | 106 | protected JToggleButton createButton() { 107 | JToggleButton button = new JToggleButton("--"); 108 | button.putClientProperty(FlatClientProperties.STYLE, "" + 109 | "font:+15;" + 110 | "toolbar.margin:3,5,3,5;" + 111 | "foreground:contrast($Component.accentColor,$ToggleButton.background,#fff);" + 112 | "background:null;" + 113 | "toolbar.hoverBackground:null"); 114 | return button; 115 | } 116 | 117 | protected JButton createAmPmButton(boolean isAm) { 118 | String[] amPM = DateFormatSymbols.getInstance(Locale.ENGLISH).getAmPmStrings(); 119 | String amOrPm = isAm ? amPM[0] : amPM[1]; 120 | JButton button = new JButton(amOrPm); 121 | button.addActionListener(e -> actionAmPmChanged(isAm)); 122 | button.putClientProperty(FlatClientProperties.STYLE, "" + 123 | "font:+1;" + 124 | "foreground:contrast($Component.accentColor,$ToggleButton.background,#fff);" + 125 | "background:null;" + 126 | "toolbar.hoverBackground:null"); 127 | return button; 128 | } 129 | 130 | private void actionAmPmChanged(boolean isAm) { 131 | TimeSelectionModel timeSelectionModel = timePicker.getTimeSelectionModel(); 132 | int hour = timeSelectionModel.getHour(); 133 | int minute = timeSelectionModel.getMinute(); 134 | if (hour == -1 && minute == -1) { 135 | setSelectedAm(isAm); 136 | 137 | // need to repaint the panel clock to update the paint text selection able 138 | timePicker.repaint(); 139 | } else { 140 | if (isAm) { 141 | if (hour >= 12) { 142 | hour -= 12; 143 | } 144 | } else { 145 | if (hour < 12) { 146 | hour += 12; 147 | } 148 | } 149 | timeSelectionModel.set(hour, minute); 150 | } 151 | } 152 | 153 | private void setSelectedAm(boolean isAm) { 154 | buttonAm.setSelected(isAm); 155 | buttonPm.setSelected(!isAm); 156 | } 157 | 158 | protected JLabel createSplit() { 159 | JLabel label = new JLabel(":"); 160 | label.putClientProperty(FlatClientProperties.STYLE, "" + 161 | "font:+10;" + 162 | "foreground:contrast($Component.accentColor,$Label.background,#fff)"); 163 | return label; 164 | } 165 | 166 | protected JToolBar createAmPm() { 167 | amPmToolBar = new JToolBar(); 168 | amPmToolBar.setOrientation(SwingConstants.VERTICAL); 169 | amPmToolBar.putClientProperty(FlatClientProperties.STYLE, "" + 170 | "background:null;" + 171 | "hoverButtonGroupBackground:null"); 172 | buttonAm = createAmPmButton(true); 173 | buttonPm = createAmPmButton(false); 174 | amPmToolBar.add(buttonAm); 175 | amPmToolBar.add(buttonPm); 176 | return amPmToolBar; 177 | } 178 | 179 | public void setColor(Color color) { 180 | this.color = color; 181 | } 182 | 183 | /** 184 | * Override this method to return the background color to the JToolBar 185 | * When JToolBar use null background, so it will paint the parent background. 186 | */ 187 | @Override 188 | public Color getBackground() { 189 | if (color != null) { 190 | return color; 191 | } 192 | return UIManager.getColor("Component.accentColor"); 193 | } 194 | 195 | @Override 196 | public void setEnabled(boolean enabled) { 197 | super.setEnabled(enabled); 198 | buttonAm.setEnabled(enabled); 199 | buttonPm.setEnabled(enabled); 200 | } 201 | 202 | private JToggleButton buttonHour; 203 | private JToggleButton buttonMinute; 204 | 205 | private JToolBar amPmToolBar; 206 | private JButton buttonAm; 207 | private JButton buttonPm; 208 | } 209 | -------------------------------------------------------------------------------- /src/main/java/raven/datetime/component/time/PanelClock.java: -------------------------------------------------------------------------------- 1 | package raven.datetime.component.time; 2 | 3 | import com.formdev.flatlaf.FlatClientProperties; 4 | import com.formdev.flatlaf.FlatLaf; 5 | import com.formdev.flatlaf.ui.FlatUIUtils; 6 | import com.formdev.flatlaf.util.ColorFunctions; 7 | import com.formdev.flatlaf.util.UIScale; 8 | import raven.datetime.TimePicker; 9 | import raven.datetime.component.time.event.TimeActionListener; 10 | 11 | import javax.swing.*; 12 | import java.awt.*; 13 | import java.awt.event.MouseAdapter; 14 | import java.awt.event.MouseEvent; 15 | import java.awt.geom.*; 16 | 17 | public class PanelClock extends JPanel { 18 | 19 | private final TimePicker timePicker; 20 | private final TimeActionListener timeActionListener; 21 | private boolean use24hour; 22 | private boolean hourSelectionView = true; 23 | private AnimationChange animationChange; 24 | private final int margin12h = 20; 25 | private final int margin24h = 50; 26 | private Color color; 27 | 28 | public void setHourSelectionView(boolean hourSelectionView) { 29 | if (this.hourSelectionView != hourSelectionView) { 30 | this.hourSelectionView = hourSelectionView; 31 | repaint(); 32 | runAnimation(true); 33 | } 34 | } 35 | 36 | public void setHourSelectionViewImmediately(boolean hourSelectionView) { 37 | if (this.hourSelectionView != hourSelectionView) { 38 | this.hourSelectionView = hourSelectionView; 39 | repaint(); 40 | runAnimation(false); 41 | } 42 | } 43 | 44 | public boolean isHourSelectionView() { 45 | return hourSelectionView; 46 | } 47 | 48 | public void setUse24hour(boolean use24hour) { 49 | if (this.use24hour != use24hour) { 50 | this.use24hour = use24hour; 51 | } 52 | } 53 | 54 | public boolean isUse24hour() { 55 | return use24hour; 56 | } 57 | 58 | public PanelClock(TimePicker timePicker, TimeActionListener timeActionListener) { 59 | this.timePicker = timePicker; 60 | this.timeActionListener = timeActionListener; 61 | init(); 62 | } 63 | 64 | private void init() { 65 | animationChange = new AnimationChange(this); 66 | putClientProperty(FlatClientProperties.STYLE, "" + 67 | "border:5,15,5,15;" + 68 | "background:null;" + 69 | "foreground:contrast($Component.accentColor,$Panel.background,#fff)"); 70 | MouseAdapter mouseAdapter = new MouseAdapter() { 71 | @Override 72 | public void mousePressed(MouseEvent e) { 73 | if (isEnabled()) { 74 | mouseChanged(e); 75 | } 76 | } 77 | 78 | @Override 79 | public void mouseReleased(MouseEvent e) { 80 | if (isEnabled()) { 81 | timeActionListener.selectionViewChanged(hourSelectionView); 82 | } 83 | } 84 | 85 | @Override 86 | public void mouseDragged(MouseEvent e) { 87 | if (isEnabled()) { 88 | mouseChanged(e); 89 | } 90 | } 91 | 92 | private void mouseChanged(MouseEvent e) { 93 | int value = getValueOf(e.getPoint(), hourSelectionView); 94 | if (hourSelectionView) { 95 | timePicker.getTimeSelectionModel().setHour(value); 96 | } else { 97 | timePicker.getTimeSelectionModel().setMinute(value); 98 | } 99 | } 100 | }; 101 | addMouseListener(mouseAdapter); 102 | addMouseMotionListener(mouseAdapter); 103 | } 104 | 105 | public void updateClock() { 106 | TimeSelectionModel timeSelectionModel = timePicker.getTimeSelectionModel(); 107 | if (hourSelectionView) { 108 | animationChange.set(getAngleOf(timeSelectionModel.getHour(), true), getTargetMargin()); 109 | } else { 110 | animationChange.set(getAngleOf(timeSelectionModel.getMinute(), false), getTargetMargin()); 111 | } 112 | } 113 | 114 | @Override 115 | protected void paintComponent(Graphics g) { 116 | super.paintComponent(g); 117 | Graphics2D g2 = (Graphics2D) g.create(); 118 | FlatUIUtils.setRenderingHints(g2); 119 | Insets insets = getInsets(); 120 | int width = getWidth() - (insets.left + insets.right); 121 | int height = getHeight() - (insets.top + insets.bottom); 122 | 123 | int size = Math.min(width, height); 124 | g2.translate(insets.left, insets.top); 125 | int x = (width - size) / 2; 126 | int y = (height - size) / 2; 127 | 128 | // create clock background 129 | g2.setColor(getClockBackground()); 130 | g2.fill(new Ellipse2D.Double(x, y, size, size)); 131 | 132 | // create selection 133 | paintSelection(g2, x, y, size); 134 | 135 | // create clock number 136 | paintClockNumber(g2, x, y, size); 137 | g2.dispose(); 138 | } 139 | 140 | protected void paintSelection(Graphics2D g2, int x, int y, int size) { 141 | TimeSelectionModel timeSelectionModel = timePicker.getTimeSelectionModel(); 142 | AffineTransform tran = g2.getTransform(); 143 | size = size / 2; 144 | final float margin = UIScale.scale(animationChange.getMargin()); 145 | float centerSize = UIScale.scale(8f); 146 | float lineSize = UIScale.scale(3); 147 | float selectSize = UIScale.scale(25f); 148 | float unselectSize = UIScale.scale(4); 149 | float lineHeight = size - margin; 150 | Area area = new Area(new Ellipse2D.Float(x + size - (centerSize / 2), y + size - (centerSize / 2), centerSize, centerSize)); 151 | if ((hourSelectionView && timeSelectionModel.getHour() != -1) || (!hourSelectionView && timeSelectionModel.getMinute() != -1)) { 152 | area.add(new Area(new RoundRectangle2D.Float(x + size - (lineSize / 2), y + margin, lineSize, lineHeight, lineSize, lineSize))); 153 | area.add(new Area(new Ellipse2D.Float(x + size - (selectSize / 2), y + margin - selectSize / 2, selectSize, selectSize))); 154 | if (!hourSelectionView && !animationChange.isRunning() && (timeSelectionModel.getMinute() % 5 != 0)) { 155 | area.subtract(new Area(new Ellipse2D.Float(x + size - (unselectSize / 2), y + margin - unselectSize / 2, unselectSize, unselectSize))); 156 | } 157 | } 158 | g2.setColor(getSelectedColor()); 159 | float angle = animationChange.getAngle(); 160 | g2.rotate(Math.toRadians(angle), x + size, y + size); 161 | g2.fill(area); 162 | g2.setTransform(tran); 163 | } 164 | 165 | protected void paintClockNumber(Graphics2D g2, int x, int y, int size) { 166 | paintClockNumber(g2, x, y, size, margin12h, 0, hourSelectionView ? 1 : 5); 167 | if (hourSelectionView && use24hour) { 168 | paintClockNumber(g2, x, y, size, margin24h, 12, 1); 169 | } 170 | } 171 | 172 | protected void paintClockNumber(Graphics2D g2, int x, int y, int size, int margin, int start, int add) { 173 | TimeSelectionModel timeSelectionModel = timePicker.getTimeSelectionModel(); 174 | final int mg = UIScale.scale(margin); 175 | float center = size / 2f; 176 | float angle = 360 / 12f; 177 | for (int i = 1; i <= 12; i++) { 178 | float ag = angle * i - 90; 179 | int value = fixHour((start + i * add), hourSelectionView); 180 | float nx = (float) (center + (Math.cos(Math.toRadians(ag)) * (center - mg))); 181 | float ny = (float) (center + (Math.sin(Math.toRadians(ag)) * (center - mg))); 182 | 183 | int hour; 184 | int minute; 185 | if (hourSelectionView) { 186 | hour = valueToTime(value, true); 187 | minute = timeSelectionModel.getMinute(); 188 | } else { 189 | hour = timeSelectionModel.getHour(); 190 | minute = value; 191 | } 192 | boolean isSelectedAble = timeSelectionModel.checkSelection(hour, minute); 193 | paintNumber(g2, x + nx, y + ny, fixNumberAndToString(value), isSelected(value), isSelectedAble); 194 | } 195 | } 196 | 197 | protected void paintNumber(Graphics2D g2, float x, float y, String num, boolean isSelected, boolean isSelectedAble) { 198 | FontMetrics fm = g2.getFontMetrics(); 199 | Rectangle2D rec = fm.getStringBounds(num, g2); 200 | float x1 = (float) (x - rec.getWidth() / 2f); 201 | float y1 = (float) (y - rec.getHeight() / 2f); 202 | if (!isSelectedAble) { 203 | g2.setColor(UIManager.getColor("Label.disabledForeground")); 204 | } else if (isSelected) { 205 | g2.setColor(getSelectedForeground()); 206 | } else { 207 | g2.setColor(UIManager.getColor("Panel.foreground")); 208 | } 209 | g2.drawString(num, x1, y1 + fm.getAscent()); 210 | } 211 | 212 | protected Color getClockBackground() { 213 | if (FlatLaf.isLafDark()) { 214 | return ColorFunctions.lighten(getBackground(), 0.03f); 215 | } else { 216 | return ColorFunctions.darken(getBackground(), 0.03f); 217 | } 218 | } 219 | 220 | protected boolean isSelected(int value) { 221 | if (hourSelectionView) { 222 | int hour = getHourValue(timePicker.getTimeSelectionModel().getHour()); 223 | return value == hour; 224 | } else { 225 | return value == timePicker.getTimeSelectionModel().getMinute(); 226 | } 227 | } 228 | 229 | protected Color getSelectedColor() { 230 | if (color != null) { 231 | return color; 232 | } 233 | return UIManager.getColor("Component.accentColor"); 234 | } 235 | 236 | protected Color getSelectedForeground() { 237 | return getForeground(); 238 | } 239 | 240 | /** 241 | * Convert angle to hour or minute base on the hourView 242 | * Return value hour or minute 243 | */ 244 | private int getValueOf(float angle, boolean hourView) { 245 | float ag = angle / 360; 246 | int value = (int) (ag * (hourView ? 12 : 60)); 247 | if (hourView) { 248 | if (isUse24hour()) { 249 | return value == 0 ? 12 : value; 250 | } else { 251 | return value == 12 ? 0 : value; 252 | } 253 | } else { 254 | return value == 60 ? 0 : value; 255 | } 256 | } 257 | 258 | /** 259 | * Convert point location to the value hour or minute base on the hourView 260 | * Return value hour or minute 261 | */ 262 | private int getValueOf(Point point, boolean hourView) { 263 | float angle = getAngleOf(point) + (hourView ? 360 / 12f / 2f : 360 / 60f / 2f); 264 | int value = getValueOf(angle, hourView); 265 | if (hourView) { 266 | boolean isAdd12Hour = (!use24hour && !timePicker.getHeader().isAm()) 267 | || (use24hour && is24hourSelect(point)); 268 | return fixHour(value + (isAdd12Hour ? 12 : 0), true); 269 | } else { 270 | return value; 271 | } 272 | } 273 | 274 | private boolean is24hourSelect(Point point) { 275 | Insets insets = getInsets(); 276 | int width = getWidth() - (insets.left + insets.right); 277 | int height = getHeight() - (insets.top + insets.bottom); 278 | int size = Math.min(width, height) / 2; 279 | int distanceTarget = (size - UIScale.scale(margin12h + 15)); 280 | float centerX = insets.left + width / 2f; 281 | float centerY = insets.top + height / 2f; 282 | double distance = Math.sqrt(Math.pow((point.x - centerX), 2) + Math.pow((point.y - centerY), 2)); 283 | return distance < distanceTarget; 284 | } 285 | 286 | /** 287 | * Convert hour or minute to the angle base on the hourView 288 | * Return angle vales 289 | */ 290 | private float getAngleOf(int number, boolean hourView) { 291 | float ag = 360 / (hourView ? 12f : 60f); 292 | return fixAngle(ag * number); 293 | } 294 | 295 | /** 296 | * Convert point location to angle 297 | * Return angle 298 | */ 299 | private float getAngleOf(Point point) { 300 | Insets insets = getInsets(); 301 | int width = getWidth() - (insets.left + insets.right); 302 | int height = getHeight() - (insets.top + insets.bottom); 303 | float centerX = insets.left + width / 2f; 304 | float centerY = insets.top + height / 2f; 305 | float x = point.x - centerX; 306 | float y = point.y - centerY; 307 | double angle = Math.toDegrees(Math.atan2(y, x)) + 90; 308 | if (angle < 0) { 309 | angle += 360; 310 | } 311 | return (float) angle; 312 | } 313 | 314 | /** 315 | * Make the angle is between 0 and 360-1 316 | */ 317 | private float fixAngle(float angle) { 318 | if (angle > 360) { 319 | angle -= 360; 320 | } 321 | if (angle == 360) { 322 | return 0; 323 | } 324 | return angle; 325 | } 326 | 327 | /** 328 | * Fix hour or minute base on the hourView 329 | * If 24h ( return 0 to 23 ) 330 | * If 12h ( return 1 to 12 ) 331 | * If minute ( return 0 to 59 ) 332 | */ 333 | private int fixHour(int value, boolean hourView) { 334 | if (hourView) { 335 | if (use24hour) { 336 | if (value == 24) { 337 | return 0; 338 | } 339 | } 340 | } else { 341 | if (value == 60) { 342 | return 0; 343 | } 344 | } 345 | return value; 346 | } 347 | 348 | private String fixNumberAndToString(int num) { 349 | if (num == 0) { 350 | return "00"; 351 | } 352 | return num + ""; 353 | } 354 | 355 | private int getHourValue(int hour) { 356 | if (isUse24hour()) { 357 | return hour; 358 | } else { 359 | hour = (timePicker.getHeader().isAm() ? hour : hour - 12); 360 | return hour == 0 ? 12 : hour; 361 | } 362 | } 363 | 364 | private int valueToTime(int value, boolean hourView) { 365 | if (!hourView) { 366 | return value; 367 | } 368 | if (!isUse24hour()) { 369 | boolean isAm = timePicker.getHeader().isAm(); 370 | value += isAm ? 0 : 12; 371 | if (isAm && value == 12) { 372 | return 0; 373 | } 374 | if (!isAm && value == 24) { 375 | return 12; 376 | } 377 | } 378 | return value; 379 | } 380 | 381 | private boolean is24hour() { 382 | int hour = timePicker.getTimeSelectionModel().getHour(); 383 | return use24hour && (hour == 0 || hour > 12); 384 | } 385 | 386 | private int getTargetMargin() { 387 | return is24hour() && hourSelectionView ? margin24h : margin12h; 388 | } 389 | 390 | /** 391 | * Start animation selection change 392 | */ 393 | private void runAnimation(boolean animate) { 394 | int value = hourSelectionView ? timePicker.getTimeSelectionModel().getHour() : timePicker.getTimeSelectionModel().getMinute(); 395 | float angleTarget = getAngleOf(value, hourSelectionView); 396 | float marginTarget = getTargetMargin(); 397 | animationChange.start(angleTarget, marginTarget, animate); 398 | } 399 | 400 | public void setColor(Color color) { 401 | this.color = color; 402 | } 403 | } 404 | -------------------------------------------------------------------------------- /src/main/java/raven/datetime/component/time/TimeSelectionModel.java: -------------------------------------------------------------------------------- 1 | package raven.datetime.component.time; 2 | 3 | import raven.datetime.TimeSelectionAble; 4 | import raven.datetime.component.time.event.TimeSelectionModelEvent; 5 | import raven.datetime.component.time.event.TimeSelectionModelListener; 6 | 7 | import javax.swing.event.EventListenerList; 8 | import java.time.LocalTime; 9 | 10 | public class TimeSelectionModel { 11 | 12 | protected EventListenerList listenerList = new EventListenerList(); 13 | private TimeSelectionAble timeSelectionAble; 14 | private int hour = -1; 15 | private int minute = -1; 16 | 17 | public TimeSelectionModel() { 18 | } 19 | 20 | public TimeSelectionAble getTimeSelectionAble() { 21 | return timeSelectionAble; 22 | } 23 | 24 | public void setTimeSelectionAble(TimeSelectionAble timeSelectionAble) { 25 | this.timeSelectionAble = timeSelectionAble; 26 | } 27 | 28 | public int getHour() { 29 | return hour; 30 | } 31 | 32 | public void setHour(int hour) { 33 | if (!checkSelection(hour, minute)) { 34 | return; 35 | } 36 | if (this.hour != hour) { 37 | this.hour = hour; 38 | fireTimePickerChanged(new TimeSelectionModelEvent(this, TimeSelectionModelEvent.HOUR)); 39 | } 40 | } 41 | 42 | public int getMinute() { 43 | return minute; 44 | } 45 | 46 | public void setMinute(int minute) { 47 | if (hour == -1) { 48 | set(getDefaultSelectionHour(minute), minute); 49 | } else { 50 | if (this.minute != minute) { 51 | if (!checkSelection(hour, minute)) { 52 | return; 53 | } 54 | this.minute = minute; 55 | fireTimePickerChanged(new TimeSelectionModelEvent(this, TimeSelectionModelEvent.MINUTE)); 56 | } 57 | } 58 | } 59 | 60 | public boolean isSelected() { 61 | return hour != -1 && minute != -1; 62 | } 63 | 64 | public LocalTime getTime() { 65 | if (isSelected()) { 66 | return LocalTime.of(hour, minute); 67 | } 68 | return null; 69 | } 70 | 71 | public void set(int hour, int minute) { 72 | int action = 0; 73 | if (this.hour != hour && this.minute != minute) { 74 | action = TimeSelectionModelEvent.HOUR_MINUTE; 75 | } else if (this.hour != hour) { 76 | action = TimeSelectionModelEvent.HOUR; 77 | } else if (this.minute != minute) { 78 | action = TimeSelectionModelEvent.MINUTE; 79 | } 80 | if (action != 0) { 81 | if (!checkSelection(hour, minute)) { 82 | return; 83 | } 84 | this.hour = hour; 85 | this.minute = minute; 86 | fireTimePickerChanged(new TimeSelectionModelEvent(this, action)); 87 | } 88 | } 89 | 90 | /** 91 | * When hour unselected and user select the minute 92 | * So we need set the default hour 93 | */ 94 | public int getDefaultSelectionHour(int minute) { 95 | int defaultHour = 0; 96 | if (timeSelectionAble != null) { 97 | defaultHour = getAvailableDefaultHour(defaultHour, minute); 98 | } 99 | return defaultHour; 100 | } 101 | 102 | /** 103 | * Default hour can be disabled by timeSelectionAble 104 | * Use this method to get the available hour 105 | */ 106 | private int getAvailableDefaultHour(int startHour, int minute) { 107 | int hour = startHour; 108 | for (int i = 0; i < 23; i++) { 109 | if (checkSelection(hour, minute)) { 110 | return hour; 111 | } 112 | hour++; 113 | if (hour == 24) { 114 | hour = 0; 115 | } 116 | } 117 | return startHour; 118 | } 119 | 120 | /** 121 | * When check time selection able but minute not set 122 | * Use this for default minute to check the available hour 123 | */ 124 | private int getDefaultMinuteCheck() { 125 | return 0; 126 | } 127 | 128 | public boolean checkSelection(int hour, int minute) { 129 | if (timeSelectionAble == null || hour == -1 || (minute == -1 && hour == -1)) { 130 | return true; 131 | } 132 | // hourView true if only hour are set. use to display the hour on the PanelClock 133 | boolean hourView = false; 134 | if (minute == -1) { 135 | minute = getDefaultMinuteCheck(); 136 | hourView = true; 137 | } 138 | return timeSelectionAble.isTimeSelectedAble(LocalTime.of(hour, minute), hourView); 139 | } 140 | 141 | public void addTimePickerSelectionListener(TimeSelectionModelListener listener) { 142 | listenerList.add(TimeSelectionModelListener.class, listener); 143 | } 144 | 145 | public void removeTimePickerSelectionListener(TimeSelectionModelListener listener) { 146 | listenerList.remove(TimeSelectionModelListener.class, listener); 147 | } 148 | 149 | public void fireTimePickerChanged(TimeSelectionModelEvent event) { 150 | Object[] listeners = listenerList.getListenerList(); 151 | for (int i = listeners.length - 2; i >= 0; i -= 2) { 152 | if (listeners[i] == TimeSelectionModelListener.class) { 153 | ((TimeSelectionModelListener) listeners[i + 1]).timeSelectionModelChanged(event); 154 | } 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/main/java/raven/datetime/component/time/event/TimeActionListener.java: -------------------------------------------------------------------------------- 1 | package raven.datetime.component.time.event; 2 | 3 | public interface TimeActionListener { 4 | 5 | void selectionViewChanged(boolean isHourSelectionView); 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/raven/datetime/component/time/event/TimeSelectionModelEvent.java: -------------------------------------------------------------------------------- 1 | package raven.datetime.component.time.event; 2 | 3 | import java.util.EventObject; 4 | 5 | public class TimeSelectionModelEvent extends EventObject { 6 | 7 | public static final int HOUR = 1; 8 | public static final int MINUTE = 2; 9 | public static final int HOUR_MINUTE = 3; 10 | 11 | protected int action; 12 | 13 | public TimeSelectionModelEvent(Object source, int action) { 14 | super(source); 15 | this.action = action; 16 | } 17 | 18 | public int getAction() { 19 | return action; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/raven/datetime/component/time/event/TimeSelectionModelListener.java: -------------------------------------------------------------------------------- 1 | package raven.datetime.component.time.event; 2 | 3 | import java.util.EventListener; 4 | 5 | public interface TimeSelectionModelListener extends EventListener { 6 | 7 | void timeSelectionModelChanged(TimeSelectionModelEvent e); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/raven/datetime/event/DateSelectionEvent.java: -------------------------------------------------------------------------------- 1 | package raven.datetime.event; 2 | 3 | import java.util.EventObject; 4 | 5 | public class DateSelectionEvent extends EventObject { 6 | 7 | public DateSelectionEvent(Object source) { 8 | super(source); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/raven/datetime/event/DateSelectionListener.java: -------------------------------------------------------------------------------- 1 | package raven.datetime.event; 2 | 3 | import java.util.EventListener; 4 | 5 | public interface DateSelectionListener extends EventListener { 6 | 7 | void dateSelected(DateSelectionEvent dateSelectionEvent); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/raven/datetime/event/TimeSelectionEvent.java: -------------------------------------------------------------------------------- 1 | package raven.datetime.event; 2 | 3 | import java.util.EventObject; 4 | 5 | public class TimeSelectionEvent extends EventObject { 6 | 7 | public TimeSelectionEvent(Object source) { 8 | super(source); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/raven/datetime/event/TimeSelectionListener.java: -------------------------------------------------------------------------------- 1 | package raven.datetime.event; 2 | 3 | import java.util.EventListener; 4 | 5 | public interface TimeSelectionListener extends EventListener { 6 | 7 | void timeSelected(TimeSelectionEvent timeSelectionEvent); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/raven/datetime/swing/slider/PanelSlider.java: -------------------------------------------------------------------------------- 1 | package raven.datetime.swing.slider; 2 | 3 | import com.formdev.flatlaf.util.Animator; 4 | import com.formdev.flatlaf.util.CubicBezierEasing; 5 | 6 | import javax.swing.*; 7 | import java.awt.*; 8 | import java.awt.image.VolatileImage; 9 | 10 | public class PanelSlider extends JLayeredPane { 11 | 12 | public Component getSlideComponent() { 13 | return slideComponent; 14 | } 15 | 16 | private PanelSnapshot panelSnapshot; 17 | private Component slideComponent; 18 | 19 | public PanelSlider() { 20 | init(); 21 | } 22 | 23 | private void init() { 24 | panelSnapshot = new PanelSnapshot(); 25 | setLayout(new CardLayout()); 26 | setLayer(panelSnapshot, JLayeredPane.DRAG_LAYER); 27 | add(panelSnapshot); 28 | panelSnapshot.setVisible(false); 29 | } 30 | 31 | public void addSlide(Component component, SliderTransition transition) { 32 | this.slideComponent = component; 33 | if (getComponentCount() == 1) { 34 | add(component); 35 | repaint(); 36 | revalidate(); 37 | component.setVisible(true); 38 | } else { 39 | Component oldComponent = getComponent(1); 40 | add(component); 41 | if (transition != null) { 42 | doLayout(); 43 | component.doLayout(); 44 | Image oldImage = createImage(oldComponent); 45 | Image newImage = createImage(component); 46 | remove(oldComponent); 47 | panelSnapshot.animate(component, transition, oldImage, newImage); 48 | } else { 49 | component.setVisible(true); 50 | remove(oldComponent); 51 | revalidate(); 52 | repaint(); 53 | } 54 | } 55 | } 56 | 57 | private Image createImage(Component component) { 58 | VolatileImage snapshot = component.createVolatileImage(getWidth(), getHeight()); 59 | if (snapshot != null) { 60 | component.paint(snapshot.getGraphics()); 61 | } 62 | return snapshot; 63 | } 64 | 65 | private static class PanelSnapshot extends JComponent { 66 | 67 | private final Animator animator; 68 | private Component component; 69 | private float animate; 70 | 71 | private SliderTransition sliderTransition; 72 | private Image oldImage; 73 | private Image newImage; 74 | 75 | public PanelSnapshot() { 76 | animator = new Animator(400, new Animator.TimingTarget() { 77 | @Override 78 | public void timingEvent(float v) { 79 | animate = v; 80 | repaint(); 81 | } 82 | 83 | @Override 84 | public void end() { 85 | setVisible(false); 86 | component.setVisible(true); 87 | if (newImage != null) { 88 | newImage.flush(); 89 | } 90 | if (oldImage != null) { 91 | oldImage.flush(); 92 | } 93 | } 94 | }); 95 | animator.setInterpolator(CubicBezierEasing.EASE); 96 | } 97 | 98 | protected void animate(Component component, SliderTransition sliderTransition, Image oldImage, Image newImage) { 99 | if (animator.isRunning()) { 100 | animator.stop(); 101 | } 102 | this.component = component; 103 | this.oldImage = oldImage; 104 | this.newImage = newImage; 105 | this.sliderTransition = sliderTransition; 106 | this.animate = 0f; 107 | repaint(); 108 | setVisible(true); 109 | animator.start(); 110 | } 111 | 112 | @Override 113 | public void paint(Graphics g) { 114 | if (sliderTransition != null) { 115 | int width = getWidth(); 116 | int height = getHeight(); 117 | sliderTransition.render(g, oldImage, newImage, width, height, animate); 118 | } 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/main/java/raven/datetime/swing/slider/SimpleTransition.java: -------------------------------------------------------------------------------- 1 | package raven.datetime.swing.slider; 2 | 3 | import java.awt.*; 4 | 5 | public class SimpleTransition { 6 | 7 | public static SliderTransition get(SliderType sliderType) { 8 | SliderTransition transition = null; 9 | if (sliderType == SliderType.BACK) { 10 | transition = new SliderTransition() { 11 | 12 | @Override 13 | public void renderImageNew(Graphics g, Image image, int width, int height, float animate) { 14 | int x = -(int) (width * (1f - animate)); 15 | g.drawImage(image, x, 0, null); 16 | g.dispose(); 17 | } 18 | 19 | @Override 20 | public void renderImageOld(Graphics g, Image image, int width, int height, float animate) { 21 | int x = (int) (width * animate); 22 | g.drawImage(image, x, 0, null); 23 | g.dispose(); 24 | } 25 | }; 26 | } else if (sliderType == SliderType.FORWARD) { 27 | transition = new SliderTransition() { 28 | 29 | @Override 30 | public void renderImageNew(Graphics g, Image image, int width, int height, float animate) { 31 | int x = (int) (width - width * animate); 32 | g.drawImage(image, x, 0, null); 33 | g.dispose(); 34 | } 35 | 36 | @Override 37 | public void renderImageOld(Graphics g, Image image, int width, int height, float animate) { 38 | int x = -(int) (width * animate); 39 | g.drawImage(image, x, 0, null); 40 | g.dispose(); 41 | } 42 | }; 43 | } else if (sliderType == SliderType.ZOOM_OUT) { 44 | transition = new SliderTransition() { 45 | 46 | @Override 47 | public void renderImageNew(Graphics g, Image image, int width, int height, float animate) { 48 | Graphics2D g2 = (Graphics2D) g; 49 | g2.setComposite(AlphaComposite.SrcOver.derive(animate)); 50 | g.drawImage(image, 0, 0, null); 51 | g2.dispose(); 52 | } 53 | 54 | @Override 55 | public void renderImageOld(Graphics g, Image image, int width, int height, float animate) { 56 | Graphics2D g2 = (Graphics2D) g; 57 | float sx = 1 + animate; 58 | float sy = 1 + animate; 59 | int x = width / 2; 60 | int y = height / 2; 61 | g2.translate(x, y); 62 | g2.scale(sx, sy); 63 | g2.translate(-x, -y); 64 | g2.setComposite(AlphaComposite.SrcOver.derive(1f - animate)); 65 | g.drawImage(image, 0, 0, null); 66 | g2.dispose(); 67 | } 68 | }; 69 | } else if (sliderType == SliderType.TOP_DOWN) { 70 | transition = new SliderTransition() { 71 | 72 | @Override 73 | public void renderImageNew(Graphics g, Image image, int width, int height, float animate) { 74 | Graphics2D g2 = (Graphics2D) g; 75 | int y = (int) (-height + height * animate); 76 | g.drawImage(image, 0, y, null); 77 | g2.dispose(); 78 | } 79 | 80 | @Override 81 | public void renderImageOld(Graphics g, Image image, int width, int height, float animate) { 82 | Graphics2D g2 = (Graphics2D) g; 83 | g.drawImage(image, 0, 0, null); 84 | g2.dispose(); 85 | } 86 | }; 87 | } else if (sliderType == SliderType.DOWN_TOP) { 88 | transition = new SliderTransition() { 89 | 90 | @Override 91 | public void renderImageNew(Graphics g, Image image, int width, int height, float animate) { 92 | Graphics2D g2 = (Graphics2D) g; 93 | g.drawImage(image, 0, 0, null); 94 | g2.dispose(); 95 | } 96 | 97 | @Override 98 | public void renderImageOld(Graphics g, Image image, int width, int height, float animate) { 99 | Graphics2D g2 = (Graphics2D) g; 100 | int y = (int) ( - height * animate); 101 | g.drawImage(image, 0, y, null); 102 | g2.dispose(); 103 | } 104 | 105 | @Override 106 | public void render(Graphics g, Image imageOld, Image imageNew, int width, int height, float animate) { 107 | renderImageNew(g.create(), imageNew, width, height, animate); 108 | renderImageOld(g.create(), imageOld, width, height, animate); 109 | } 110 | }; 111 | } 112 | return transition; 113 | } 114 | 115 | public enum SliderType { 116 | DEFAULT, BACK, FORWARD, ZOOM_IN, ZOOM_OUT, TOP_DOWN, DOWN_TOP 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/main/java/raven/datetime/swing/slider/SliderTransition.java: -------------------------------------------------------------------------------- 1 | package raven.datetime.swing.slider; 2 | 3 | import java.awt.*; 4 | 5 | public abstract class SliderTransition { 6 | 7 | public abstract void renderImageOld(Graphics g, Image image, int width, int height, float animate); 8 | 9 | public abstract void renderImageNew(Graphics g, Image image, int width, int height, float animate); 10 | 11 | public void render(Graphics g, Image imageOld, Image imageNew, int width, int height, float animate) { 12 | renderImageOld(g.create(), imageOld, width, height, animate); 13 | renderImageNew(g.create(), imageNew, width, height, animate); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/raven/datetime/util/InputUtils.java: -------------------------------------------------------------------------------- 1 | package raven.datetime.util; 2 | 3 | import com.formdev.flatlaf.FlatClientProperties; 4 | 5 | import javax.swing.*; 6 | import javax.swing.text.DefaultFormatterFactory; 7 | import javax.swing.text.MaskFormatter; 8 | import java.awt.*; 9 | import java.beans.PropertyChangeListener; 10 | import java.text.DateFormat; 11 | import java.text.ParseException; 12 | import java.text.SimpleDateFormat; 13 | import java.time.DateTimeException; 14 | import java.time.LocalDate; 15 | import java.time.LocalTime; 16 | import java.time.ZoneId; 17 | import java.time.format.DateTimeFormatter; 18 | import java.time.format.DateTimeFormatterBuilder; 19 | import java.util.Date; 20 | import java.util.HashMap; 21 | import java.util.Locale; 22 | import java.util.Map; 23 | import java.util.function.Consumer; 24 | 25 | public class InputUtils extends MaskFormatter { 26 | 27 | private static Map inputMap; 28 | 29 | public static LocalDate dateToLocalDate(Date date) { 30 | return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); 31 | } 32 | 33 | public static LocalTime stringToTime(boolean use24h, String value) { 34 | try { 35 | if (use24h) { 36 | final DateTimeFormatter format24h = DateTimeFormatter.ofPattern("HH:mm", Locale.ENGLISH); 37 | return LocalTime.from(format24h.parse(value)); 38 | } else { 39 | final DateTimeFormatter format12h = DateTimeFormatter.ofPattern("hh:mm a", Locale.ENGLISH); 40 | return LocalTime.from(format12h.parse(value.toUpperCase())); 41 | } 42 | } catch (Exception e) { 43 | System.err.println(e.getMessage()); 44 | return null; 45 | } 46 | } 47 | 48 | public static LocalDate stringToDate(DateTimeFormatter format, String value) { 49 | try { 50 | return LocalDate.from(format.parse(value)); 51 | } catch (Exception e) { 52 | return null; 53 | } 54 | } 55 | 56 | public static LocalDate[] stringToDate(DateTimeFormatter format, String separator, String value) { 57 | try { 58 | String[] dates = value.split(separator); 59 | LocalDate from = LocalDate.from(format.parse(dates[0])); 60 | LocalDate to = LocalDate.from(format.parse(dates[1])); 61 | return new LocalDate[]{from, to}; 62 | } catch (Exception e) { 63 | return null; 64 | } 65 | } 66 | 67 | public static void useTimeInput(JFormattedTextField txt, boolean use24h, ValueCallback callback, InputValidationListener inputValidationListener) { 68 | try { 69 | TimeInputFormat mask = new TimeInputFormat(use24h ? "##:##" : "##:## ??", use24h, inputValidationListener); 70 | OldEditorProperty oldEditorProperty = initEditor(txt, mask, callback); 71 | 72 | PropertyChangeListener propertyChangeListener = evt -> callback.valueChanged(txt.getValue()); 73 | txt.addPropertyChangeListener("value", propertyChangeListener); 74 | oldEditorProperty.propertyChangeListener = propertyChangeListener; 75 | putPropertyChange(txt, oldEditorProperty); 76 | } catch (ParseException e) { 77 | System.err.println(e.getMessage()); 78 | } 79 | } 80 | 81 | public static void useDateInput(JFormattedTextField txt, String pattern, boolean between, String separator, ValueCallback callback, InputValidationListener inputValidationListener) { 82 | try { 83 | String format = datePatternToInputFormat(pattern, "#"); 84 | DateInputFormat mask = new DateInputFormat(between ? format + separator + format : format, between, separator, pattern, inputValidationListener); 85 | OldEditorProperty oldEditorProperty = initEditor(txt, mask, callback); 86 | 87 | if (callback != null) { 88 | PropertyChangeListener propertyChangeListener = evt -> callback.valueChanged(txt.getValue()); 89 | txt.addPropertyChangeListener("value", propertyChangeListener); 90 | oldEditorProperty.propertyChangeListener = propertyChangeListener; 91 | } 92 | putPropertyChange(txt, oldEditorProperty); 93 | } catch (ParseException e) { 94 | System.err.println(e.getMessage()); 95 | } 96 | } 97 | 98 | public static void changeTimeFormatted(JFormattedTextField txt, boolean use24h, InputValidationListener inputValidationListener) { 99 | try { 100 | TimeInputFormat mask = new TimeInputFormat(use24h ? "##:##" : "##:## ??", use24h, inputValidationListener); 101 | mask.setCommitsOnValidEdit(true); 102 | mask.setPlaceholderCharacter('-'); 103 | DefaultFormatterFactory df = new DefaultFormatterFactory(mask); 104 | txt.setFormatterFactory(df); 105 | } catch (ParseException e) { 106 | System.err.println(e.getMessage()); 107 | } 108 | } 109 | 110 | public static void changeDateFormatted(JFormattedTextField txt, String pattern, boolean between, String separator, InputValidationListener inputValidationListener) { 111 | try { 112 | String format = datePatternToInputFormat(pattern, "#"); 113 | DateInputFormat mask = new DateInputFormat(between ? format + separator + format : format, between, separator, pattern, inputValidationListener); 114 | mask.setCommitsOnValidEdit(true); 115 | mask.setPlaceholderCharacter('-'); 116 | DefaultFormatterFactory df = new DefaultFormatterFactory(mask); 117 | txt.setFormatterFactory(df); 118 | } catch (ParseException e) { 119 | System.err.println(e.getMessage()); 120 | } 121 | } 122 | 123 | public static String datePatternToInputFormat(String pattern, String rpm) { 124 | String regex = "[dmy]"; 125 | return pattern.toLowerCase().replaceAll(regex, rpm); 126 | } 127 | 128 | private static OldEditorProperty initEditor(JFormattedTextField txt, MaskFormatter format, ValueCallback callback) { 129 | removePropertyChange(txt); 130 | OldEditorProperty oldEditorProperty = OldEditorProperty.getFromOldEditor(txt); 131 | format.setCommitsOnValidEdit(true); 132 | format.setPlaceholderCharacter('-'); 133 | DefaultFormatterFactory df = new DefaultFormatterFactory(format); 134 | txt.setFormatterFactory(df); 135 | 136 | txt.putClientProperty(FlatClientProperties.TEXT_FIELD_SHOW_CLEAR_BUTTON, true); 137 | txt.putClientProperty(FlatClientProperties.TEXT_FIELD_CLEAR_CALLBACK, (Consumer) o -> { 138 | txt.setValue(null); 139 | if (callback != null) { 140 | callback.valueChanged(null); 141 | } 142 | }); 143 | return oldEditorProperty; 144 | } 145 | 146 | private static void putPropertyChange(JFormattedTextField txt, OldEditorProperty oldEditorProperty) { 147 | if (inputMap == null) { 148 | inputMap = new HashMap<>(); 149 | } 150 | inputMap.put(txt, oldEditorProperty); 151 | } 152 | 153 | public static void removePropertyChange(JFormattedTextField txt) { 154 | if (inputMap == null) { 155 | return; 156 | } 157 | OldEditorProperty oldEditorProperty = inputMap.get(txt); 158 | if (oldEditorProperty != null) { 159 | oldEditorProperty.removeFromEditor(txt); 160 | inputMap.remove(txt); 161 | } 162 | } 163 | 164 | private static class TimeInputFormat extends MaskFormatter { 165 | 166 | private final InputValidationListener inputValidationListener; 167 | private final DateTimeFormatter timeFormat; 168 | 169 | public TimeInputFormat(String mark, boolean use24h, InputValidationListener inputValidationListener) throws ParseException { 170 | super(mark); 171 | this.inputValidationListener = inputValidationListener; 172 | this.timeFormat = new DateTimeFormatterBuilder() 173 | .parseCaseInsensitive() 174 | .appendPattern(use24h ? "HH:mm" : "hh:mm a") 175 | .toFormatter(Locale.ENGLISH); 176 | } 177 | 178 | @Override 179 | public Object stringToValue(String value) throws ParseException { 180 | checkTime(value); 181 | return super.stringToValue(value); 182 | } 183 | 184 | public void checkTime(String value) throws ParseException { 185 | try { 186 | LocalTime time = LocalTime.parse(value, timeFormat); 187 | 188 | if (inputValidationListener == null) return; 189 | 190 | // validate time selection able 191 | if (inputValidationListener.isValidation()) { 192 | if (!inputValidationListener.checkSelectionAble(time)) { 193 | throw new DateTimeException("error selection able"); 194 | } 195 | } 196 | inputValidationListener.inputChanged(true); 197 | } catch (DateTimeException e) { 198 | if (inputValidationListener != null) { 199 | inputValidationListener.inputChanged(false); 200 | } 201 | throw new ParseException(e.getMessage(), 0); 202 | } 203 | } 204 | } 205 | 206 | private static class DateInputFormat extends MaskFormatter { 207 | 208 | private final boolean between; 209 | private final String separator; 210 | private final DateFormat dateFormat; 211 | private final InputValidationListener inputValidationListener; 212 | 213 | public DateInputFormat(String mark, boolean between, String separator, String pattern, InputValidationListener inputValidationListener) throws ParseException { 214 | super(mark); 215 | this.between = between; 216 | this.separator = separator; 217 | this.dateFormat = new SimpleDateFormat(pattern); 218 | this.inputValidationListener = inputValidationListener; 219 | this.dateFormat.setLenient(false); 220 | } 221 | 222 | @Override 223 | public Object stringToValue(String value) throws ParseException { 224 | checkTime(value); 225 | return super.stringToValue(value); 226 | } 227 | 228 | public void checkTime(String value) throws ParseException { 229 | try { 230 | if (between) { 231 | String[] values = value.split(separator); 232 | Date fromDate = dateFormat.parse(values[0]); 233 | Date toDate = dateFormat.parse(values[1]); 234 | 235 | if (inputValidationListener == null) return; 236 | 237 | // validate date selection able 238 | if (inputValidationListener.isValidation()) { 239 | LocalDate date1 = dateToLocalDate(fromDate); 240 | LocalDate date2 = dateToLocalDate(toDate); 241 | if (!inputValidationListener.checkSelectionAble(date1) || 242 | !inputValidationListener.checkSelectionAble(date2)) { 243 | throw new ParseException("error selection able", 0); 244 | } 245 | } 246 | } else { 247 | Date date = dateFormat.parse(value); 248 | 249 | if (inputValidationListener == null) return; 250 | 251 | // validate date selection able 252 | if (inputValidationListener.isValidation()) { 253 | LocalDate d = dateToLocalDate(date); 254 | if (!inputValidationListener.checkSelectionAble(d)) { 255 | throw new ParseException("error selection able", 0); 256 | } 257 | } 258 | } 259 | inputValidationListener.inputChanged(true); 260 | } catch (ParseException e) { 261 | if (inputValidationListener != null) { 262 | inputValidationListener.inputChanged(false); 263 | } 264 | throw e; 265 | } 266 | } 267 | } 268 | 269 | public interface ValueCallback { 270 | void valueChanged(Object value); 271 | } 272 | 273 | private static class OldEditorProperty { 274 | 275 | protected PropertyChangeListener propertyChangeListener; 276 | protected JFormattedTextField.AbstractFormatterFactory formatter; 277 | protected Component oldTrailingComponent; 278 | private boolean isShowClearButton; 279 | private Consumer clearButtonCallback; 280 | private String outline; 281 | protected Object value; 282 | protected String text; 283 | 284 | protected static OldEditorProperty getFromOldEditor(JFormattedTextField editor) { 285 | OldEditorProperty oldEditorProperty = new OldEditorProperty(); 286 | oldEditorProperty.formatter = editor.getFormatterFactory(); 287 | oldEditorProperty.oldTrailingComponent = FlatClientProperties.clientProperty(editor, FlatClientProperties.TEXT_FIELD_TRAILING_COMPONENT, null, Component.class); 288 | oldEditorProperty.isShowClearButton = FlatClientProperties.clientPropertyBoolean(editor, FlatClientProperties.TEXT_FIELD_SHOW_CLEAR_BUTTON, false); 289 | oldEditorProperty.clearButtonCallback = FlatClientProperties.clientProperty(editor, FlatClientProperties.TEXT_FIELD_CLEAR_CALLBACK, null, Consumer.class); 290 | oldEditorProperty.outline = FlatClientProperties.clientProperty(editor, FlatClientProperties.OUTLINE, null, String.class); 291 | oldEditorProperty.value = editor.getValue(); 292 | oldEditorProperty.text = editor.getText(); 293 | return oldEditorProperty; 294 | } 295 | 296 | protected void removeFromEditor(JFormattedTextField editor) { 297 | editor.removePropertyChangeListener("value", propertyChangeListener); 298 | editor.setFormatterFactory(formatter); 299 | editor.putClientProperty(FlatClientProperties.TEXT_FIELD_TRAILING_COMPONENT, oldTrailingComponent); 300 | editor.putClientProperty(FlatClientProperties.TEXT_FIELD_SHOW_CLEAR_BUTTON, isShowClearButton); 301 | editor.putClientProperty(FlatClientProperties.TEXT_FIELD_SHOW_CLEAR_BUTTON, clearButtonCallback); 302 | editor.putClientProperty(FlatClientProperties.OUTLINE, outline); 303 | editor.setValue(value); 304 | editor.setText(text); 305 | } 306 | } 307 | } 308 | -------------------------------------------------------------------------------- /src/main/java/raven/datetime/util/InputValidationListener.java: -------------------------------------------------------------------------------- 1 | package raven.datetime.util; 2 | 3 | public interface InputValidationListener { 4 | 5 | boolean isValidation(); 6 | 7 | void inputChanged(boolean isValid); 8 | 9 | boolean checkSelectionAble(T data); 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/raven/datetime/util/Utils.java: -------------------------------------------------------------------------------- 1 | package raven.datetime.util; 2 | 3 | import com.formdev.flatlaf.FlatLaf; 4 | import com.formdev.flatlaf.ui.FlatUIUtils; 5 | import com.formdev.flatlaf.util.ColorFunctions; 6 | import com.formdev.flatlaf.util.UIScale; 7 | 8 | import javax.swing.*; 9 | import java.awt.*; 10 | 11 | public class Utils { 12 | 13 | public static Point adjustPopupLocation(JPopupMenu popupMenu, Component component, Point space) { 14 | Window window = SwingUtilities.getWindowAncestor(component); 15 | if (window == null) { 16 | return new Point(0, component.getHeight()); 17 | } 18 | Insets frameInsets = window.getInsets(); 19 | frameInsets = FlatUIUtils.addInsets(frameInsets, UIScale.scale(new Insets(5, 5, 5, 5))); 20 | Dimension popupSize = popupMenu.getComponent().getPreferredSize(); 21 | int frameWidth = window.getWidth() - (frameInsets.left + frameInsets.right); 22 | int frameHeight = window.getHeight() - (frameInsets.top + frameInsets.bottom); 23 | Point locationOnFrame = SwingUtilities.convertPoint(component, new Point(0, component.getHeight()), window); 24 | int bottomSpace = frameHeight - locationOnFrame.y - popupSize.height - space.y; 25 | int rightSpace = frameWidth - locationOnFrame.x - popupSize.width - space.x; 26 | int x = frameInsets.left + rightSpace > 0 ? locationOnFrame.x + space.x : Math.max(locationOnFrame.x + component.getWidth() - popupSize.width - space.x, frameInsets.left); 27 | int y = frameInsets.top + bottomSpace > 0 ? locationOnFrame.y + space.y : Math.max(locationOnFrame.y - component.getHeight() - popupSize.height - space.y, frameInsets.top); 28 | return SwingUtilities.convertPoint(window, new Point(x, y), component); 29 | } 30 | 31 | public static Color getColor(Color color, boolean press, boolean hover) { 32 | if (press) { 33 | return FlatLaf.isLafDark() ? ColorFunctions.lighten(color, 0.1f) : ColorFunctions.darken(color, 0.1f); 34 | } else if (hover) { 35 | return FlatLaf.isLafDark() ? ColorFunctions.lighten(color, 0.03f) : ColorFunctions.darken(color, 0.03f); 36 | } 37 | return color; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/resources/raven/datetime/icon/back.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/main/resources/raven/datetime/icon/calendar.svg: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/main/resources/raven/datetime/icon/clock.svg: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/main/resources/raven/datetime/icon/forward.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/test/java/test/TestDate.java: -------------------------------------------------------------------------------- 1 | package test; 2 | 3 | import com.formdev.flatlaf.themes.FlatMacDarkLaf; 4 | import net.miginfocom.swing.MigLayout; 5 | import raven.datetime.DatePicker; 6 | 7 | import javax.swing.*; 8 | import javax.swing.border.TitledBorder; 9 | import java.awt.*; 10 | import java.time.LocalDate; 11 | import java.time.format.DateTimeFormatter; 12 | 13 | public class TestDate extends TestFrame { 14 | 15 | private DatePicker datePicker; 16 | 17 | public TestDate() { 18 | setLayout(new MigLayout("wrap")); 19 | datePicker = new DatePicker(); 20 | datePicker.addDateSelectionListener(dateEvent -> { 21 | DateTimeFormatter df = DateTimeFormatter.ofPattern("dd-MM-yyyy"); 22 | if (datePicker.getDateSelectionMode() == DatePicker.DateSelectionMode.SINGLE_DATE_SELECTED) { 23 | LocalDate date = datePicker.getSelectedDate(); 24 | if (date != null) { 25 | System.out.println("date change " + df.format(datePicker.getSelectedDate())); 26 | } else { 27 | System.out.println("date change to null"); 28 | } 29 | } else { 30 | LocalDate dates[] = datePicker.getSelectedDateRange(); 31 | if (dates != null) { 32 | System.out.println("date change " + df.format(dates[0]) + " to " + df.format(dates[1])); 33 | } else { 34 | System.out.println("date change to null"); 35 | } 36 | } 37 | }); 38 | 39 | datePicker.setDateSelectionAble((date) -> !date.isAfter(LocalDate.now())); 40 | 41 | datePicker.now(); 42 | JFormattedTextField editor = new JFormattedTextField(); 43 | datePicker.setEditor(editor); 44 | add(editor, "width 250"); 45 | createDateOption(); 46 | } 47 | 48 | private void createDateOption() { 49 | JPanel panel = new JPanel(new MigLayout("wrap")); 50 | panel.setBorder(new TitledBorder("Option")); 51 | JCheckBox chBtw = new JCheckBox("Use date between"); 52 | chBtw.addActionListener(e -> { 53 | if (chBtw.isSelected()) { 54 | datePicker.setDateSelectionMode(DatePicker.DateSelectionMode.BETWEEN_DATE_SELECTED); 55 | } else { 56 | datePicker.setDateSelectionMode(DatePicker.DateSelectionMode.SINGLE_DATE_SELECTED); 57 | } 58 | }); 59 | JCheckBox chDateOpt = new JCheckBox("Use panel option"); 60 | chDateOpt.addActionListener(e -> datePicker.setUsePanelOption(chDateOpt.isSelected())); 61 | JCheckBox chClose = new JCheckBox("Close after selected"); 62 | chClose.addActionListener(e -> datePicker.setCloseAfterSelected(chClose.isSelected())); 63 | 64 | JCheckBox chEditorValidation = new JCheckBox("Editor validation", datePicker.isEditorValidation()); 65 | JCheckBox chValidationOnNull = new JCheckBox("Validation on null"); 66 | JCheckBox chAnimationEnabled = new JCheckBox("Animation enabled", datePicker.isAnimationEnabled()); 67 | JCheckBox chStartWeekOnMonday = new JCheckBox("Start week on monday", datePicker.isStartWeekOnMonday()); 68 | 69 | chEditorValidation.addActionListener(e -> { 70 | datePicker.setEditorValidation(chEditorValidation.isSelected()); 71 | chValidationOnNull.setEnabled(chEditorValidation.isSelected()); 72 | }); 73 | 74 | chValidationOnNull.addActionListener(e -> datePicker.setValidationOnNull(chValidationOnNull.isSelected())); 75 | chAnimationEnabled.addActionListener(e -> datePicker.setAnimationEnabled(chAnimationEnabled.isSelected())); 76 | chStartWeekOnMonday.addActionListener(e -> datePicker.setStartWeekOnMonday(chStartWeekOnMonday.isSelected())); 77 | 78 | panel.add(chBtw); 79 | panel.add(chDateOpt); 80 | panel.add(chClose); 81 | panel.add(chEditorValidation); 82 | panel.add(chValidationOnNull); 83 | panel.add(chAnimationEnabled); 84 | panel.add(chStartWeekOnMonday); 85 | add(panel); 86 | } 87 | 88 | public static void main(String[] args) { 89 | FlatMacDarkLaf.setup(); 90 | EventQueue.invokeLater(() -> new TestDate().setVisible(true)); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/test/java/test/TestFrame.java: -------------------------------------------------------------------------------- 1 | package test; 2 | 3 | import com.formdev.flatlaf.FlatLaf; 4 | import com.formdev.flatlaf.extras.FlatAnimatedLafChange; 5 | import com.formdev.flatlaf.intellijthemes.FlatAllIJThemes; 6 | 7 | import javax.swing.*; 8 | import java.awt.*; 9 | 10 | public class TestFrame extends JFrame { 11 | 12 | public TestFrame() { 13 | setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 14 | setSize(new Dimension(800, 600)); 15 | setLocationRelativeTo(null); 16 | createMenuBar(); 17 | } 18 | 19 | private void createMenuBar() { 20 | JMenuBar menuBar = new JMenuBar(); 21 | JMenu menuFile = new JMenu("File"); 22 | JMenu menuThemes = new JMenu("Themes"); 23 | JMenuItem menuExit = new JMenuItem("Exit"); 24 | ButtonGroup group = new ButtonGroup(); 25 | JCheckBoxMenuItem menuMacDark = new JCheckBoxMenuItem("Mac Dark"); 26 | JCheckBoxMenuItem menuMacLight = new JCheckBoxMenuItem("Mac Light"); 27 | group.add(menuMacDark); 28 | group.add(menuMacLight); 29 | 30 | menuMacDark.setSelected(true); 31 | 32 | menuExit.addActionListener(e -> System.exit(0)); 33 | 34 | menuFile.add(menuExit); 35 | for (FlatAllIJThemes.FlatIJLookAndFeelInfo themeInfo : FlatAllIJThemes.INFOS) { 36 | menuThemes.add(createThemeButton(group, themeInfo)); 37 | } 38 | menuBar.add(menuFile); 39 | menuBar.add(menuThemes); 40 | setJMenuBar(menuBar); 41 | } 42 | 43 | private JMenuItem createThemeButton(ButtonGroup group, FlatAllIJThemes.FlatIJLookAndFeelInfo themeInfo) { 44 | JCheckBoxMenuItem menu = new JCheckBoxMenuItem(themeInfo.getName()); 45 | menu.addActionListener(e -> changeTheme(themeInfo)); 46 | group.add(menu); 47 | return menu; 48 | } 49 | 50 | private void changeTheme(FlatAllIJThemes.FlatIJLookAndFeelInfo themeInfo) { 51 | EventQueue.invokeLater(() -> { 52 | FlatAnimatedLafChange.showSnapshot(); 53 | try { 54 | UIManager.setLookAndFeel(themeInfo.getClassName()); 55 | } catch (Exception e) { 56 | e.printStackTrace(); 57 | } 58 | FlatLaf.updateUI(); 59 | FlatAnimatedLafChange.hideSnapshotWithAnimation(); 60 | }); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/test/java/test/TestTime.java: -------------------------------------------------------------------------------- 1 | package test; 2 | 3 | import com.formdev.flatlaf.themes.FlatMacLightLaf; 4 | import net.miginfocom.swing.MigLayout; 5 | import raven.datetime.TimePicker; 6 | 7 | import javax.swing.*; 8 | import javax.swing.border.TitledBorder; 9 | import java.awt.*; 10 | import java.time.LocalTime; 11 | import java.time.format.DateTimeFormatter; 12 | 13 | public class TestTime extends TestFrame { 14 | 15 | private TimePicker timePicker; 16 | 17 | public TestTime() { 18 | setLayout(new MigLayout("wrap")); 19 | timePicker = new TimePicker(); 20 | timePicker.addTimeSelectionListener(timeEvent -> { 21 | if (timePicker.isTimeSelected()) { 22 | DateTimeFormatter df = DateTimeFormatter.ofPattern("hh:mm a"); 23 | System.out.println("event selected : " + timePicker.getSelectedTime().format(df)); 24 | } else { 25 | System.out.println("event selected : null"); 26 | } 27 | }); 28 | 29 | // set enable selection time from 30 | // 0 to 19:30 or 31 | // 12:00 am to 07:30 pm 32 | // timePicker.setTimeSelectionAble((time, hourView) -> !time.isAfter(LocalTime.of(19, 30))); 33 | 34 | timePicker.now(); 35 | 36 | JFormattedTextField editor = new JFormattedTextField(); 37 | timePicker.setEditor(editor); 38 | add(editor, "width 200"); 39 | createTimeOption(); 40 | } 41 | 42 | private void createTimeOption() { 43 | JPanel panel = new JPanel(new MigLayout("wrap", "[150]")); 44 | panel.setBorder(new TitledBorder("Option")); 45 | JCheckBox ch24hourView = new JCheckBox("24 hour view"); 46 | JCheckBox chHorizontal = new JCheckBox("Horizontal"); 47 | JCheckBox chDisablePast = new JCheckBox("Disable past"); 48 | JCheckBox chEditorValidation = new JCheckBox("Editor validation", timePicker.isEditorValidation()); 49 | JCheckBox chValidationOnNull = new JCheckBox("Validation on null"); 50 | ch24hourView.addActionListener(e -> { 51 | timePicker.set24HourView(ch24hourView.isSelected()); 52 | }); 53 | chHorizontal.addActionListener(e -> timePicker.setOrientation(chHorizontal.isSelected() ? SwingConstants.HORIZONTAL : SwingConstants.VERTICAL)); 54 | chDisablePast.addActionListener(e -> { 55 | boolean disable = chDisablePast.isSelected(); 56 | if (disable) { 57 | disablePast(); 58 | } else { 59 | timePicker.setTimeSelectionAble(null); 60 | } 61 | }); 62 | chEditorValidation.addActionListener(e -> { 63 | timePicker.setEditorValidation(chEditorValidation.isSelected()); 64 | chValidationOnNull.setEnabled(chEditorValidation.isSelected()); 65 | }); 66 | 67 | chValidationOnNull.addActionListener(e -> timePicker.setValidationOnNull(chValidationOnNull.isSelected())); 68 | 69 | panel.add(ch24hourView); 70 | panel.add(chHorizontal); 71 | panel.add(chDisablePast); 72 | panel.add(chEditorValidation); 73 | panel.add(chValidationOnNull); 74 | add(panel); 75 | } 76 | 77 | private void disablePast() { 78 | timePicker.setTimeSelectionAble((time, hourView) -> { 79 | LocalTime now = LocalTime.now().withSecond(0).withNano(0); 80 | if (hourView) { 81 | // use this to enable the hour selection on the PanelClock 82 | return time.getHour() >= now.getHour(); 83 | } 84 | return !time.isBefore(now); 85 | }); 86 | } 87 | 88 | public static void main(String[] args) { 89 | FlatMacLightLaf.setup(); 90 | EventQueue.invokeLater(() -> new TestTime().setVisible(true)); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/test/java/test/renderer/CustomDateCellRenderer.java: -------------------------------------------------------------------------------- 1 | package test.renderer; 2 | 3 | import com.formdev.flatlaf.util.UIScale; 4 | import raven.datetime.DatePicker; 5 | import raven.datetime.component.date.ButtonDate; 6 | import raven.datetime.component.date.DefaultDateCellRenderer; 7 | import raven.datetime.component.date.SingleDate; 8 | 9 | import java.awt.*; 10 | import java.awt.geom.Arc2D; 11 | import java.awt.geom.Rectangle2D; 12 | import java.time.LocalDate; 13 | import java.util.ArrayList; 14 | import java.util.List; 15 | 16 | public class CustomDateCellRenderer extends DefaultDateCellRenderer { 17 | 18 | private List list; 19 | 20 | public CustomDateCellRenderer() { 21 | // init list 22 | list = new ArrayList<>(); 23 | list.add(new DateStatus(LocalDate.of(2025, 5, 12), LabelDate.MOSTLY_BOOKED)); 24 | list.add(new DateStatus(LocalDate.of(2025, 5, 13), LabelDate.FREE)); 25 | list.add(new DateStatus(LocalDate.of(2025, 5, 16), LabelDate.FULLY_BOOKED)); 26 | list.add(new DateStatus(LocalDate.of(2025, 5, 20), LabelDate.SOME_SLOTS)); 27 | list.add(new DateStatus(LocalDate.of(2025, 5, 23), LabelDate.MOSTLY_BOOKED)); 28 | list.add(new DateStatus(LocalDate.of(2025, 5, 28), LabelDate.MOSTLY_FREE)); 29 | 30 | list.add(new DateStatus(LocalDate.of(2025, 6, 26), LabelDate.SOME_SLOTS)); 31 | } 32 | 33 | @Override 34 | public void paint(Graphics2D g2, DatePicker datePicker, ButtonDate component, SingleDate date, float width, float height) { 35 | super.paint(g2, datePicker, component, date, width, height); 36 | LabelDate labelDate = getLabelDate(date.toLocalDate()); 37 | if (labelDate != null) { 38 | g2.setColor(labelDate.getColor()); 39 | 40 | // use rendering hint to control draw strokes line 41 | g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE); 42 | g2.setStroke(new BasicStroke(UIScale.scale(getSelectedFocusWidth()), BasicStroke.CAP_ROUND, BasicStroke.JOIN_MITER)); 43 | Rectangle2D.Float rec = getRectangle(width, height); 44 | g2.draw(new Arc2D.Float(rec, 90, 360 * labelDate.getValue(), Arc2D.OPEN)); 45 | } 46 | } 47 | 48 | private LabelDate getLabelDate(LocalDate date) { 49 | for (DateStatus d : list) { 50 | if (d.getDate().equals(date)) { 51 | return d.getLabel(); 52 | } 53 | } 54 | return null; 55 | } 56 | 57 | private class DateStatus { 58 | 59 | public LocalDate getDate() { 60 | return date; 61 | } 62 | 63 | public void setDate(LocalDate date) { 64 | this.date = date; 65 | } 66 | 67 | public LabelDate getLabel() { 68 | return label; 69 | } 70 | 71 | public void setLabel(LabelDate label) { 72 | this.label = label; 73 | } 74 | 75 | public DateStatus(LocalDate date, LabelDate label) { 76 | this.date = date; 77 | this.label = label; 78 | } 79 | 80 | private LocalDate date; 81 | private LabelDate label; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/test/java/test/renderer/LabelDate.java: -------------------------------------------------------------------------------- 1 | package test.renderer; 2 | 3 | import java.awt.*; 4 | 5 | public enum LabelDate { 6 | 7 | FREE("Free", new Color(59, 155, 60), 1f), 8 | MOSTLY_BOOKED("Mostly Booked", new Color(239, 138, 138), 0.75f), 9 | SOME_SLOTS("Some Slots", new Color(213, 189, 44), 0.5f), 10 | FULLY_BOOKED("Fully Booked", new Color(231, 41, 41), 1f), 11 | MOSTLY_FREE("Mostly Free", new Color(140, 225, 141), 0.75f); 12 | 13 | private final String name; 14 | private final Color color; 15 | private final float value; 16 | 17 | LabelDate(String name, Color color, float value) { 18 | this.name = name; 19 | this.color = color; 20 | this.value = value; 21 | } 22 | 23 | public String getName() { 24 | return name; 25 | } 26 | 27 | public Color getColor() { 28 | return color; 29 | } 30 | 31 | public float getValue() { 32 | return value; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/test/java/test/renderer/TestDateCellRenderer.java: -------------------------------------------------------------------------------- 1 | package test.renderer; 2 | 3 | import com.formdev.flatlaf.themes.FlatMacDarkLaf; 4 | import net.miginfocom.swing.MigLayout; 5 | import raven.datetime.DatePicker; 6 | import test.TestFrame; 7 | 8 | import java.awt.*; 9 | import java.time.LocalDate; 10 | import java.time.format.DateTimeFormatter; 11 | 12 | public class TestDateCellRenderer extends TestFrame { 13 | 14 | public TestDateCellRenderer() { 15 | setLayout(new MigLayout("")); 16 | DatePicker datePicker = new DatePicker(); 17 | datePicker.setDateSelectionMode(DatePicker.DateSelectionMode.BETWEEN_DATE_SELECTED); 18 | datePicker.addDateSelectionListener(dateEvent -> { 19 | DateTimeFormatter df = DateTimeFormatter.ofPattern("dd-MM-yyyy"); 20 | if (datePicker.getDateSelectionMode() == DatePicker.DateSelectionMode.SINGLE_DATE_SELECTED) { 21 | LocalDate date = datePicker.getSelectedDate(); 22 | if (date != null) { 23 | System.out.println("date change " + df.format(datePicker.getSelectedDate())); 24 | } else { 25 | System.out.println("date change to null"); 26 | } 27 | } else { 28 | LocalDate dates[] = datePicker.getSelectedDateRange(); 29 | if (dates != null) { 30 | System.out.println("date change " + df.format(dates[0]) + " to " + df.format(dates[1])); 31 | } else { 32 | System.out.println("date change to null"); 33 | } 34 | } 35 | }); 36 | 37 | datePicker.setDefaultDateCellRenderer(new CustomDateCellRenderer()); 38 | datePicker.setSelectedDate(LocalDate.of(2025, 5, 10)); 39 | // date picker minimum size 300px 40 | add(datePicker, "width 300::,height 330::"); 41 | } 42 | 43 | 44 | public static void main(String[] args) { 45 | // System.setProperty("flatlaf.uiScale", "150%"); 46 | FlatMacDarkLaf.setup(); 47 | EventQueue.invokeLater(() -> new TestDateCellRenderer().setVisible(true)); 48 | } 49 | } 50 | --------------------------------------------------------------------------------