From ded8c670a09873bbcaea1188bec98af64667fd12 Mon Sep 17 00:00:00 2001 From: Florian Blasius Date: Mon, 13 May 2024 15:07:31 +0200 Subject: [PATCH] TimePicker: cleanup, tests and refactoring --- CHANGELOG.md | 1 + .../src/language/widgets/time-picker.md | 44 ++- examples/gallery/ui/pages/controls_page.slint | 33 +- internal/compiler/builtins.slint | 5 + .../widgets/common/time-picker-base.slint | 226 +++++++---- .../compiler/widgets/cosmic-base/_clock.svg | 12 + .../widgets/cosmic-base/_keyboard.svg | 11 + .../cosmic-base/std-widgets-base.slint | 3 + .../widgets/cosmic-base/styling.slint | 2 + .../widgets/cosmic-base/time-picker.slint | 118 +++++- .../widgets/cupertino-base/_clock.svg | 3 + .../widgets/cupertino-base/_keyboard.svg | 1 + .../cupertino-base/std-widgets-base.slint | 3 + .../widgets/cupertino-base/styling.slint | 2 + .../widgets/cupertino-base/time-picker.slint | 109 ++++++ .../compiler/widgets/fluent-base/_clock.svg | 3 + .../widgets/fluent-base/_keyboard.svg | 1 + .../fluent-base/std-widgets-base.slint | 3 + .../widgets/fluent-base/styling.slint | 2 + .../widgets/fluent-base/time-picker.slint | 109 ++++++ .../compiler/widgets/material-base/_clock.svg | 3 + .../material-base/std-widgets-base.slint | 5 +- .../widgets/material-base/styling.slint | 1 + .../widgets/material-base/time-picker.slint | 355 +++--------------- .../compiler/widgets/qt/std-widgets.slint | 3 + .../compiler/widgets/qt/time-picker.slint | 108 ++++++ tests/cases/widgets/timepicker.slint | 52 +++ 27 files changed, 808 insertions(+), 410 deletions(-) create mode 100644 internal/compiler/widgets/cosmic-base/_clock.svg create mode 100644 internal/compiler/widgets/cosmic-base/_keyboard.svg create mode 100644 internal/compiler/widgets/cupertino-base/_clock.svg create mode 100644 internal/compiler/widgets/cupertino-base/_keyboard.svg create mode 100644 internal/compiler/widgets/fluent-base/_clock.svg create mode 100644 internal/compiler/widgets/fluent-base/_keyboard.svg create mode 100644 internal/compiler/widgets/material-base/_clock.svg create mode 100644 internal/compiler/widgets/qt/time-picker.slint create mode 100644 tests/cases/widgets/timepicker.slint diff --git a/CHANGELOG.md b/CHANGELOG.md index 01715fe097b..cb30870a4cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ All notable changes to this project are documented in this file. - Fixed updating model of ComboBox does not change current-value - Fixed set current-index of ComboBox to -1 does not reset current-value - Fixed issue where the text of `SpinBox` is not updated after value is changed from outside + - Added `TimePicker` widget. ## [1.6.0] - 2024-05-13 diff --git a/docs/reference/src/language/widgets/time-picker.md b/docs/reference/src/language/widgets/time-picker.md index 6691f47910d..c2b8207c4db 100644 --- a/docs/reference/src/language/widgets/time-picker.md +++ b/docs/reference/src/language/widgets/time-picker.md @@ -15,11 +15,11 @@ A timer picker that is usd for selecting the time, in either 24-hour or AM/PM mo ### Properties +- **`twenty-four-hour`**: (_in_ _bool_): Sets to true to enable 24 hour selection otherwise it is displayed in AM/PM mode. - **`title`** (_in_ _string_): The text that is displayed at the top of the picker. -- **`ok-label`** (_in_ _string_): The text written in the ok button. - **`cancel-label`** (_in_ _string_): The text written in the cancel button. -- **`is-twenty-four-hour`**: (_in_ _bool_): Sets to true to enable 24 hour selection otherwise it is displayed in AM/PM mode. -- **`curren-time`**: (_in-out_ _Time_): Gets and sets the current selected time.. +- **`ok-label`** (_in_ _string_): The text written in the ok button. +- **`time`**: (_in_ _Time_): Set the initinal displayed time. ### Callbacks @@ -29,22 +29,34 @@ A timer picker that is usd for selecting the time, in either 24-hour or AM/PM mo ### Example ```slint -import { TimePicker } from "std-widgets.slint"; +import { TimePicker, Button } from "std-widgets.slint"; export component Example inherits Window { - width: 200px; - height: 130px; - - time-picker-popup := Popup { - TimePicker { - + width: 600px; + height: 600px; + + time-picker-button := Button { + text: @tr("Open TimePicker"); + + clicked => { + time-picker.show(); + } } - } - Button { - text: "Open time picker"; - clicked => { - time-picker-popup.show(); + time-picker := PopupWindow { + width: 340px; + height: 500px; + close-on-click: false; + + TimePicker { + canceled => { + time-picker.close(); + } + + accepted(time) => { + debug(time); + time-picker.close(); + } + } } - } } ``` diff --git a/examples/gallery/ui/pages/controls_page.slint b/examples/gallery/ui/pages/controls_page.slint index de84ec5b0a3..18ea988f483 100644 --- a/examples/gallery/ui/pages/controls_page.slint +++ b/examples/gallery/ui/pages/controls_page.slint @@ -17,8 +17,6 @@ export component ControlsPage inherits Page { VerticalLayout { padding: 0px; - TimePicker {} - HorizontalBox { alignment: start; @@ -89,7 +87,7 @@ export component ControlsPage inherits Page { } GroupBox { - title: @tr("LineEdit - SpinBox"); + title: @tr("LineEdit - SpinBox - TimePicker"); vertical-stretch: 0; HorizontalBox { @@ -106,6 +104,14 @@ export component ControlsPage inherits Page { value: 42; enabled: GallerySettings.widgets-enabled; } + + time-picker-button := Button { + text: @tr("Open TimePicker"); + + clicked => { + time-picker.show(); + } + } } } @@ -141,6 +147,7 @@ export component ControlsPage inherits Page { row: 0; col: 1; rowspan: 2; + Spinner { progress: i-progress-indicator.progress; indeterminate: i-progress-indicator.indeterminate; @@ -213,4 +220,24 @@ export component ControlsPage inherits Page { } } } + + time-picker := PopupWindow { + x: (root.width - 340px) / 2; + y: (root.height - 500px) / 2; + width: 340px; + height: 500px; + close-on-click: false; + + TimePicker { + twenty-four-hour: true; + canceled => { + time-picker.close(); + } + + accepted(time) => { + debug(time); + time-picker.close(); + } + } + } } diff --git a/internal/compiler/builtins.slint b/internal/compiler/builtins.slint index dd7d98d7aed..c6380675ed9 100644 --- a/internal/compiler/builtins.slint +++ b/internal/compiler/builtins.slint @@ -546,6 +546,11 @@ export component NativeTab { //-is_internal } +export struct NativeTime { + hour: int, + minute: int +} + export global NativeStyleMetrics { out property layout-spacing; out property layout-padding; diff --git a/internal/compiler/widgets/common/time-picker-base.slint b/internal/compiler/widgets/common/time-picker-base.slint index 7e010cbd239..fb2d172a483 100644 --- a/internal/compiler/widgets/common/time-picker-base.slint +++ b/internal/compiler/widgets/common/time-picker-base.slint @@ -46,8 +46,9 @@ export component Clock { in property <[int]> model; in property two-columns; in property style; + in property total; in-out property current-item; - out property current-value: root.model[root.current-item]; + in property current-value; callback curren-item-changed(/* index */ int); @@ -58,8 +59,10 @@ export component Clock { property inner-padding: 32px; property radius-outer: root.center - root.outer-padding; property radius-inner: root.center - root.inner-padding; - property half-model-length: root.model.length / 2; + property half-total: root.total / 2; property rotation: 0.25turn; + property current-x: get-index-x(root.current-value); + property current-y: get-index-y(root.current-value); min-width: 256px; min-height: 256px; @@ -83,8 +86,8 @@ export component Clock { } LineTo { - x: root.radius / 1px + ((root.radius - (root.two-columns && root.current-item >= root.half-model-length ? root.inner-padding : root.outer-padding)) / 1px * cos(root.index-to-angle(root.current-item))); - y: root.radius / 1px + ((root.radius - (root.two-columns && root.current-item >= root.half-model-length ? root.inner-padding : root.outer-padding)) / 1px * sin(root.index-to-angle(root.current-item))); + x: (root.current-x + root.picker-ditameter / 2) / 1px; + y: (root.current-y + root.picker-ditameter / 2) / 1px; } } @@ -95,58 +98,76 @@ export component Clock { border-radius: 4px; } - if root.current-item >= 0 || root.current-item < root.model.length: Rectangle { - x: get-index-x(root.current-item); - y: get-index-y(root.current-item); + if root.current-item < root.model.length: Rectangle { + x: root.current-x; + y: root.current-y; width: root.picker-ditameter; height: root.picker-ditameter; border-radius: root.picker-ditameter / 2; background: root.style.foreground; + + if root.current-item < 0: Rectangle { + width: 4px; + height: 4px; + border-radius: 2px; + background: root.style.time-selector-style.foreground; + } } for val[index] in root.model: TimeSelector { - x: root.get-index-x(index); - y: root.get-index-y(index); + x: get-index-x(val); + y: get-index-y(val); width: root.picker-ditameter; height: root.picker-ditameter; value: val; selected: index == root.current-item; style: root.style.time-selector-style; + accessible-role: button; + accessible-label: @tr("{} Hours or minutes of {}", val, root.total); + accessible-action-default => { + self.clicked(); + } clicked => { root.set-current-item(index); } } - pure function index-to-angle(index: int) -> angle { + pure function value-to-angle(value: int) -> angle { if root.two-columns { - if index >= root.half-model-length { - return clamp((index - root.half-model-length) / root.half-model-length * 1turn, 0, 0.999999turn) - root.rotation; + if value >= root.half-total { + return clamp((value - root.half-total) / root.half-total * 1turn, 0, 0.999999turn) - root.rotation; } - return clamp(index / root.half-model-length * 1turn, 0, 0.99999turn) - root.rotation; + return clamp(value / root.half-total * 1turn, 0, 0.99999turn) - root.rotation; } - clamp(index / root.model.length * 1turn, 0, 0.99999turn) - root.rotation; + clamp(value / root.total * 1turn, 0, 0.99999turn) - root.rotation; } - pure function get-index-x(index: int) -> length { - if root.two-columns && index >= root.half-model-length { - return root.center + (root.radius-inner / 1px * cos(root.index-to-angle(index))) * 1px; + pure function get-index-x(value: int) -> length { + if root.two-columns && value >= root.half-total { + return root.center + (root.radius-inner / 1px * cos(root.value-to-angle(value))) * 1px; } - root.center + (root.radius-outer / 1px * cos(root.index-to-angle(index))) * 1px + root.center + (root.radius-outer / 1px * cos(root.value-to-angle(value))) * 1px } - pure function get-index-y(index: int) -> length { - if root.two-columns && index >= root.half-model-length { - return root.center + (root.radius-inner / 1px * sin(root.index-to-angle(index))) * 1px; + pure function get-index-y(value: int) -> length { + // this is only for 24 mode + if root.total == 24 && value == 0 { + return root.center + (root.radius-inner / 1px * sin(root.value-to-angle(value))) * 1px; } - root.center + (root.radius-outer / 1px * sin(root.index-to-angle(index))) * 1px + if root.total == 24 && value == 12 { + return root.center + (root.radius-outer / 1px * sin(root.value-to-angle(value))) * 1px; + } + if root.two-columns && value >= root.half-total { + return root.center + (root.radius-inner / 1px * sin(root.value-to-angle(value))) * 1px; + } + root.center + (root.radius-outer / 1px * sin(root.value-to-angle(value))) * 1px } function set-current-item(index: int) { if root.current-item == index { return; } - root.current-item = index; root.curren-item-changed(index); } } @@ -163,16 +184,17 @@ export component TimePickerInput { in property style; in property read-only <=> text-input.read-only; in property checked; - in-out property value; + in-out property text <=> text-input.text; callback clicked; - callback accepted(int); + callback edited(int); min-width: max(96px, text-input.min-width); min-height: max(80px, text-input.min-height); vertical-stretch: 0; horizontal-stretch: 0; + forward-focus: text-input; background-layer := Rectangle { border-radius: root.style.border-radius; background: root.style.background; @@ -181,19 +203,14 @@ export component TimePickerInput { text-input := TextInput { vertical-alignment: center; horizontal-alignment: center; - text: root.value; width: 100%; height: 100%; color: root.style.foreground; font-size: root.style.font-size; font-weight: root.style.font-weight; input-type: number; - - accepted => { - if !self.text.is-float() { - return; - } - root.accepted(self.text.to-float()); + edited => { + root.edited(self.text.to-float()); } } @@ -203,11 +220,6 @@ export component TimePickerInput { } } - function update-value(value: int) { - root.value = value; - text-input.text = root.value; - } - states [ checked when root.checked: { background-layer.background: root.style.background-selected; @@ -243,14 +255,16 @@ export component PeriodSelectorItem { states [ checked when root.checked: { background-layer.background: root.style.background-selected; - label.color: root.style.foreground; + label.color: root.style.foreground-selected; } ] } export struct PeriodSelectorStyle { item-style: PeriodSelectorItemStyle, - border-brush: brush} + border-brush: brush, + border-radius: length, + border-width: length} export component PeriodSelector { in property style; @@ -258,13 +272,16 @@ export component PeriodSelector { callback update-period(bool); - min-height: 38px; + min-width: max(38px, layout.min-width); + accessible-label: "AM or PM"; + accessible-role: checkbox; + accessible-checked: root.am-selected; Rectangle { border-radius: border.border-radius; clip: true; - VerticalLayout { + layout := VerticalLayout { PeriodSelectorItem { text: "AM"; checked: root.am-selected; @@ -300,8 +317,8 @@ export component PeriodSelector { } border := Rectangle { - border-radius: 8px; - border-width: 1px; + border-radius: root.style.border-radius; + border-width: root.style.border-width; border-color: root.style.border-brush; } } @@ -312,7 +329,8 @@ export struct Time { export struct TimePickerStyle { foreground: brush, - spacing: length, + horizontal-spacing: length, + vertical-spacing: length, clock-style: ClockStyle, input-style: TimePickerInputStyle, period-selector-style: PeriodSelectorStyle, @@ -322,10 +340,10 @@ export component TimePickerBase { in property twenty-four-hour; in property selection-mode: true; in property style; - in-out property