-
Notifications
You must be signed in to change notification settings - Fork 57
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add TimeRangePicker control and demo application
Introduce the TimeRangePicker custom control for selecting time ranges, supporting both single and multiple selection modes. Additionally, include a demo application showcasing the usage of the new control with various example time ranges and interaction features.
- Loading branch information
Showing
2 changed files
with
194 additions
and
0 deletions.
There are no files selected for viewing
52 changes: 52 additions & 0 deletions
52
gemsfx-demo/src/main/java/com/dlsc/gemsfx/demo/HelloTimeRangePicker.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
package com.dlsc.gemsfx.demo; | ||
|
||
import com.dlsc.gemsfx.TimeRangePicker; | ||
import com.dlsc.gemsfx.demo.fake.SimpleControlPane; | ||
import javafx.application.Application; | ||
import javafx.scene.Node; | ||
import javafx.scene.Scene; | ||
import javafx.scene.control.ComboBox; | ||
import javafx.scene.control.SelectionMode; | ||
import javafx.scene.control.SplitPane; | ||
import javafx.scene.layout.StackPane; | ||
import javafx.stage.Stage; | ||
|
||
public class HelloTimeRangePicker extends Application { | ||
|
||
private TimeRangePicker timeRangePicker; | ||
|
||
@Override | ||
public void start(Stage primaryStage) throws Exception { | ||
timeRangePicker = new TimeRangePicker(); | ||
timeRangePicker.setPrefWidth(200); | ||
timeRangePicker.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); | ||
|
||
timeRangePicker.getSelectionModel().selectIndices(1, 2, 3); | ||
|
||
StackPane wrapper = new StackPane(timeRangePicker); | ||
wrapper.setStyle("-fx-background-color: white; -fx-padding: 10;"); | ||
|
||
SplitPane splitPane = new SplitPane(wrapper, createControlPane()); | ||
splitPane.setDividerPositions(0.7); | ||
|
||
primaryStage.setScene(new Scene(splitPane, 800, 600)); | ||
primaryStage.setTitle("Hello TimeRangePicker"); | ||
primaryStage.show(); | ||
} | ||
|
||
private Node createControlPane() { | ||
ComboBox<SelectionMode> controlPane = new ComboBox<>(); | ||
controlPane.getItems().addAll(SelectionMode.values()); | ||
controlPane.valueProperty().bindBidirectional(timeRangePicker.getSelectionModel().selectionModeProperty()); | ||
|
||
return new SimpleControlPane( | ||
"Time Range Picker", | ||
new SimpleControlPane.ControlItem("Selection Mode", controlPane) | ||
); | ||
} | ||
|
||
|
||
public static void main(String[] args) { | ||
launch(args); | ||
} | ||
} |
142 changes: 142 additions & 0 deletions
142
gemsfx/src/main/java/com/dlsc/gemsfx/TimeRangePicker.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
package com.dlsc.gemsfx; | ||
|
||
import com.dlsc.gemsfx.util.SimpleStringConverter; | ||
import javafx.scene.control.Button; | ||
import javafx.util.StringConverter; | ||
|
||
import java.time.LocalTime; | ||
import java.util.ArrayList; | ||
import java.util.Comparator; | ||
import java.util.List; | ||
import java.util.Objects; | ||
|
||
/** | ||
* A custom control that allows users to select time ranges. | ||
* It provides support for two selection modes: single range and multiple ranges. | ||
* <ul> | ||
* <li>SINGLE mode allows selection of only one time range at a time.</li> | ||
* <li>MULTIPLE mode allows selection of multiple time ranges.</li> | ||
* </ul> | ||
* | ||
* @see SelectionBox | ||
*/ | ||
public class TimeRangePicker extends SelectionBox<TimeRangePicker.TimeRange> { | ||
|
||
private static final String DEFAULT_STYLE_CLASS = "time-range-picker"; | ||
|
||
public TimeRangePicker() { | ||
// add some default time ranges | ||
this(new TimeRange(LocalTime.of(10, 0), LocalTime.of(12, 0)), | ||
new TimeRange(LocalTime.of(12, 0), LocalTime.of(14, 0)), | ||
new TimeRange(LocalTime.of(14, 0), LocalTime.of(16, 0)), | ||
new TimeRange(LocalTime.of(16, 0), LocalTime.of(18, 0))); | ||
} | ||
|
||
public TimeRangePicker(TimeRange... ranges) { | ||
getStyleClass().add(DEFAULT_STYLE_CLASS); | ||
|
||
// Set time ranges | ||
getItems().setAll(ranges); | ||
|
||
// set extra buttons | ||
setExtraButtonsProvider(model -> switch (model.getSelectionMode()) { | ||
case SINGLE -> { | ||
Button clearButton = createExtraButton("Clear", getSelectionModel()::clearSelection); | ||
yield List.of(clearButton); | ||
} | ||
case MULTIPLE -> { | ||
Button clearButton = createExtraButton("Clear", model::clearSelection); | ||
Button selectAllButton = createExtraButton("Select All", model::selectAll); | ||
yield List.of(clearButton, selectAllButton); | ||
} | ||
}); | ||
|
||
// set result converter | ||
setSelectedItemsConverter(new SimpleStringConverter<>(selectedRanges -> { | ||
int selectedCount = selectedRanges.size(); | ||
|
||
if (selectedCount == 0) { | ||
return "No Data"; | ||
} else if (selectedCount == 1) { | ||
return convertRangeToText(selectedRanges.get(0)); | ||
} else { | ||
// Merge consecutive ranges | ||
List<TimeRange> mergedRanges = mergeConsecutiveItem(selectedRanges); | ||
|
||
// Build the display text | ||
List<String> rangeStrings = new ArrayList<>(); | ||
for (TimeRange range : mergedRanges) { | ||
rangeStrings.add(convertRangeToText(range)); | ||
} | ||
return String.join(", ", rangeStrings); | ||
} | ||
})); | ||
} | ||
|
||
private List<TimeRange> mergeConsecutiveItem(List<TimeRange> ranges) { | ||
List<TimeRange> mergedRanges = new ArrayList<>(); | ||
|
||
if (ranges.isEmpty()) { | ||
return mergedRanges; | ||
} | ||
|
||
// Sort ranges by start time | ||
ranges.sort(Comparator.comparing(TimeRange::startTime)); | ||
|
||
TimeRange currentRange = ranges.get(0); | ||
|
||
for (int i = 1; i < ranges.size(); i++) { | ||
TimeRange nextRange = ranges.get(i); | ||
|
||
if (currentRange.endTime().equals(nextRange.startTime())) { | ||
// Ranges are consecutive, merge them | ||
currentRange = new TimeRange(currentRange.startTime(), nextRange.endTime()); | ||
} else { | ||
// Ranges are not consecutive, add the current range and move to next | ||
mergedRanges.add(currentRange); | ||
currentRange = nextRange; | ||
} | ||
} | ||
|
||
// Add the last range | ||
mergedRanges.add(currentRange); | ||
|
||
return mergedRanges; | ||
} | ||
|
||
private String convertRangeToText(TimeRange range) { | ||
if (range == null) { | ||
return ""; | ||
} | ||
StringConverter<TimeRange> itemConverter = getItemConverter(); | ||
if (itemConverter != null) { | ||
return itemConverter.toString(range); | ||
} | ||
return range.toString(); | ||
} | ||
|
||
/** | ||
* Represents a time range with a start time and an end time. | ||
* Ensures that the start time is not after the end time. | ||
*/ | ||
public record TimeRange(LocalTime startTime, LocalTime endTime) { | ||
|
||
public TimeRange { | ||
// Ensure startTime and endTime are not null | ||
Objects.requireNonNull(startTime, "startTime cannot be null"); | ||
Objects.requireNonNull(endTime, "endTime cannot be null"); | ||
|
||
// If startTime is after endTime, swap them | ||
if (startTime.isAfter(endTime)) { | ||
LocalTime temp = startTime; | ||
startTime = endTime; | ||
endTime = temp; | ||
} | ||
} | ||
|
||
@Override | ||
public String toString() { | ||
return startTime + " ~ " + endTime; | ||
} | ||
} | ||
} |