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.fillWidth: true
SplitView.fillHeight: true SplitView.fillHeight: true
StatusButton { StatusButton {
anchors.top: parent.top anchors.centerIn: parent
anchors.topMargin: 100 text: "Launch popup"
anchors.horizontalCenter: parent.horizontalCenter
text: "Launch popoup"
onClicked: dialog.open() onClicked: dialog.open()
} }
StatusDateRangePicker { StatusDateRangePicker {
id: dialog id: dialog
anchors.centerIn: parent anchors.centerIn: parent
width: 440 destroyOnClose: false
height: 300 fromTimestamp: new Date().setDate(new Date().getDate() - 7) // 7 days ago
fromTimestamp: new Date().setDate(new Date().getDate() - 7)
toTimestamp: Date.now()
supportedStartYear: 1900
onNewRangeSet: { onNewRangeSet: {
console.warn(" from timeStamp = ", new Date(fromTimestamp).toISOString()) console.warn(" from timeStamp = ", new Date(fromTimestamp).toISOString())
console.warn(" to timeStamp = ", new Date(toTimestamp).toISOString()) console.warn(" to timeStamp = ", new Date(toTimestamp).toISOString())
@ -40,6 +34,5 @@ SplitView {
} }
Component.onCompleted: dialog.open() 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") property string customTodayText: qsTr("Today")
readonly property alias isTodaySelected: d.isTodaySelected
QtObject { QtObject {
id: d id: d
property date selectedDate: new Date() property date selectedDate: new Date()
@ -71,12 +73,13 @@ StatusComboBox {
} }
} }
implicitHeight: 44
indicatorIcon: "calendar" indicatorIcon: "calendar"
control.delegate: null control.delegate: null
control.displayText: d.isTodaySelected && root.customTodayText ? root.customTodayText control.displayText: d.isTodaySelected && root.customTodayText ? root.customTodayText
: LocaleUtils.formatDate(d.selectedDate, root.dateFormat) : LocaleUtils.formatDate(d.selectedDate, root.dateFormat)
control.implicitHeight: 44
control.padding: 12
control.leftPadding: 16
control.popup.horizontalPadding: 16 control.popup.horizontalPadding: 16
control.popup.verticalPadding: 16 control.popup.verticalPadding: 16
control.popup.width: 340 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 StatusOnlineBadge 0.1 StatusOnlineBadge.qml
StatusGroupBox 0.1 StatusGroupBox.qml StatusGroupBox 0.1 StatusGroupBox.qml
StatusPageIndicator 0.1 StatusPageIndicator.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/ModelChangeGuard.qml</file>
<file>StatusQ/Core/Utils/StackViewStates.qml</file> <file>StatusQ/Core/Utils/StackViewStates.qml</file>
<file>StatusQ/Controls/StatusBlockProgressBar.qml</file> <file>StatusQ/Controls/StatusBlockProgressBar.qml</file>
<file>StatusQ/Components/StatusDateInput.qml</file>
<file>StatusQ/Components/private/dateInput/StatusBaseDateInput.qml</file>
</qresource> </qresource>
</RCC> </RCC>

View File

@ -1,7 +1,6 @@
import QtQuick 2.14 import QtQuick 2.14
import QtQuick.Layouts 1.12 import QtQuick.Layouts 1.15
import QtQml.Models 2.15 import QtQml.Models 2.15
import QtQuick.Controls 2.15
import StatusQ.Core.Theme 0.1 import StatusQ.Core.Theme 0.1
import StatusQ.Core 0.1 import StatusQ.Core 0.1
@ -14,94 +13,95 @@ StatusDialog {
property double fromTimestamp: Date.now() property double fromTimestamp: Date.now()
property double toTimestamp: Date.now() property double toTimestamp: Date.now()
property int supportedStartYear
signal newRangeSet(double fromTimestamp, double toTimestamp) signal newRangeSet(double fromTimestamp, double toTimestamp)
onOpened: fromInput.forceActiveFocus()
topPadding: 0
title: qsTr("Filter activity by period") title: qsTr("Filter activity by period")
contentItem: RowLayout { QtObject {
spacing: 20 id: d
// From Date function getFromTimestampUTC(date) {
ColumnLayout { date.setHours(0, 0, 0, 0)
spacing: 8 return date.valueOf()
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
}
} }
// To Date function getToTimestampUTC(date) {
ColumnLayout { date.setDate(date.getDate() + 1) // next day...
Layout.preferredWidth: toInput.width date.setHours(0, 0, 0, -1) // ... but just 1ms before midnight -> whole day included
spacing: 8 return date.valueOf()
}
}
contentItem: Item {
GridLayout {
width: parent.width
anchors.verticalCenter: parent.verticalCenter
columns: 3
columnSpacing: 16
rowSpacing: 8
StatusBaseText {
text: qsTr("From")
}
RowLayout { RowLayout {
Layout.preferredWidth: parent.width Layout.fillWidth: true
StatusBaseText { StatusBaseText {
Layout.alignment: Qt.AlignLeft
height: visible ? contentHeight : 0
elide: Text.ElideRight
text: qsTr("To") text: qsTr("To")
font.pixelSize: 15
color: Theme.palette.directColor1
} }
StatusButton { Item { Layout.fillWidth: true }
Layout.alignment: Qt.AlignRight StatusFlatButton {
horizontalPadding: 0 horizontalPadding: 0
verticalPadding: 0 verticalPadding: 0
spacing: 0
normalColor: Theme.palette.transparent
hoverColor: Theme.palette.transparent hoverColor: Theme.palette.transparent
font.weight: Font.Normal text: qsTr("Now")
text: toInput.isEditMode ? qsTr("Now") : qsTr("Edit") enabled: !toInput.isTodaySelected
onClicked: { onClicked: toInput.selectedDate = new Date()
if(toInput.isEditMode)
root.toTimestamp = Date.now()
toInput.isEditMode = !toInput.isEditMode
}
} }
} }
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 { StatusDatePicker {
Layout.preferredHeight: fromInput.height Layout.alignment: Qt.AlignTop
Layout.alignment: Qt.AlignVCenter Layout.row: 1
Layout.topMargin: 28 Layout.preferredWidth: 168
text: qsTr("Reset") readonly property bool hasChange: selectedDate.valueOf() !== root.fromTimestamp
enabled: fromInput.hasChange || toInput.hasChange id: fromInput
normalColor: Theme.palette.transparent selectedDate: new Date(fromTimestamp)
borderColor: Theme.palette.baseColor2 customTodayText: qsTr("Now")
hoverColor: Theme.palette.primaryColor3 validationError: {
onClicked: { if (selectedDate.valueOf() > toInput.selectedDate.valueOf() && !toInput.isTodaySelected) // from > to; today in both is fine
toInput.isEditMode = false return qsTr("'From' can't be later than 'To'")
fromInput.reset()
toInput.reset() 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 { rightButtons: ObjectModel {
StatusButton { StatusButton {
text: qsTr("Apply") text: qsTr("Apply")
enabled: fromInput.valid && toInput.valid && (fromInput.hasChange || toInput.hasChange) enabled: !fromInput.validationError && !toInput.validationError && (fromInput.hasChange || toInput.hasChange)
onClicked: { 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() root.close()
} }
} }