From 274fc98839b19c891d478455a7c2f8aa9ce7091e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Tinkl?= Date: Mon, 5 Jun 2023 10:46:39 +0200 Subject: [PATCH] fix(StatusDateRangePicker): use StatusDatePicker for the range selection - exchange the direct input with a calendar popup - extend the validation of the range (so that from < to === true, etc) - remove direct input components (Status[Base]DateInput.qml) Fixes #10900 --- storybook/pages/StatusDateRangePickerPage.qml | 15 +- .../StatusQ/Components/StatusDateInput.qml | 313 ------------------ .../StatusQ/Components/StatusDatePicker.qml | 7 +- .../private/dateInput/StatusBaseDateInput.qml | 118 ------- ui/StatusQ/src/StatusQ/Components/qmldir | 1 - ui/StatusQ/src/statusq.qrc | 2 - .../Wallet/controls/StatusDateRangePicker.qml | 150 ++++----- 7 files changed, 85 insertions(+), 521 deletions(-) delete mode 100644 ui/StatusQ/src/StatusQ/Components/StatusDateInput.qml delete mode 100644 ui/StatusQ/src/StatusQ/Components/private/dateInput/StatusBaseDateInput.qml diff --git a/storybook/pages/StatusDateRangePickerPage.qml b/storybook/pages/StatusDateRangePickerPage.qml index 8806abc978..06ee433b5b 100644 --- a/storybook/pages/StatusDateRangePickerPage.qml +++ b/storybook/pages/StatusDateRangePickerPage.qml @@ -16,23 +16,17 @@ SplitView { SplitView.fillWidth: true SplitView.fillHeight: true - StatusButton { - anchors.top: parent.top - anchors.topMargin: 100 - anchors.horizontalCenter: parent.horizontalCenter - text: "Launch popoup" + anchors.centerIn: parent + text: "Launch popup" onClicked: dialog.open() } StatusDateRangePicker { id: dialog anchors.centerIn: parent - width: 440 - height: 300 - fromTimestamp: new Date().setDate(new Date().getDate() - 7) - toTimestamp: Date.now() - supportedStartYear: 1900 + destroyOnClose: false + fromTimestamp: new Date().setDate(new Date().getDate() - 7) // 7 days ago onNewRangeSet: { console.warn(" from timeStamp = ", new Date(fromTimestamp).toISOString()) console.warn(" to timeStamp = ", new Date(toTimestamp).toISOString()) @@ -40,6 +34,5 @@ SplitView { } Component.onCompleted: dialog.open() - } } diff --git a/ui/StatusQ/src/StatusQ/Components/StatusDateInput.qml b/ui/StatusQ/src/StatusQ/Components/StatusDateInput.qml deleted file mode 100644 index f4dcd7c706..0000000000 --- a/ui/StatusQ/src/StatusQ/Components/StatusDateInput.qml +++ /dev/null @@ -1,313 +0,0 @@ -import QtQuick 2.15 -import QtQuick.Layouts 1.12 -import QtQuick.Controls 2.15 - -import StatusQ.Core 0.1 -import StatusQ.Core.Theme 0.1 - -import "./private/dateInput" - -/*! - \qmltype StatusDateInput - \inherits Control - \inqmlmodule StatusQ.Components - \since StatusQ.Components 0.1 - \brief It allows entering a date string in dd/mm/yyyy format - - Example of how to use it: - - \qml - StatusDateInput { - datePlaceholderText: qsTr("dd") - monthPlaceholderText: qsTr("mm") - yearPlaceholderText: qsTr("yyyy") - presetTimestamp: fromTimestamp - } - \endqml - - For a list of components available see StatusQ. -*/ - -Control { - id: root - - /*! - \qmlproperty string StatusDateInput::datePlaceholderText - This property sets the placeholder text for the date input - */ - property string datePlaceholderText - /*! - \qmlproperty string StatusDateInput::monthPlaceholderText - This property sets the placeholder text for the month input - */ - property string monthPlaceholderText - /*! - \qmlproperty string StatusDateInput::yearPlaceholderText - This property sets the placeholder text for the year input - */ - property string yearPlaceholderText - - /*! - \qmlproperty string StatusDateInput::nowText - This property holds the now text to be shown on the widget - */ - property string nowText - /*! - \qmlproperty double StatusDateInput::presetTimestamp - This property holds the timestamp chosen before entering the popup - */ - property double presetTimestamp: Date.now() - /*! - \qmlproperty bool StatusDateInput::isEditMode - This property can turn and off the edit mode for the input - */ - property bool isEditMode: false - /*! - \qmlproperty bool StatusDateInput::showBackground - This property helps turning of the background of the input - */ - property bool showBackground: true - /*! - \qmlproperty var StatusDateInput::newDate - Represents the newly set date in the input - */ - property var newDate - /*! - \qmlproperty string StatusDateInput::errorMessage - This property to assign errorMessage for the date input - */ - property string errorMessage - /*! - \qmlproperty bool StatusDateInput::valid - This property exposes if the input has a valid date - */ - readonly property bool valid: inputLoader1.item.acceptableInput && inputLoader2.item.acceptableInput && inputLoader3.item.acceptableInput - /*! - \qmlproperty bool StatusDateInput::hasChange - This property exposes if the input has been modified by user - */ - readonly property bool hasChange: d.presetDate.valueOf() !== newDate.valueOf() - /*! - \qmlproperty bool StatusDateInput::supportedStartYear - This property helps set the sypported start year for the input - */ - property int supportedStartYear: 0 - - /*! - \qmlmethod - This function resets the input's text - */ - function reset() { - d.presetDate = d.getDateWithoutTime(presetTimestamp) - } - /*! - \qmlmethod - This function sets the active focus to edit date - */ - function forceActiveFocus() { - inputLoader1.item.forceActiveFocus() - inputLoader1.item.cursorPosition = 0 - } - - QtObject { - id: d - readonly property string separator: "/" - readonly property string space: " " - readonly property string dateId: "d" - readonly property string monthId: "m" - readonly property string yearId: "y" - readonly property bool hasActiveFocus: inputLoader1.item.activeFocus || inputLoader2.item.activeFocus || inputLoader3.item.activeFocus - readonly property bool isCurrentTimestamp: getDateWithoutTime(Date.now().valueOf()).valueOf() === newDate.valueOf() - property var presetDate: d.getDateWithoutTime(presetTimestamp) - readonly property bool showError: (!inputLoader1.item.acceptableInput && !!inputLoader1.item.text) || (!inputLoader2.item.acceptableInput && !!inputLoader2.item.text) || (!inputLoader3.item.acceptableInput && !!inputLoader3.item.text) - readonly property var dateTimeFormat: Qt.locale().dateTimeFormat(Locale.ShortFormat).split(space)[0].toLowerCase().split(separator) - - function setNewDate() { - if (!!inputLoader1.item && !!inputLoader2.item && !!inputLoader3.item) { - newDate = new Date(getDateString(yearId), getDateString(monthId), getDateString(dateId)) - } - } - - function getDateWithoutTime(timeStamp) { - let d = new Date(timeStamp) - d.setHours(0, 0, 0, 0) - return d - } - - function clearAll() { - if(!!inputLoader1.item.selectedText) - inputLoader1.item.clear() - if(!!inputLoader2.item.selectedText) - inputLoader2.item.clear() - if(!!inputLoader3.item.selectedText) - inputLoader3.item.clear() - } - - function selectAll() { - inputLoader1.item.selectAll() - inputLoader2.item.selectAll() - inputLoader3.item.selectAll() - } - - - function getComponent(itemPos) { - return d.dateTimeFormat[(itemPos)].startsWith(yearId) ? editYear : d.dateTimeFormat[(itemPos)].startsWith(monthId) ? editMonth: editDate - } - - function getDateString(identifier) { - return dateTimeFormat[0].startsWith(identifier) ? inputLoader1.item.text : dateTimeFormat[1].startsWith(identifier) ? inputLoader2.item.text: inputLoader3.item.text - } - } - - implicitHeight: 44 - implicitWidth: 135 - leftPadding: 12 - rightPadding: 12 - - background: Rectangle { - color: root.showBackground ? Theme.palette.baseColor2: Theme.palette.transparent - radius: 8 - clip: true - border.width: 1 - border.color: { - if (!root.showBackground) { - return Theme.palette.transparent - } - if (d.showError) { - return Theme.palette.dangerColor1 - } - if (d.hasActiveFocus) { - return Theme.palette.primaryColor1 - } - return hoverHandler.hovered ? Theme.palette.primaryColor2 : Theme.palette.transparent - } - HoverHandler { id: hoverHandler } - } - - contentItem: ColumnLayout { - id: mainLayout - spacing: 11 - RowLayout { - spacing: 3 - StatusBaseText { - id: nowInput - Layout.fillWidth: true - verticalAlignment: Text.AlignVCenter - color: Theme.palette.directColor1 - font.pixelSize: 15 - text: nowText - visible: d.isCurrentTimestamp && !isEditMode && !!nowInput.text - } - Loader { - id: inputLoader1 - Layout.preferredWidth: Math.max(item.contentWidth, item.placeholder.contentWidth) - Layout.preferredHeight: root.height - sourceComponent: d.getComponent(0) - onLoaded: { - d.setNewDate() - item.tabNavItem = inputLoader2.item - } - } - StatusBaseText { - font.pixelSize: 15 - color: Theme.palette.baseColor1 - lineHeightMode: Text.FixedHeight - lineHeight: 22 - text: d.separator - visible: !nowInput.visible - } - Loader { - id: inputLoader2 - Layout.preferredWidth: Math.max(item.contentWidth, item.placeholder.contentWidth) - Layout.preferredHeight: root.height - sourceComponent: d.getComponent(1) - onLoaded: { - d.setNewDate() - item.tabNavItem = inputLoader3.item - } - } - StatusBaseText { - font.pixelSize: 15 - color: Theme.palette.baseColor1 - lineHeightMode: Text.FixedHeight - lineHeight: 22 - text: d.separator - visible: !nowInput.visible - } - Loader { - id: inputLoader3 - Layout.preferredWidth: Math.max(item.contentWidth, item.placeholder.contentWidth) - Layout.preferredHeight: root.height - sourceComponent: d.getComponent(2) - onLoaded: { - d.setNewDate() - item.tabNavItem = inputLoader1.item - } - } - } - StatusBaseText { - Layout.maximumWidth: root.width - Layout.rightMargin: -root.rightPadding - Layout.alignment: Qt.AlignRight - font.pixelSize: 12 - color: Theme.palette.dangerColor1 - lineHeightMode: Text.FixedHeight - lineHeight: 16 - elide: Text.ElideRight - text: errorMessage - visible: d.showError - } - } - - Component { - id: editDate - StatusBaseDateInput { - maximumLength: 2 - placeholderText: root.datePlaceholderText - text: ('0' + d.presetDate.getDate()).slice(-2) - onTextChanged: d.setNewDate() - visible: !nowInput.visible - - validator: IntValidator { bottom: 1; top: { - let tempDate = newDate - tempDate.setDate(0) - return tempDate.getDate() } - } - - onTrippleTap: d.selectAll() - onClearEvent: d.clearAll() - } - } - - Component { - id: editMonth - StatusBaseDateInput { - maximumLength: 2 - placeholderText: root.monthPlaceholderText - text: ('0' + d.presetDate.getMonth()).slice(-2) - onTextChanged: d.setNewDate() - visible: !nowInput.visible - - validator: IntValidator { bottom: 1; top: 12 } - - onTrippleTap: d.selectAll() - onClearEvent: d.clearAll() - } - } - - Component { - id: editYear - StatusBaseDateInput { - maximumLength: 4 - placeholderText: root.yearPlaceholderText - text: d.presetDate.getFullYear() - onTextChanged: d.setNewDate() - visible: !nowInput.visible - - validator: IntValidator { bottom: supportedStartYear; top: new Date().getFullYear() } - - onTrippleTap: d.selectAll() - onClearEvent: d.clearAll() - } - } -} diff --git a/ui/StatusQ/src/StatusQ/Components/StatusDatePicker.qml b/ui/StatusQ/src/StatusQ/Components/StatusDatePicker.qml index 800ec72392..d63889af0e 100644 --- a/ui/StatusQ/src/StatusQ/Components/StatusDatePicker.qml +++ b/ui/StatusQ/src/StatusQ/Components/StatusDatePicker.qml @@ -54,6 +54,8 @@ StatusComboBox { */ property string customTodayText: qsTr("Today") + readonly property alias isTodaySelected: d.isTodaySelected + QtObject { id: d property date selectedDate: new Date() @@ -71,12 +73,13 @@ StatusComboBox { } } - implicitHeight: 44 - indicatorIcon: "calendar" control.delegate: null control.displayText: d.isTodaySelected && root.customTodayText ? root.customTodayText : LocaleUtils.formatDate(d.selectedDate, root.dateFormat) + control.implicitHeight: 44 + control.padding: 12 + control.leftPadding: 16 control.popup.horizontalPadding: 16 control.popup.verticalPadding: 16 control.popup.width: 340 diff --git a/ui/StatusQ/src/StatusQ/Components/private/dateInput/StatusBaseDateInput.qml b/ui/StatusQ/src/StatusQ/Components/private/dateInput/StatusBaseDateInput.qml deleted file mode 100644 index 2427a3d89f..0000000000 --- a/ui/StatusQ/src/StatusQ/Components/private/dateInput/StatusBaseDateInput.qml +++ /dev/null @@ -1,118 +0,0 @@ -import QtQuick 2.15 - -import StatusQ.Core 0.1 -import StatusQ.Core.Theme 0.1 -import StatusQ.Components 0.1 - -TextInput { - id: root - - /*! - \qmlproperty var StatusBaseDateInput::placeholderText - This property sets the placeholderText for input - */ - property alias placeholderText: placeholder.text - /*! - \qmlproperty var StatusBaseDateInput::placeholder - This property exposes the placeholder for customisation - */ - property alias placeholder: placeholder - /*! - \qmlproperty var StatusBaseDateInput::tabNavItem - This property sets the tab key navigation item. - */ - property var tabNavItem: null - - /*! - \qmlsignal - This signal when the input is tapped 3 times - */ - signal trippleTap() - /*! - \qmlsignal - This signal is emitted when backspace is hit - */ - signal clearEvent() - - verticalAlignment: TextInput.AlignVCenter - horizontalAlignment: TextInput.AlignHCenter - - selectByMouse: false - activeFocusOnPress: false - persistentSelection: true - - font.pixelSize: 15 - font.family: Theme.palette.baseFont.name - color: Theme.palette.directColor1 - selectedTextColor: color - selectionColor: Theme.palette.primaryColor2 - - KeyNavigation.priority: !!root.tabNavItem ? KeyNavigation.BeforeItem : KeyNavigation.AfterItem - KeyNavigation.tab: root.tabNavItem - Keys.onPressed: { - switch(event.key) { - case Qt.Key_Backspace: - return root.clearEvent() - - case Qt.Key_Space: - return root.tabNavItem.forceActiveFocus() - } - } - - cursorDelegate: StatusCursorDelegate { - cursorVisible: root.cursorVisible - } - - MouseArea { - id: mouseArea - anchors.fill: parent - anchors.leftMargin: -5 - anchors.rightMargin: -5 - drag.target: dragItem - drag.axis: Drag.XAxis - onClicked: { - root.forceActiveFocus() - root.cursorPosition = root.positionAt(mouse.x,mouse.y) - } - onDoubleClicked: root.selectAll() - TapHandler { - acceptedButtons: Qt.AllButtons - onTapped: if (tapCount == 3) { root.trippleTap() } - } - } - - StatusBaseText { - id: placeholder - anchors.centerIn: parent - verticalAlignment: parent.verticalAlignment - horizontalAlignment: parent.horizontalAlignment - font.pixelSize: 15 - text: root.placeholderText - wrapMode: Text.NoWrap - elide: Text.ElideRight - color: Theme.palette.baseColor1 - visible: (root.length === 0) - } - - DropArea { - anchors.fill: parent - onEntered: { - root.forceActiveFocus() - root.cursorPosition = root.positionAt(drag.x,drag.y) - } - drag.onXChanged: { - root.moveCursorSelection(root.positionAt(drag.x,drag.y), root.mouseSelectionMode) - } - } - - Item { - id: dragItem - width: 1 - height: 5 - Drag.active: mouseArea.drag.active - Drag.hotSpot.x: dragItem.width / 2 - Drag.hotSpot.y: dragItem.height / 2 - } -} - - diff --git a/ui/StatusQ/src/StatusQ/Components/qmldir b/ui/StatusQ/src/StatusQ/Components/qmldir index c45e78a599..dccb3cde5d 100644 --- a/ui/StatusQ/src/StatusQ/Components/qmldir +++ b/ui/StatusQ/src/StatusQ/Components/qmldir @@ -63,4 +63,3 @@ StatusSyncDeviceDelegate 0.1 StatusSyncDeviceDelegate.qml StatusOnlineBadge 0.1 StatusOnlineBadge.qml StatusGroupBox 0.1 StatusGroupBox.qml StatusPageIndicator 0.1 StatusPageIndicator.qml -StatusDateInput 0.1 StatusDateInput.qml diff --git a/ui/StatusQ/src/statusq.qrc b/ui/StatusQ/src/statusq.qrc index 5b516af29a..b72968d8f7 100644 --- a/ui/StatusQ/src/statusq.qrc +++ b/ui/StatusQ/src/statusq.qrc @@ -219,7 +219,5 @@ StatusQ/Core/Utils/ModelChangeGuard.qml StatusQ/Core/Utils/StackViewStates.qml StatusQ/Controls/StatusBlockProgressBar.qml - StatusQ/Components/StatusDateInput.qml - StatusQ/Components/private/dateInput/StatusBaseDateInput.qml diff --git a/ui/app/AppLayouts/Wallet/controls/StatusDateRangePicker.qml b/ui/app/AppLayouts/Wallet/controls/StatusDateRangePicker.qml index fee2b2a27f..0eacc2e441 100644 --- a/ui/app/AppLayouts/Wallet/controls/StatusDateRangePicker.qml +++ b/ui/app/AppLayouts/Wallet/controls/StatusDateRangePicker.qml @@ -1,7 +1,6 @@ import QtQuick 2.14 -import QtQuick.Layouts 1.12 +import QtQuick.Layouts 1.15 import QtQml.Models 2.15 -import QtQuick.Controls 2.15 import StatusQ.Core.Theme 0.1 import StatusQ.Core 0.1 @@ -14,94 +13,95 @@ StatusDialog { property double fromTimestamp: Date.now() property double toTimestamp: Date.now() - property int supportedStartYear signal newRangeSet(double fromTimestamp, double toTimestamp) - onOpened: fromInput.forceActiveFocus() - - topPadding: 0 title: qsTr("Filter activity by period") - contentItem: RowLayout { - spacing: 20 + QtObject { + id: d - // From Date - ColumnLayout { - spacing: 8 - StatusBaseText { - height: visible ? contentHeight : 0 - elide: Text.ElideRight - text: qsTr("From") - font.pixelSize: 15 - color: Theme.palette.directColor1 - } - StatusDateInput { - id: fromInput - datePlaceholderText: qsTr("dd") - monthPlaceholderText: qsTr("mm") - yearPlaceholderText: qsTr("yyyy") - presetTimestamp: fromTimestamp - errorMessage: qsTr("Invalid range") - supportedStartYear: root.supportedStartYear - } + function getFromTimestampUTC(date) { + date.setHours(0, 0, 0, 0) + return date.valueOf() } - // To Date - ColumnLayout { - Layout.preferredWidth: toInput.width - spacing: 8 + function getToTimestampUTC(date) { + date.setDate(date.getDate() + 1) // next day... + date.setHours(0, 0, 0, -1) // ... but just 1ms before midnight -> whole day included + return date.valueOf() + } + } + + contentItem: Item { + GridLayout { + width: parent.width + anchors.verticalCenter: parent.verticalCenter + columns: 3 + columnSpacing: 16 + rowSpacing: 8 + + StatusBaseText { + text: qsTr("From") + } + RowLayout { - Layout.preferredWidth: parent.width + Layout.fillWidth: true StatusBaseText { - Layout.alignment: Qt.AlignLeft - height: visible ? contentHeight : 0 - elide: Text.ElideRight text: qsTr("To") - font.pixelSize: 15 - color: Theme.palette.directColor1 } - StatusButton { - Layout.alignment: Qt.AlignRight + Item { Layout.fillWidth: true } + StatusFlatButton { horizontalPadding: 0 verticalPadding: 0 - spacing: 0 - normalColor: Theme.palette.transparent hoverColor: Theme.palette.transparent - font.weight: Font.Normal - text: toInput.isEditMode ? qsTr("Now") : qsTr("Edit") - onClicked: { - if(toInput.isEditMode) - root.toTimestamp = Date.now() - toInput.isEditMode = !toInput.isEditMode - } + text: qsTr("Now") + enabled: !toInput.isTodaySelected + onClicked: toInput.selectedDate = new Date() } } - StatusDateInput { - id: toInput - datePlaceholderText: qsTr("dd") - monthPlaceholderText: qsTr("mm") - yearPlaceholderText: qsTr("yyyy") - presetTimestamp: toTimestamp - nowText: qsTr("Now") - errorMessage: qsTr("Invalid range") - supportedStartYear: root.supportedStartYear - } - } - StatusButton { - Layout.preferredHeight: fromInput.height - Layout.alignment: Qt.AlignVCenter - Layout.topMargin: 28 - text: qsTr("Reset") - enabled: fromInput.hasChange || toInput.hasChange - normalColor: Theme.palette.transparent - borderColor: Theme.palette.baseColor2 - hoverColor: Theme.palette.primaryColor3 - onClicked: { - toInput.isEditMode = false - fromInput.reset() - toInput.reset() + StatusDatePicker { + Layout.alignment: Qt.AlignTop + Layout.row: 1 + Layout.preferredWidth: 168 + readonly property bool hasChange: selectedDate.valueOf() !== root.fromTimestamp + id: fromInput + selectedDate: new Date(fromTimestamp) + customTodayText: qsTr("Now") + validationError: { + if (selectedDate.valueOf() > toInput.selectedDate.valueOf() && !toInput.isTodaySelected) // from > to; today in both is fine + return qsTr("'From' can't be later than 'To'") + + if (selectedDate.valueOf() > new Date()) // from > now + return qsTr("Can't set date to future") + + return "" + } + } + + StatusDatePicker { + Layout.alignment: Qt.AlignTop + Layout.preferredWidth: 168 + readonly property bool hasChange: selectedDate.valueOf() !== root.toTimestamp + id: toInput + selectedDate: new Date(toTimestamp) + customTodayText: qsTr("Now") + validationError: selectedDate.valueOf() > new Date() // to > now + ? qsTr("Can't set date to future") : "" + } + + StatusButton { + Layout.alignment: Qt.AlignTop + Layout.preferredHeight: toInput.control.height + text: qsTr("Reset") + enabled: fromInput.hasChange || toInput.hasChange + normalColor: Theme.palette.transparent + borderColor: Theme.palette.baseColor2 + onClicked: { + fromInput.selectedDate = new Date(root.fromTimestamp) + toInput.selectedDate = new Date(root.toTimestamp) + } } } } @@ -110,9 +110,11 @@ StatusDialog { rightButtons: ObjectModel { StatusButton { text: qsTr("Apply") - enabled: fromInput.valid && toInput.valid && (fromInput.hasChange || toInput.hasChange) + enabled: !fromInput.validationError && !toInput.validationError && (fromInput.hasChange || toInput.hasChange) onClicked: { - root.newRangeSet(fromInput.newDate.valueOf(), toInput.newDate.valueOf()) + root.newRangeSet(d.getFromTimestampUTC(fromInput.selectedDate), + toInput.isTodaySelected ? new Date().valueOf() // now means now, including the time today + : d.getToTimestampUTC(toInput.selectedDate)) root.close() } }