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
This commit is contained in:
Lukáš Tinkl 2023-06-05 10:46:39 +02:00 committed by Lukáš Tinkl
parent a0146014d1
commit 274fc98839
7 changed files with 85 additions and 521 deletions

View File

@ -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()
}
}

View File

@ -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()
}
}
}

View File

@ -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

View File

@ -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
}
}

View File

@ -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

View File

@ -219,7 +219,5 @@
<file>StatusQ/Core/Utils/ModelChangeGuard.qml</file>
<file>StatusQ/Core/Utils/StackViewStates.qml</file>
<file>StatusQ/Controls/StatusBlockProgressBar.qml</file>
<file>StatusQ/Components/StatusDateInput.qml</file>
<file>StatusQ/Components/private/dateInput/StatusBaseDateInput.qml</file>
</qresource>
</RCC>

View File

@ -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()
}
}