mirror of
https://github.com/subsurface/subsurface.git
synced 2025-02-09 07:26:16 +00:00
68414531ad
QML's ListView uses the "section" property to test if items belong to the same section. Apparently, this must be a string and therefore we can't pass e.g. a dive-trip object. Therefore a specially formatted string was passed in, which was guaranteed to be unique (contained the dive-trip pointer value) and the fully formatted trip-title and short-date. The disadvantage of that approach is that the formatting is performed for every dive and not every trip. Perhaps not a problem now, but it makes it for example necessary to cache the number of filtered dives. To be more flexible, pass in only the pointer value formatted as hexadecimal string and provide a function to convert that string back to a trip-pointer (in the form of a QVariant, so that it can be passed to QML). Moreover provide two functions for formatting the title and the short-date. The three new functions are members of DiveListSortModel. This might not be the perfect place, but it is easy to reach from the DiveListView. Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
609 lines
17 KiB
QML
609 lines
17 KiB
QML
// SPDX-License-Identifier: GPL-2.0
|
|
import QtQuick 2.6
|
|
import QtQuick.Controls 2.2 as Controls
|
|
import QtQuick.Layouts 1.2
|
|
import QtQuick.Window 2.2
|
|
import QtQuick.Dialogs 1.2
|
|
import org.kde.kirigami 2.5 as Kirigami
|
|
import org.subsurfacedivelog.mobile 1.0
|
|
|
|
Kirigami.ScrollablePage {
|
|
id: page
|
|
objectName: "DiveList"
|
|
title: qsTr("Dive list")
|
|
verticalScrollBarPolicy: Qt.ScrollBarAlwaysOff
|
|
width: subsurfaceTheme.columnWidth
|
|
property int credentialStatus: prefs.credentialStatus
|
|
property int numDives: diveListView.count
|
|
property color textColor: subsurfaceTheme.textColor
|
|
property color secondaryTextColor: subsurfaceTheme.secondaryTextColor
|
|
property int horizontalPadding: Kirigami.Units.gridUnit / 2 - Kirigami.Units.smallSpacing + 1
|
|
property string activeTrip
|
|
property QtObject diveListModel: diveModel
|
|
property string numShownText
|
|
|
|
supportsRefreshing: true
|
|
onRefreshingChanged: {
|
|
if (refreshing) {
|
|
if (prefs.credentialStatus === CloudStatus.CS_VERIFIED) {
|
|
detailsWindow.endEditMode()
|
|
manager.saveChangesCloud(true)
|
|
refreshing = false
|
|
} else {
|
|
console.log("sync with cloud storage requested, but credentialStatus is " + prefs.credentialStatus)
|
|
console.log("no syncing, turn off spinner")
|
|
refreshing = false
|
|
}
|
|
}
|
|
}
|
|
|
|
Component {
|
|
id: diveDelegate
|
|
Kirigami.AbstractListItem {
|
|
// this looks weird, but it's how we can tell that this dive isn't in a trip
|
|
property bool diveOutsideTrip: dive.tripNrDives === 0
|
|
leftPadding: 0
|
|
topPadding: 0
|
|
id: innerListItem
|
|
enabled: true
|
|
supportsMouseEvents: true
|
|
checked: diveListView.currentIndex === model.index
|
|
width: parent.width
|
|
height: diveOutsideTrip ? diveListEntry.height + Kirigami.Units.smallSpacing : 0
|
|
visible: diveOutsideTrip
|
|
backgroundColor: checked ? subsurfaceTheme.primaryColor : subsurfaceTheme.backgroundColor
|
|
activeBackgroundColor: subsurfaceTheme.primaryColor
|
|
textColor: checked ? subsurfaceTheme.primaryTextColor : subsurfaceTheme.textColor
|
|
|
|
states: [
|
|
State {
|
|
name: "isHidden";
|
|
when: dive.tripId !== activeTrip && ! diveOutsideTrip
|
|
PropertyChanges {
|
|
target: innerListItem
|
|
height: 0
|
|
visible: false
|
|
}
|
|
},
|
|
State {
|
|
name: "isVisible";
|
|
when: dive.tripId === activeTrip || diveOutsideTrip
|
|
PropertyChanges {
|
|
target: innerListItem
|
|
height: diveListEntry.height + Kirigami.Units.smallSpacing
|
|
visible: true
|
|
}
|
|
}
|
|
]
|
|
transitions: [
|
|
Transition {
|
|
from: "isHidden"
|
|
to: "isVisible"
|
|
SequentialAnimation {
|
|
NumberAnimation {
|
|
property: "visible"
|
|
duration: 1
|
|
}
|
|
NumberAnimation {
|
|
property: "height"
|
|
duration: 200 + 20 * dive.tripNrDives
|
|
easing.type: Easing.InOutQuad
|
|
}
|
|
}
|
|
},
|
|
Transition {
|
|
from: "isVisible"
|
|
to: "isHidden"
|
|
SequentialAnimation {
|
|
NumberAnimation {
|
|
property: "height"
|
|
duration: 200 + 20 * dive.tripNrDives
|
|
easing.type: Easing.InOutQuad
|
|
}
|
|
NumberAnimation {
|
|
property: "visible"
|
|
duration: 1
|
|
}
|
|
}
|
|
}
|
|
]
|
|
|
|
// When clicked, the mode changes to details view
|
|
onClicked: {
|
|
if (detailsWindow.state === "view") {
|
|
diveListView.currentIndex = index
|
|
detailsWindow.showDiveIndex(index);
|
|
pageStack.push(detailsWindow);
|
|
}
|
|
}
|
|
|
|
property bool deleteButtonVisible: false
|
|
property bool copyButtonVisible: false
|
|
property bool pasteButtonVisible: false
|
|
|
|
onPressAndHold: {
|
|
deleteButtonVisible = true
|
|
copyButtonVisible = true
|
|
pasteButtonVisible = true
|
|
timer.restart()
|
|
}
|
|
Item {
|
|
Rectangle {
|
|
id: leftBarDive
|
|
width: dive.tripId == "" ? 0 : Kirigami.Units.smallSpacing
|
|
height: diveListEntry.height * 0.8
|
|
color: subsurfaceTheme.lightPrimaryColor
|
|
anchors {
|
|
left: parent.left
|
|
top: parent.top
|
|
leftMargin: Kirigami.Units.smallSpacing
|
|
topMargin: Kirigami.Units.smallSpacing * 2
|
|
bottomMargin: Kirigami.Units.smallSpacing * 2
|
|
}
|
|
}
|
|
Item {
|
|
id: diveListEntry
|
|
width: parent.width - Kirigami.Units.gridUnit * (innerListItem.deleteButtonVisible ? 3 * 3 : 1)
|
|
height: Math.ceil(childrenRect.height + Kirigami.Units.smallSpacing)
|
|
anchors.left: leftBarDive.right
|
|
Controls.Label {
|
|
id: locationText
|
|
text: dive.location
|
|
font.weight: Font.Bold
|
|
font.pointSize: subsurfaceTheme.regularPointSize
|
|
elide: Text.ElideRight
|
|
maximumLineCount: 1 // needed for elide to work at all
|
|
color: textColor
|
|
anchors {
|
|
left: parent.left
|
|
leftMargin: horizontalPadding * 2
|
|
topMargin: Kirigami.Units.smallSpacing
|
|
top: parent.top
|
|
right: parent.right
|
|
}
|
|
}
|
|
Row {
|
|
anchors {
|
|
left: locationText.left
|
|
top: locationText.bottom
|
|
topMargin: Kirigami.Units.smallSpacing
|
|
bottom: numberText.bottom
|
|
}
|
|
|
|
Controls.Label {
|
|
id: dateLabel
|
|
text: dive.date + " " + dive.time
|
|
width: Math.max(locationText.width * 0.45, paintedWidth) // helps vertical alignment throughout listview
|
|
font.pointSize: subsurfaceTheme.smallPointSize
|
|
color: innerListItem.checked ? subsurfaceTheme.darkerPrimaryTextColor : secondaryTextColor
|
|
}
|
|
// let's try to show the depth / duration very compact
|
|
Controls.Label {
|
|
text: dive.depth + ' / ' + dive.duration
|
|
width: Math.max(Kirigami.Units.gridUnit * 3, paintedWidth) // helps vertical alignment throughout listview
|
|
font.pointSize: subsurfaceTheme.smallPointSize
|
|
color: innerListItem.checked ? subsurfaceTheme.darkerPrimaryTextColor : secondaryTextColor
|
|
}
|
|
}
|
|
Controls.Label {
|
|
id: numberText
|
|
text: "#" + dive.number
|
|
font.pointSize: subsurfaceTheme.smallPointSize
|
|
color: innerListItem.checked ? subsurfaceTheme.darkerPrimaryTextColor : secondaryTextColor
|
|
anchors {
|
|
right: parent.right
|
|
rightMargin: horizontalPadding
|
|
top: locationText.bottom
|
|
topMargin: Kirigami.Units.smallSpacing
|
|
}
|
|
}
|
|
}
|
|
Rectangle {
|
|
id: copyButton
|
|
visible: copyButtonVisible
|
|
height: diveListEntry.height - 2 * Kirigami.Units.smallSpacing
|
|
width: height
|
|
color: subsurfaceTheme.lightDrawerColor
|
|
antialiasing: true
|
|
radius: Kirigami.Units.smallSpacing
|
|
anchors {
|
|
left: diveListEntry.right
|
|
verticalCenter: diveListEntry.verticalCenter
|
|
verticalCenterOffset: Kirigami.Units.smallSpacing / 2
|
|
rightMargin: horizontalPadding * 2
|
|
leftMargin: horizontalPadding * 2
|
|
}
|
|
Kirigami.Icon {
|
|
anchors {
|
|
horizontalCenter: parent.horizontalCenter
|
|
verticalCenter: parent.verticalCenter
|
|
}
|
|
source: ":/icons/edit-copy"
|
|
width: parent.height
|
|
height: width
|
|
}
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
enabled: parent.visible
|
|
onClicked: {
|
|
deleteButtonVisible = false
|
|
copyButtonVisible = false
|
|
pasteButtonVisible = false
|
|
timer.stop()
|
|
manager.copyDiveData(dive.id)
|
|
}
|
|
}
|
|
}
|
|
Rectangle {
|
|
id: pasteButton
|
|
visible: pasteButtonVisible
|
|
height: diveListEntry.height - 2 * Kirigami.Units.smallSpacing
|
|
width: height
|
|
color: subsurfaceTheme.lightDrawerColor
|
|
antialiasing: true
|
|
radius: Kirigami.Units.smallSpacing
|
|
anchors {
|
|
left: copyButton.right
|
|
verticalCenter: diveListEntry.verticalCenter
|
|
verticalCenterOffset: Kirigami.Units.smallSpacing / 2
|
|
rightMargin: horizontalPadding * 2
|
|
leftMargin: horizontalPadding * 2
|
|
}
|
|
Kirigami.Icon {
|
|
anchors {
|
|
horizontalCenter: parent.horizontalCenter
|
|
verticalCenter: parent.verticalCenter
|
|
}
|
|
source: ":/icons/edit-paste"
|
|
width: parent.height
|
|
height: width
|
|
}
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
enabled: parent.visible
|
|
onClicked: {
|
|
deleteButtonVisible = false
|
|
copyButtonVisible = false
|
|
pasteButtonVisible = false
|
|
timer.stop()
|
|
manager.pasteDiveData(dive.id)
|
|
}
|
|
}
|
|
}
|
|
Rectangle {
|
|
id: deleteButton
|
|
visible: deleteButtonVisible
|
|
height: diveListEntry.height - 2 * Kirigami.Units.smallSpacing
|
|
width: height
|
|
color: subsurfaceTheme.contrastAccentColor
|
|
antialiasing: true
|
|
radius: Kirigami.Units.smallSpacing
|
|
anchors {
|
|
left: pasteButton.right
|
|
right: parent.right
|
|
verticalCenter: diveListEntry.verticalCenter
|
|
verticalCenterOffset: Kirigami.Units.smallSpacing / 2
|
|
rightMargin: horizontalPadding * 2
|
|
leftMargin: horizontalPadding * 2
|
|
}
|
|
Kirigami.Icon {
|
|
anchors {
|
|
horizontalCenter: parent.horizontalCenter
|
|
verticalCenter: parent.verticalCenter
|
|
}
|
|
source: ":/icons/trash-empty"
|
|
width: parent.height
|
|
height: width
|
|
}
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
enabled: parent.visible
|
|
onClicked: {
|
|
deleteButtonVisible = false
|
|
copyButtonVisible = false
|
|
pasteButtonVisible = false
|
|
timer.stop()
|
|
manager.deleteDive(dive.id)
|
|
}
|
|
}
|
|
}
|
|
Timer {
|
|
id: timer
|
|
interval: 4000
|
|
onTriggered: {
|
|
deleteButtonVisible = false
|
|
copyButtonVisible = false
|
|
pasteButtonVisible = false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Component {
|
|
id: tripHeading
|
|
Item {
|
|
width: page.width
|
|
height: childrenRect.height
|
|
Rectangle {
|
|
id: headingBackground
|
|
height: section == "" ? 0 : sectionText.height + Kirigami.Units.gridUnit
|
|
anchors {
|
|
left: parent.left
|
|
right: parent.right
|
|
}
|
|
color: subsurfaceTheme.lightPrimaryColor
|
|
visible: section != ""
|
|
Rectangle {
|
|
id: dateBox
|
|
visible: section != ""
|
|
height: section == "" ? 0 : parent.height - Kirigami.Units.smallSpacing
|
|
width: section == "" ? 0 : 2.5 * Kirigami.Units.gridUnit * PrefDisplay.mobile_scale
|
|
color: subsurfaceTheme.primaryColor
|
|
radius: Kirigami.Units.smallSpacing * 2
|
|
antialiasing: true
|
|
anchors {
|
|
verticalCenter: parent.verticalCenter
|
|
left: parent.left
|
|
leftMargin: Kirigami.Units.smallSpacing
|
|
}
|
|
Controls.Label {
|
|
text: {
|
|
var trip = diveListView.model.tripIdToObject(section);
|
|
diveListView.model.tripShortDate(trip);
|
|
}
|
|
color: subsurfaceTheme.primaryTextColor
|
|
font.pointSize: subsurfaceTheme.smallPointSize
|
|
lineHeightMode: Text.FixedHeight
|
|
lineHeight: Kirigami.Units.gridUnit *.9
|
|
horizontalAlignment: Text.AlignHCenter
|
|
anchors {
|
|
horizontalCenter: parent.horizontalCenter
|
|
verticalCenter: parent.verticalCenter
|
|
}
|
|
}
|
|
}
|
|
MouseArea {
|
|
anchors.fill: headingBackground
|
|
onClicked: {
|
|
if (activeTrip === section)
|
|
activeTrip = ""
|
|
else
|
|
activeTrip = section
|
|
}
|
|
}
|
|
Controls.Label {
|
|
id: sectionText
|
|
text: {
|
|
var trip = diveListView.model.tripIdToObject(section);
|
|
diveListView.model.tripTitle(trip);
|
|
}
|
|
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
|
|
visible: text !== ""
|
|
font.weight: Font.Bold
|
|
font.pointSize: subsurfaceTheme.regularPointSize
|
|
anchors {
|
|
top: parent.top
|
|
left: dateBox.right
|
|
topMargin: Math.max(2, Kirigami.Units.gridUnit / 2)
|
|
leftMargin: horizontalPadding * 2
|
|
right: parent.right
|
|
}
|
|
color: subsurfaceTheme.lightPrimaryTextColor
|
|
}
|
|
}
|
|
Rectangle {
|
|
height: section == "" ? 0 : 1
|
|
width: parent.width
|
|
anchors.top: headingBackground.bottom
|
|
color: "#B2B2B2"
|
|
}
|
|
}
|
|
}
|
|
|
|
StartPage {
|
|
id: startPage
|
|
anchors.fill: parent
|
|
opacity: credentialStatus === CloudStatus.CS_NOCLOUD ||
|
|
(credentialStatus === CloudStatus.CS_VERIFIED) ? 0 : 1
|
|
visible: opacity > 0
|
|
Behavior on opacity { NumberAnimation { duration: Kirigami.Units.shortDuration } }
|
|
function setupActions() {
|
|
if (prefs.credentialStatus === CloudStatus.CS_VERIFIED ||
|
|
prefs.credentialStatus === CloudStatus.CS_NOCLOUD) {
|
|
page.actions.main = page.downloadFromDCAction
|
|
page.actions.right = page.addDiveAction
|
|
page.actions.left = page.filterToggleAction
|
|
page.title = qsTr("Dive list")
|
|
if (diveListView.count === 0)
|
|
showPassiveNotification(qsTr("Please tap the '+' button to add a dive (or download dives from a supported dive computer)"), 3000)
|
|
} else {
|
|
page.actions.main = null
|
|
page.actions.right = null
|
|
page.title = qsTr("Cloud credentials")
|
|
}
|
|
}
|
|
onVisibleChanged: {
|
|
setupActions();
|
|
}
|
|
|
|
Component.onCompleted: {
|
|
manager.finishSetup();
|
|
setupActions();
|
|
}
|
|
}
|
|
|
|
Controls.Label {
|
|
anchors.fill: parent
|
|
horizontalAlignment: Text.AlignHCenter
|
|
verticalAlignment: Text.AlignVCenter
|
|
text: diveListModel ? qsTr("No dives in dive list") : qsTr("Please wait, filtering dive list")
|
|
visible: diveListView.visible && diveListView.count === 0
|
|
}
|
|
Component {
|
|
id: filterHeader
|
|
Rectangle {
|
|
id: filterRectangle
|
|
visible: filterBar.height > 0
|
|
implicitHeight: filterBar.implicitHeight
|
|
implicitWidth: filterBar.implicitWidth
|
|
height: filterBar.height
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
color: subsurfaceTheme.backgroundColor
|
|
enabled: rootItem.filterToggle
|
|
RowLayout {
|
|
id: filterBar
|
|
z: 5 //make sure it sits on top
|
|
states: [
|
|
State {
|
|
name: "isVisible"
|
|
when: rootItem.filterToggle
|
|
PropertyChanges { target: filterBar; height: sitefilter.implicitHeight }
|
|
},
|
|
State {
|
|
name: "isHidden"
|
|
when: !rootItem.filterToggle
|
|
PropertyChanges { target: filterBar; height: 0 }
|
|
}
|
|
|
|
]
|
|
transitions: [
|
|
Transition {
|
|
NumberAnimation { property: "height"; duration: 400; easing.type: Easing.InOutQuad }
|
|
}
|
|
]
|
|
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.leftMargin: Kirigami.Units.gridUnit / 2
|
|
anchors.rightMargin: Kirigami.Units.gridUnit / 2
|
|
Controls.TextField {
|
|
id: sitefilter
|
|
z: 10
|
|
verticalAlignment: TextInput.AlignVCenter
|
|
Layout.fillWidth: true
|
|
text: ""
|
|
placeholderText: "Full text search"
|
|
onAccepted: {
|
|
manager.setFilter(text)
|
|
}
|
|
onEnabledChanged: {
|
|
// reset the filter when it gets toggled
|
|
text = ""
|
|
if (visible) {
|
|
forceActiveFocus()
|
|
}
|
|
}
|
|
}
|
|
Controls.Label {
|
|
id: numShown
|
|
z: 10
|
|
verticalAlignment: Text.AlignVCenter
|
|
text: numShownText
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
ListView {
|
|
id: diveListView
|
|
anchors.fill: parent
|
|
opacity: 1.0 - startPage.opacity
|
|
visible: opacity > 0
|
|
model: page.diveListModel
|
|
currentIndex: -1
|
|
delegate: diveDelegate
|
|
header: filterHeader
|
|
headerPositioning: ListView.OverlayHeader
|
|
boundsBehavior: Flickable.DragOverBounds
|
|
maximumFlickVelocity: parent.height * 5
|
|
bottomMargin: Kirigami.Units.iconSizes.medium + Kirigami.Units.gridUnit
|
|
cacheBuffer: 40 // this will increase memory use, but should help with scrolling
|
|
section.property: "dive.tripId"
|
|
section.criteria: ViewSection.FullString
|
|
section.delegate: tripHeading
|
|
section.labelPositioning: ViewSection.CurrentLabelAtStart | ViewSection.InlineLabels
|
|
onModelChanged: {
|
|
numShownText = diveModel.shown()
|
|
}
|
|
Connections {
|
|
target: detailsWindow
|
|
onCurrentIndexChanged: diveListView.currentIndex = detailsWindow.currentIndex
|
|
}
|
|
}
|
|
|
|
function showDownloadPage(vendor, product, connection) {
|
|
downloadFromDc.dcImportModel.clearTable()
|
|
pageStack.push(downloadFromDc)
|
|
if (vendor !== undefined && product !== undefined && connection !== undefined) {
|
|
/* set up the correct values on the download page */
|
|
if (vendor !== -1)
|
|
downloadFromDc.vendor = vendor
|
|
if (product !== -1)
|
|
downloadFromDc.product = product
|
|
if (connection !== -1)
|
|
downloadFromDc.connection = connection
|
|
}
|
|
}
|
|
|
|
property QtObject downloadFromDCAction: Kirigami.Action {
|
|
icon {
|
|
name: ":/icons/downloadDC"
|
|
color: subsurfaceTheme.primaryColor
|
|
}
|
|
text: qsTr("Download dives")
|
|
onTriggered: {
|
|
showDownloadPage()
|
|
}
|
|
}
|
|
|
|
property QtObject addDiveAction: Kirigami.Action {
|
|
icon {
|
|
name: ":/icons/list-add"
|
|
}
|
|
text: qsTr("Add dive")
|
|
onTriggered: {
|
|
startAddDive()
|
|
}
|
|
}
|
|
|
|
property QtObject filterToggleAction: Kirigami.Action {
|
|
icon {
|
|
name: ":icons/ic_filter_list"
|
|
}
|
|
text: qsTr("Filter dives")
|
|
onTriggered: {
|
|
rootItem.filterToggle = !rootItem.filterToggle
|
|
diveModel.resetFilter()
|
|
numShownText = diveModel.shown()
|
|
}
|
|
}
|
|
|
|
onBackRequested: {
|
|
if (startPage.visible && diveListView.count > 0 &&
|
|
prefs.credentialStatus !== CloudStatus.CS_INCORRECT_USER_PASSWD) {
|
|
prefs.credentialStatus = oldStatus
|
|
event.accepted = true;
|
|
}
|
|
if (!startPage.visible) {
|
|
if (Qt.platform.os != "ios") {
|
|
manager.quit()
|
|
}
|
|
// let's make sure Kirigami doesn't quit on our behalf
|
|
event.accepted = true
|
|
}
|
|
}
|
|
|
|
function setCurrentDiveListIndex(idx, noScroll) {
|
|
diveListView.currentIndex = idx
|
|
// updating the index of the ListView triggers a non-linear scroll
|
|
// animation that can be very slow. the fix is to stop this animation
|
|
// by setting contentY to itself and then using positionViewAtIndex().
|
|
// the downside is that the view jumps to the index immediately.
|
|
if (noScroll) {
|
|
diveListView.contentY = diveListView.contentY
|
|
diveListView.positionViewAtIndex(idx, ListView.Center)
|
|
}
|
|
}
|
|
}
|