diff options
author | Dirk Hohndel <dirk@hohndel.org> | 2016-04-04 22:02:03 -0700 |
---|---|---|
committer | Dirk Hohndel <dirk@hohndel.org> | 2016-04-04 22:33:58 -0700 |
commit | 7be962bfc2879a72c32ff67518731347dcdff6de (patch) | |
tree | d05bf7ab234a448ee37a15b608e2b939f2285d07 /mobile-widgets | |
parent | 2d760a7bff71c46c5aeba37c40d236ea16eefea2 (diff) | |
download | subsurface-7be962bfc2879a72c32ff67518731347dcdff6de.tar.gz |
Move subsurface-core to core and qt-mobile to mobile-widgets
Having subsurface-core as a directory name really messes with
autocomplete and is obviously redundant. Simmilarly, qt-mobile caused an
autocomplete conflict and also was inconsistent with the desktop-widget
name for the directory containing the "other" UI.
And while cleaning up the resulting change in the path name for include
files, I decided to clean up those even more to make them consistent
overall.
This could have been handled in more commits, but since this requires a
make clean before the build, it seemed more sensible to do it all in one.
Signed-off-by: Dirk Hohndel <dirk@hohndel.org>
Diffstat (limited to 'mobile-widgets')
31 files changed, 3823 insertions, 0 deletions
diff --git a/mobile-widgets/qml/About.qml b/mobile-widgets/qml/About.qml new file mode 100644 index 000000000..b1ca6e6bc --- /dev/null +++ b/mobile-widgets/qml/About.qml @@ -0,0 +1,59 @@ +import QtQuick 2.3 +import QtQuick.Controls 1.2 +import QtQuick.Layouts 1.1 +import org.kde.kirigami 1.0 as Kirigami +import org.subsurfacedivelog.mobile 1.0 + +Kirigami.ScrollablePage { + id: aboutPage + property int pageWidth: subsurfaceTheme.columnWidth - Kirigami.Units.smallSpacing + title: "About Subsurface-mobile" + + ColumnLayout { + spacing: Kirigami.Units.largeSpacing + width: aboutPage.width + Layout.margins: Kirigami.Units.gridUnit / 2 + + + Kirigami.Heading { + text: "About Subsurface-mobile" + Layout.topMargin: Kirigami.Units.gridUnit + Layout.alignment: Qt.AlignHCenter + Layout.maximumWidth: pageWidth + wrapMode: TextEdit.WrapAtWordBoundaryOrAnywhere + } + Image { + id: image + source: "qrc:/qml/subsurface-mobile-icon.png" + width: pageWidth / 2 + height: width + fillMode: Image.Stretch + Layout.alignment: Qt.AlignCenter + horizontalAlignment: Image.AlignHCenter + } + + Kirigami.Heading { + text: "A mobile version of the free Subsurface divelog software.\n" + + "View your dive logs while on the go." + level: 4 + Layout.alignment: Qt.AlignHCenter + Layout.topMargin: Kirigami.Units.largeSpacing * 3 + Layout.maximumWidth: pageWidth + wrapMode: TextEdit.WrapAtWordBoundaryOrAnywhere + anchors.horizontalCenter: parent.Center + horizontalAlignment: Text.AlignHCenter + } + + Kirigami.Heading { + text: "Version: " + manager.getVersion() + "\n\n© Subsurface developer team\n2011-2016" + level: 5 + font.pointSize: subsurfaceTheme.smallPointSize + 1 + Layout.alignment: Qt.AlignHCenter + Layout.topMargin: Kirigami.Units.largeSpacing + Layout.maximumWidth: pageWidth + wrapMode: TextEdit.WrapAtWordBoundaryOrAnywhere + anchors.horizontalCenter: parent.Center + horizontalAlignment: Text.AlignHCenter + } + } +} diff --git a/mobile-widgets/qml/CloudCredentials.qml b/mobile-widgets/qml/CloudCredentials.qml new file mode 100644 index 000000000..aa7c57651 --- /dev/null +++ b/mobile-widgets/qml/CloudCredentials.qml @@ -0,0 +1,84 @@ +import QtQuick 2.3 +import QtQuick.Controls 1.2 +import QtQuick.Window 2.2 +import QtQuick.Dialogs 1.2 +import QtQuick.Layouts 1.1 +import org.kde.kirigami 1.0 as Kirigami +import org.subsurfacedivelog.mobile 1.0 + +Item { + id: loginWindow + height: outerLayout.height + 2 * Kirigami.Units.gridUnit + + property string username: login.text; + property string password: password.text; + + function saveCredentials() { + manager.cloudUserName = login.text + manager.cloudPassword = password.text + manager.saveCloudCredentials() + } + + ColumnLayout { + id: outerLayout + width: subsurfaceTheme.columnWidth - 2 * Kirigami.Units.gridUnit + + onVisibleChanged: { + if (visible && manager.accessingCloud < 0) { + manager.appendTextToLog("Credential scrn: show kbd was: " + (Qt.inputMethod.isVisible ? "visible" : "invisible")) + Qt.inputMethod.show() + login.forceActiveFocus() + } else { + manager.appendTextToLog("Credential scrn: hide kbd was: " + (Qt.inputMethod.isVisible ? "visible" : "invisible")) + Qt.inputMethod.hide() + } + } + + Kirigami.Heading { + text: "Cloud credentials" + level: headingLevel + Layout.bottomMargin: Kirigami.Units.largeSpacing / 2 + } + + Kirigami.Label { + text: "Email" + } + + TextField { + id: login + text: manager.cloudUserName + Layout.fillWidth: true + inputMethodHints: Qt.ImhEmailCharactersOnly | + Qt.ImhNoAutoUppercase + } + + Kirigami.Label { + text: "Password" + } + + TextField { + id: password + text: manager.cloudPassword + echoMode: TextInput.Password + inputMethodHints: Qt.ImhSensitiveData | + Qt.ImhHiddenText | + Qt.ImhNoAutoUppercase + Layout.fillWidth: true + } + GridLayout { + columns: 2 + + CheckBox { + checked: false + id: showPassword + onCheckedChanged: { + password.echoMode = checked ? TextInput.Normal : TextInput.Password + } + } + Kirigami.Label { + text: "Show password" + } + } + Item { width: Kirigami.Units.gridUnit; height: width } + } +} diff --git a/mobile-widgets/qml/DiveDetails.qml b/mobile-widgets/qml/DiveDetails.qml new file mode 100644 index 000000000..108833470 --- /dev/null +++ b/mobile-widgets/qml/DiveDetails.qml @@ -0,0 +1,216 @@ +import QtQuick 2.4 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 +import QtQuick.Dialogs 1.2 +import QtQuick.Layouts 1.2 +import org.subsurfacedivelog.mobile 1.0 +import org.kde.kirigami 1.0 as Kirigami + +Kirigami.Page { + id: diveDetailsPage + property alias currentIndex: diveDetailsListView.currentIndex + property alias dive_id: detailsEdit.dive_id + property alias number: detailsEdit.number + property alias date: detailsEdit.dateText + property alias airtemp: detailsEdit.airtempText + property alias watertemp: detailsEdit.watertempText + property alias buddy: detailsEdit.buddyText + property alias divemaster: detailsEdit.divemasterText + property alias depth: detailsEdit.depthText + property alias duration: detailsEdit.durationText + property alias location: detailsEdit.locationText + property alias notes: detailsEdit.notesText + property alias suit: detailsEdit.suitText + property alias weight: detailsEdit.weightText + property alias startpressure: detailsEdit.startpressureText + property alias endpressure: detailsEdit.endpressureText + property alias gasmix: detailsEdit.gasmixText + + topPadding: applicationWindow().header.Layout.preferredHeight + leftPadding: 0 + rightPadding: 0 + bottomPadding: 0 + + title: diveDetailsListView.currentItem.modelData.dive.location + state: "view" + + states: [ + State { + name: "view" + PropertyChanges { target: diveDetailsPage; contextualActions: Qt.platform.os == "ios" ? [ deleteAction, backAction ] : [ deleteAction ] } + PropertyChanges { target: detailsEditScroll; opened: false } + }, + State { + name: "edit" + PropertyChanges { target: diveDetailsPage; contextualActions: Qt.platform.os == "ios" ? [ cancelAction ] : null } + PropertyChanges { target: detailsEditScroll; opened: true } + }, + State { + name: "add" + PropertyChanges { target: diveDetailsPage; contextualActions: Qt.platform.os == "ios" ? [ cancelAction ] : null } + PropertyChanges { target: detailsEditScroll; opened: true } + } + + ] + + property QtObject deleteAction: Action { + text: "Delete dive" + iconName: "trash-empty" + onTriggered: { + contextDrawer.close() + var deletedId = diveDetailsListView.currentItem.modelData.dive.id + manager.deleteDive(deletedId) + stackView.pop() + showPassiveNotification("Dive deleted", 3000, "Undo", + function() { + manager.undoDelete(deletedId) + }); + } + } + + property QtObject cancelAction: Kirigami.Action { + text: state === "edit" ? "Cancel edit" : "Cancel dive add" + iconName: "dialog-cancel" + onTriggered: { + contextDrawer.close() + if (state === "add") + returnTopPage() + else + endEditMode() + } + } + + property QtObject backAction: Action { + text: "Back to dive list" + iconName: "go-previous" + onTriggered: { + contextDrawer.close() + returnTopPage() + } + } + + mainAction: Action { + iconName: state !== "view" ? "document-save" : "document-edit" + onTriggered: { + if (state === "edit" || state === "add") { + detailsEdit.saveData() + } else { + startEditMode() + } + } + } + + onBackRequested: { + if (state === "edit") { + endEditMode() + event.accepted = true; + } else if (state === "add") { + endEditMode() + stackView.pop() + event.accepted = true; + } + // if we were in view mode, don't accept the event and pop the page + } + + function showDiveIndex(index) { + currentIndex = index; + diveDetailsListView.positionViewAtIndex(index, ListView.Beginning); + } + + function endEditMode() { + // if we were adding a dive, we need to remove it + if (state === "add") + manager.addDiveAborted(dive_id) + // just cancel the edit/add state + state = "view"; + Qt.inputMethod.hide(); + } + + function startEditMode() { + // set things up for editing - so make sure that the detailsEdit has + // all the right data (using the property aliases set up above) + dive_id = diveDetailsListView.currentItem.modelData.dive.id + number = diveDetailsListView.currentItem.modelData.dive.number + date = diveDetailsListView.currentItem.modelData.dive.date + " " + diveDetailsListView.currentItem.modelData.dive.time + location = diveDetailsListView.currentItem.modelData.dive.location + duration = diveDetailsListView.currentItem.modelData.dive.duration + depth = diveDetailsListView.currentItem.modelData.dive.depth + airtemp = diveDetailsListView.currentItem.modelData.dive.airTemp + watertemp = diveDetailsListView.currentItem.modelData.dive.waterTemp + suit = diveDetailsListView.currentItem.modelData.dive.suit + buddy = diveDetailsListView.currentItem.modelData.dive.buddy + divemaster = diveDetailsListView.currentItem.modelData.dive.divemaster + notes = diveDetailsListView.currentItem.modelData.dive.notes + if (diveDetailsListView.currentItem.modelData.dive.singleWeight) { + // we have only one weight, go ahead, have fun and edit it + weight = diveDetailsListView.currentItem.modelData.dive.sumWeight + } else { + // careful when translating, this text is "magic" in DiveDetailsEdit.qml + weight = "cannot edit multiple weight systems" + } + if (diveDetailsListView.currentItem.modelData.dive.getCylinder != "Multiple" ) { + startpressure = diveDetailsListView.currentItem.modelData.dive.startPressure + endpressure = diveDetailsListView.currentItem.modelData.dive.endPressure + gasmix = diveDetailsListView.currentItem.modelData.dive.firstGas + } else { + // careful when translating, this text is "magic" in DiveDetailsEdit.qml + startpressure = "cannot edit multiple cylinders" + endpressure = "cannot edit multiple cylinders" + gasmix = "cannot edit multiple gases" + } + + diveDetailsPage.state = "edit" + } + + onWidthChanged: diveDetailsListView.positionViewAtIndex(diveDetailsListView.currentIndex, ListView.Beginning); + + Item { + anchors.fill: parent + ScrollView { + id: diveDetailList + anchors.fill: parent + ListView { + id: diveDetailsListView + anchors.fill: parent + model: diveModel + currentIndex: -1 + boundsBehavior: Flickable.StopAtBounds + maximumFlickVelocity: parent.width * 5 + orientation: ListView.Horizontal + focus: true + clip: true + snapMode: ListView.SnapOneItem + onMovementEnded: { + currentIndex = indexAt(contentX+1, 1); + } + delegate: ScrollView { + id: internalScrollView + width: diveDetailsListView.width + height: diveDetailsListView.height + property var modelData: model + Flickable { + //contentWidth: parent.width + contentHeight: diveDetails.height + boundsBehavior: Flickable.StopAtBounds + DiveDetailsView { + id: diveDetails + width: internalScrollView.width + } + } + } + } + } + Kirigami.OverlaySheet { + id: detailsEditScroll + anchors.fill: parent + onOpenedChanged: { + if (!opened) { + endEditMode() + } + } + DiveDetailsEdit { + id: detailsEdit + } + } + } +} diff --git a/mobile-widgets/qml/DiveDetailsEdit.qml b/mobile-widgets/qml/DiveDetailsEdit.qml new file mode 100644 index 000000000..e4338b3b8 --- /dev/null +++ b/mobile-widgets/qml/DiveDetailsEdit.qml @@ -0,0 +1,236 @@ +import QtQuick 2.3 +import QtQuick.Controls 1.2 +import QtQuick.Controls.Styles 1.2 +import QtQuick.Dialogs 1.2 +import QtQuick.Layouts 1.1 +import org.subsurfacedivelog.mobile 1.0 +import org.kde.kirigami 1.0 as Kirigami + +Item { + id: detailsEdit + property int dive_id + property int number + property alias dateText: txtDate.text + property alias locationText: txtLocation.text + property string gpsText + property alias airtempText: txtAirTemp.text + property alias watertempText: txtWaterTemp.text + property alias suitText: txtSuit.text + property alias buddyText: txtBuddy.text + property alias divemasterText: txtDiveMaster.text + property alias notesText: txtNotes.text + property alias durationText: txtDuration.text + property alias depthText: txtDepth.text + property alias weightText: txtWeight.text + property alias startpressureText: txtStartPressure.text + property alias endpressureText: txtEndPressure.text + property alias gasmixText: txtGasMix.text + + function saveData() { + // apply the changes to the dive_table + manager.commitChanges(dive_id, detailsEdit.dateText, detailsEdit.locationText, detailsEdit.gpsText, detailsEdit.durationText, + detailsEdit.depthText, detailsEdit.airtempText, detailsEdit.watertempText, detailsEdit.suitText, + detailsEdit.buddyText, detailsEdit.divemasterText, detailsEdit.weightText, detailsEdit.notesText, + detailsEdit.startpressureText, detailsEdit.endpressureText, detailsEdit.gasmixText) + // trigger the profile to be redrawn + QMLProfile.diveId = dive_id + + // apply the changes to the dive detail view - since the edit could have changed the order + // first make sure that we are looking at the correct dive - our model allows us to look + // up the index based on the unique dive_id + var newIdx = diveModel.getIdxForId(dive_id) + diveDetailsListView.currentIndex = newIdx + diveDetailsListView.currentItem.modelData.date = detailsEdit.dateText + diveDetailsListView.currentItem.modelData.location = detailsEdit.locationText + diveDetailsListView.currentItem.modelData.duration = detailsEdit.durationText + diveDetailsListView.currentItem.modelData.depth = detailsEdit.depthText + diveDetailsListView.currentItem.modelData.airtemp = detailsEdit.airtempText + diveDetailsListView.currentItem.modelData.watertemp = detailsEdit.watertempText + diveDetailsListView.currentItem.modelData.suit = detailsEdit.suitText + diveDetailsListView.currentItem.modelData.buddy = detailsEdit.buddyText + diveDetailsListView.currentItem.modelData.divemaster = detailsEdit.divemasterText + diveDetailsListView.currentItem.modelData.notes = detailsEdit.notesText + diveDetailsPage.state = "view" + Qt.inputMethod.hide() + // now make sure we directly show the saved dive (this may be a new dive, or it may have moved) + showDiveIndex(newIdx) + } + + height: editArea.height + ColumnLayout { + id: editArea + spacing: Kirigami.Units.smallSpacing + width: subsurfaceTheme.columnWidth - 2 * Kirigami.Units.gridUnit + + GridLayout { + id: editorDetails + width: parent.width + columns: 2 + + Kirigami.Heading { + Layout.columnSpan: 2 + text: "Dive " + number + } + Kirigami.Label { + Layout.alignment: Qt.AlignRight + text: "Date:" + } + TextField { + id: txtDate; + Layout.fillWidth: true + } + Kirigami.Label { + Layout.alignment: Qt.AlignRight + text: "Location:" + } + TextField { + id: txtLocation; + Layout.fillWidth: true + } + + // we should add a checkbox here that allows the user + // to add the current location as the dive location + // (think of someone adding a dive while on the boat or + // at the dive site) + Kirigami.Label { + Layout.alignment: Qt.AlignRight + text: "Use current\nGPS location:" + } + CheckBox { + id: checkboxGPS + onCheckedChanged: { + if (checked) + gpsText = manager.getCurrentPosition() + } + } + + Kirigami.Label { + Layout.alignment: Qt.AlignRight + text: "Depth:" + } + TextField { + id: txtDepth + Layout.fillWidth: true + validator: RegExpValidator { regExp: /[^-]*/ } + } + Kirigami.Label { + Layout.alignment: Qt.AlignRight + text: "Duration:" + } + TextField { + id: txtDuration + Layout.fillWidth: true + validator: RegExpValidator { regExp: /[^-]*/ } + } + + Kirigami.Label { + Layout.alignment: Qt.AlignRight + text: "Air Temp:" + } + TextField { + id: txtAirTemp + Layout.fillWidth: true + } + + Kirigami.Label { + Layout.alignment: Qt.AlignRight + text: "Water Temp:" + } + TextField { + id: txtWaterTemp + Layout.fillWidth: true + } + + Kirigami.Label { + Layout.alignment: Qt.AlignRight + text: "Suit:" + } + TextField { + id: txtSuit + Layout.fillWidth: true + } + + Kirigami.Label { + Layout.alignment: Qt.AlignRight + text: "Buddy:" + } + TextField { + id: txtBuddy + Layout.fillWidth: true + } + + Kirigami.Label { + Layout.alignment: Qt.AlignRight + text: "Dive Master:" + } + TextField { + id: txtDiveMaster + Layout.fillWidth: true + } + + Kirigami.Label { + Layout.alignment: Qt.AlignRight + text: "Weight:" + } + TextField { + id: txtWeight + readOnly: (text == "cannot edit multiple weight systems" ? true : false) + Layout.fillWidth: true + } + + Kirigami.Label { + Layout.alignment: Qt.AlignRight + text: "Gas mix:" + } + TextField { + id: txtGasMix + readOnly: (text == "cannot edit multiple gases" ? true : false) + Layout.fillWidth: true + validator: RegExpValidator { regExp: /(EAN100|EAN\d\d|AIR|100|\d{1,2}|\d{1,2}\/\d{1,2})/ } + } + + Kirigami.Label { + Layout.alignment: Qt.AlignRight + text: "Start Pressure:" + } + TextField { + id: txtStartPressure + readOnly: (text == "cannot edit multiple cylinders" ? true : false) + Layout.fillWidth: true + } + + Kirigami.Label { + Layout.alignment: Qt.AlignRight + text: "End Pressure:" + } + TextField { + id: txtEndPressure + readOnly: (text == "cannot edit multiple cylinders" ? true : false) + Layout.fillWidth: true + } + + + Kirigami.Label { + Layout.columnSpan: 2 + Layout.alignment: Qt.AlignLeft + text: "Notes:" + } + TextArea { + Layout.columnSpan: 2 + width: parent.width + id: txtNotes + textFormat: TextEdit.RichText + focus: true + Layout.fillWidth: true + Layout.fillHeight: true + Layout.minimumHeight: Kirigami.Units.gridUnit * 6 + selectByMouse: true + wrapMode: TextEdit.WrapAtWordBoundaryOrAnywhere + } + } + Item { + height: Kirigami.Units.gridUnit * 3 + width: height // just to make sure the spacer doesn't produce scrollbars, but also isn't null + } + } +} diff --git a/mobile-widgets/qml/DiveDetailsView.qml b/mobile-widgets/qml/DiveDetailsView.qml new file mode 100644 index 000000000..ef1dc5605 --- /dev/null +++ b/mobile-widgets/qml/DiveDetailsView.qml @@ -0,0 +1,303 @@ +import QtQuick 2.3 +/* +import QtWebView 1.0 +*/ +import QtQuick.Controls 1.2 +import QtQuick.Controls.Styles 1.2 +import QtQuick.Dialogs 1.2 +import QtQuick.Layouts 1.1 +import org.subsurfacedivelog.mobile 1.0 +import org.kde.kirigami 1.0 as Kirigami + +Item { + id: detailsView + property real gridWidth: subsurfaceTheme.columnWidth - 2 * Kirigami.Units.gridUnit + property real col1Width: gridWidth * 0.23 + property real col2Width: gridWidth * 0.37 + property real col3Width: gridWidth * 0.20 + property real col4Width: gridWidth * 0.20 + + width: SubsurfaceTheme.columnWidth + height: mainLayout.implicitHeight + bottomLayout.implicitHeight + Kirigami.Units.iconSizes.large + Rectangle { + z: 99 + color: Kirigami.Theme.textColor + opacity: 0.3 + width: Kirigami.Units.smallSpacing/4 + anchors { + right: parent.right + top: parent.top + bottom: parent.bottom + } + } + GridLayout { + id: mainLayout + anchors { + top: parent.top + left: parent.left + right: parent.right + margins: Math.round(Kirigami.Units.gridUnit / 2) + } + columns: 4 + rowSpacing: Kirigami.Units.smallSpacing * 2 + columnSpacing: Kirigami.Units.smallSpacing + + Kirigami.Heading { + id: detailsViewHeading + Layout.fillWidth: true + text: dive.location + font.underline: dive.gps !== "" + Layout.columnSpan: 4 + wrapMode: TextEdit.WrapAtWordBoundaryOrAnywhere + Layout.topMargin: Kirigami.Units.largeSpacing + MouseArea { + anchors.fill: parent + onClicked: { + if (dive.gps !== "") + showMap(dive.gps) + } + } + } + Kirigami.Label { + id: dateLabel + text: "Date: " + opacity: 0.6 + Layout.alignment: Qt.AlignRight + } + Kirigami.Label { + text: dive.date + " " + dive.time + wrapMode: TextEdit.WrapAtWordBoundaryOrAnywhere + Layout.columnSpan: 2 + } + Kirigami.Label { + id: numberText + text: "#" + dive.number + color: Kirigami.Theme.textColor + wrapMode: TextEdit.WrapAtWordBoundaryOrAnywhere + } + + Kirigami.Label { + id: depthLabel + text: "Depth: " + opacity: 0.6 + Layout.alignment: Qt.AlignRight + } + Kirigami.Label { + text: dive.depth + Layout.fillWidth: true + wrapMode: TextEdit.WrapAtWordBoundaryOrAnywhere + } + Kirigami.Label { + text: "Duration: " + opacity: 0.6 + Layout.alignment: Qt.AlignRight + } + Kirigami.Label { + text: dive.duration + wrapMode: TextEdit.WrapAtWordBoundaryOrAnywhere + } + + QMLProfile { + id: qmlProfile + visible: !dive.noDive + Layout.fillWidth: true + Layout.preferredHeight: Layout.minimumHeight + Layout.minimumHeight: width * 0.75 + Layout.columnSpan: 4 + clip: false + Rectangle { + color: "transparent" + opacity: 0.6 + border.width: 1 + border.color: Kirigami.Theme.textColor; + anchors.fill: parent + } + } + Kirigami.Label { + id: noProfile + visible: dive.noDive + Layout.fillWidth: true + Layout.columnSpan: 4 + Layout.margins: Kirigami.Units.gridUnit + horizontalAlignment: Text.AlignHCenter + text: "No profile to show" + } + } + GridLayout { + id: bottomLayout + anchors { + top: mainLayout.bottom + left: parent.left + right: parent.right + margins: Math.round(Kirigami.Units.gridUnit / 2) + } + columns: 4 + rowSpacing: Kirigami.Units.smallSpacing * 2 + columnSpacing: Kirigami.Units.smallSpacing + + Kirigami.Heading { + Layout.fillWidth: true + level: 3 + text: "Dive Details" + Layout.columnSpan: 4 + } + + // first row - here we set up the column widths - total is 90% of width + Kirigami.Label { + text: "Suit:" + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + opacity: 0.6 + Layout.maximumWidth: detailsView.col1Width + Layout.preferredWidth: detailsView.col1Width + Layout.alignment: Qt.AlignRight + } + Kirigami.Label { + id: txtSuit + text: dive.suit + wrapMode: TextEdit.WrapAtWordBoundaryOrAnywhere + Layout.maximumWidth: detailsView.col2Width + Layout.preferredWidth: detailsView.col2Width + } + + Kirigami.Label { + text: "Air Temp:" + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + opacity: 0.6 + Layout.maximumWidth: detailsView.col3Width + Layout.preferredWidth: detailsView.col3Width + Layout.alignment: Qt.AlignRight + } + Kirigami.Label { + id: txtAirTemp + text: dive.airTemp + wrapMode: TextEdit.WrapAtWordBoundaryOrAnywhere + Layout.maximumWidth: detailsView.col4Width + Layout.preferredWidth: detailsView.col4Width + } + + Kirigami.Label { + text: "Cylinder:" + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + opacity: 0.6 + Layout.maximumWidth: detailsView.col1Width + Layout.preferredWidth: detailsView.col1Width + Layout.alignment: Qt.AlignRight + } + Kirigami.Label { + id: txtCylinder + text: dive.getCylinder + wrapMode: TextEdit.WrapAtWordBoundaryOrAnywhere + Layout.maximumWidth: detailsView.col2Width + Layout.preferredWidth: detailsView.col2Width + } + + Kirigami.Label { + text: "Water Temp:" + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + opacity: 0.6 + Layout.maximumWidth: detailsView.col3Width + Layout.preferredWidth: detailsView.col3Width + Layout.alignment: Qt.AlignRight + } + Kirigami.Label { + id: txtWaterTemp + text: dive.waterTemp + wrapMode: TextEdit.WrapAtWordBoundaryOrAnywhere + Layout.maximumWidth: detailsView.col4Width + Layout.preferredWidth: detailsView.col4Width + } + + Kirigami.Label { + text: "Dive Master:" + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + opacity: 0.6 + Layout.maximumWidth: detailsView.col1Width + Layout.preferredWidth: detailsView.col1Width + Layout.alignment: Qt.AlignRight + } + Kirigami.Label { + id: txtDiveMaster + text: dive.divemaster + wrapMode: TextEdit.WrapAtWordBoundaryOrAnywhere + Layout.maximumWidth: detailsView.col2Width + Layout.preferredWidth: detailsView.col2Width + } + + Kirigami.Label { + text: "Weight:" + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + opacity: 0.6 + Layout.maximumWidth: detailsView.col3Width + Layout.preferredWidth: detailsView.col3Width + Layout.alignment: Qt.AlignRight + } + Kirigami.Label { + id: txtWeight + text: dive.sumWeight + wrapMode: TextEdit.WrapAtWordBoundaryOrAnywhere + Layout.maximumWidth: detailsView.col4Width + Layout.preferredWidth: detailsView.col4Width + } + + Kirigami.Label { + text: "Buddy:" + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + opacity: 0.6 + Layout.maximumWidth: detailsView.col1Width + Layout.preferredWidth: detailsView.col1Width + Layout.alignment: Qt.AlignRight + } + Kirigami.Label { + id: txtBuddy + text: dive.buddy + wrapMode: TextEdit.WrapAtWordBoundaryOrAnywhere + Layout.maximumWidth: detailsView.col2Width + Layout.preferredWidth: detailsView.col2Width + } + + Kirigami.Label { + text: "SAC:" + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + opacity: 0.6 + Layout.maximumWidth: detailsView.col3Width + Layout.preferredWidth: detailsView.col3Width + Layout.alignment: Qt.AlignRight + } + Kirigami.Label { + id: txtSAC + text: dive.sac + wrapMode: TextEdit.WrapAtWordBoundaryOrAnywhere + Layout.maximumWidth: detailsView.col4Width + Layout.preferredWidth: detailsView.col4Width + } + + Kirigami.Heading { + Layout.fillWidth: true + level: 3 + text: "Notes" + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + Layout.columnSpan: 4 + } + + Kirigami.Label { + id: txtNotes + text: dive.notes + focus: true + Layout.columnSpan: 4 + Layout.fillWidth: true + Layout.fillHeight: true + //selectByMouse: true + wrapMode: TextEdit.WrapAtWordBoundaryOrAnywhere + } + Item { + Layout.columnSpan: 4 + Layout.fillWidth: true + Layout.minimumHeight: Kirigami.Units.gridUnit * 3 + } + Component.onCompleted: { + qmlProfile.setMargin(Kirigami.Units.smallSpacing) + qmlProfile.diveId = model.dive.id; + qmlProfile.update(); + } + } +} diff --git a/mobile-widgets/qml/DiveList.qml b/mobile-widgets/qml/DiveList.qml new file mode 100644 index 000000000..95af9a973 --- /dev/null +++ b/mobile-widgets/qml/DiveList.qml @@ -0,0 +1,302 @@ +import QtQuick 2.4 +import QtQuick.Controls 1.2 +import QtQuick.Layouts 1.2 +import QtQuick.Window 2.2 +import QtQuick.Dialogs 1.2 +import org.kde.kirigami 1.0 as Kirigami +import org.subsurfacedivelog.mobile 1.0 + +Kirigami.ScrollablePage { + id: page + objectName: "DiveList" + title: "Subsurface-mobile" + background: Rectangle { + color: Kirigami.Theme.viewBackgroundColor + } + + property int credentialStatus: manager.credentialStatus + property int numDives: diveListView.count + property color textColor: subsurfaceTheme.diveListTextColor + + function scrollToTop() { + diveListView.positionViewAtBeginning() + } + + Component { + id: diveDelegate + Kirigami.AbstractListItem { + enabled: true + supportsMouseEvents: true + checked: diveListView.currentIndex === model.index + width: parent.width + + property real detailsOpacity : 0 + property int horizontalPadding: Kirigami.Units.gridUnit / 2 - Kirigami.Units.smallSpacing + 1 + + // When clicked, the mode changes to details view + onClicked: { + if (detailsWindow.state === "view") { + diveListView.currentIndex = index + detailsWindow.showDiveIndex(index); + stackView.push(detailsWindow); + } + } + + property bool deleteButtonVisible: false + + onPressAndHold: { + deleteButtonVisible = true + timer.restart() + } + + Row { + width: parent.width - Kirigami.Units.gridUnit + height: childrenRect.height - Kirigami.Units.smallSpacing + spacing: horizontalPadding + add: Transition { + NumberAnimation { property: "opacity"; from: 0; to: 1.0; duration: 400 } + NumberAnimation { property: "scale"; from: 0; to: 1.0; duration: 400 } + } + Item { + id: diveListEntry + width: parent.width - Kirigami.Units.gridUnit + height: childrenRect.height - Kirigami.Units.smallSpacing + + Kirigami.Label { + id: locationText + text: dive.location + font.weight: Font.Light + elide: Text.ElideRight + maximumLineCount: 1 // needed for elide to work at all + color: textColor + anchors { + left: parent.left + leftMargin: horizontalPadding + top: parent.top + right: dateLabel.left + } + } + Kirigami.Label { + id: dateLabel + text: dive.date + " " + dive.time + font.pointSize: subsurfaceTheme.smallPointSize + color: textColor + anchors { + right: parent.right + top: parent.top + } + } + Row { + anchors { + left: parent.left + leftMargin: horizontalPadding + right: parent.right + rightMargin: horizontalPadding + topMargin: - Kirigami.Units.smallSpacing * 2 + bottom: numberText.bottom + } + Kirigami.Label { + text: 'Depth: ' + font.pointSize: subsurfaceTheme.smallPointSize + color: textColor + } + Kirigami.Label { + text: dive.depth + width: Math.max(Kirigami.Units.gridUnit * 3, paintedWidth) // helps vertical alignment throughout listview + font.pointSize: subsurfaceTheme.smallPointSize + color: textColor + } + Kirigami.Label { + text: 'Duration: ' + font.pointSize: subsurfaceTheme.smallPointSize + color: textColor + } + Kirigami.Label { + text: dive.duration + font.pointSize: subsurfaceTheme.smallPointSize + color: textColor + } + } + Kirigami.Label { + id: numberText + text: "#" + dive.number + font.pointSize: subsurfaceTheme.smallPointSize + color: textColor + anchors { + right: parent.right + top: locationText.bottom + topMargin: - Kirigami.Units.smallSpacing * 2 + } + } + } + Rectangle { + visible: deleteButtonVisible + height: diveListEntry.height - Kirigami.Units.smallSpacing + width: height - 3 * Kirigami.Units.smallSpacing + color: "#FF3030" + antialiasing: true + radius: Kirigami.Units.smallSpacing + Kirigami.Icon { + anchors { + horizontalCenter: parent.horizontalCenter + verticalCenter: parent.verticalCenter + } + source: "trash-empty" + } + MouseArea { + anchors.fill: parent + enabled: parent.visible + onClicked: { + parent.visible = false + timer.stop() + manager.deleteDive(dive.id) + } + } + } + Item { + Timer { + id: timer + interval: 4000 + onTriggered: { + deleteButtonVisible = false + } + } + } + } + } + } + + Component { + id: tripHeading + Item { + width: page.width - Kirigami.Units.gridUnit + height: childrenRect.height + Kirigami.Units.smallSpacing * 2 + Math.max(2, Kirigami.Units.gridUnit / 2) + + Kirigami.Heading { + id: sectionText + text: { + // if the tripMeta (which we get as "section") ends in ::-- we know + // that there's no trip -- otherwise strip the meta information before + // the :: and show the trip location + var shownText + var endsWithDoubleDash = /::--$/; + if (endsWithDoubleDash.test(section) || section === "--") { + shownText = "" + } else { + shownText = section.replace(/.*::/, "") + } + shownText + } + anchors { + top: parent.top + left: parent.left + topMargin: Math.max(2, Kirigami.Units.gridUnit / 2) + leftMargin: Kirigami.Units.gridUnit / 2 + right: parent.right + } + color: textColor + level: 2 + } + Rectangle { + height: Math.max(2, Kirigami.Units.gridUnit / 12) // we want a thicker line + anchors { + top: sectionText.bottom + left: parent.left + leftMargin: Kirigami.Units.gridUnit * -2 + rightMargin: Kirigami.Units.gridUnit * -2 + right: parent.right + } + color: subsurfaceTheme.accentColor + } + } + } + + ScrollView { + id: startPageWrapper + anchors.fill: parent + opacity: (diveListView.count > 0 && (credentialStatus == QMLManager.VALID || credentialStatus == QMLManager.VALID_EMAIL)) ? 0 : 1 + visible: opacity > 0 + Behavior on opacity { NumberAnimation { duration: Kirigami.Units.shortDuration } } + onVisibleChanged: { + if (visible) { + page.mainAction = page.saveAction + } else { + page.mainAction = page.addDiveAction + } + } + + StartPage { + id: startPage + } + } + + ListView { + id: diveListView + anchors.fill: parent + opacity: 0.8 - startPageWrapper.opacity + visible: opacity > 0 + model: diveModel + currentIndex: -1 + delegate: diveDelegate + //boundsBehavior: Flickable.StopAtBounds + maximumFlickVelocity: parent.height * 5 + bottomMargin: Kirigami.Units.iconSizes.medium + Kirigami.Units.gridUnit + cacheBuffer: 0 // seems to avoid empty rendered profiles + section.property: "dive.tripMeta" + section.criteria: ViewSection.FullString + section.delegate: tripHeading + header: Kirigami.Heading { + x: Kirigami.Units.gridUnit / 2 + height: paintedHeight + Kirigami.Units.gridUnit / 2 + verticalAlignment: Text.AlignBottom + text: "Dive Log" + } + Connections { + target: detailsWindow + onCurrentIndexChanged: diveListView.currentIndex = detailsWindow.currentIndex + } + Connections { + target: stackView + onDepthChanged: { + if (stackView.depth === 1) { + diveListView.currentIndex = -1; + } + } + } + Connections { + target: header + onTitleBarClicked: { + // if we can see the dive list and it's not at the top already, go to the top, + // otherwise have the title bar handle the click (for bread-crumb navigation) + if (stackView.currentItem.objectName === "DiveList" && diveListView.contentY > Kirigami.Units.gridUnit) { + diveListView.positionViewAtBeginning() + event.accepted = true + } else { + event.accepted = false + } + } + } + + } + + property QtObject addDiveAction: Action { + iconName: "list-add" + onTriggered: { + startAddDive() + } + } + + property QtObject saveAction: Action { + iconName: "document-save" + onTriggered: { + startPage.saveCredentials(); + } + } + + onBackRequested: { + if (startPageWrapper.visible && diveListView.count > 0 && manager.credentialStatus != QMLManager.INVALID) { + manager.credentialStatus = oldStatus + event.accepted = true; + } + } +} diff --git a/mobile-widgets/qml/DownloadFromDiveComputer.qml b/mobile-widgets/qml/DownloadFromDiveComputer.qml new file mode 100644 index 000000000..a062ffaa0 --- /dev/null +++ b/mobile-widgets/qml/DownloadFromDiveComputer.qml @@ -0,0 +1,125 @@ +import QtQuick 2.3 +import QtQuick.Controls 1.2 +import QtQuick.Controls.Styles 1.2 +import QtQuick.Window 2.2 +import QtQuick.Dialogs 1.2 +import QtQuick.Layouts 1.1 +import org.subsurfacedivelog.mobile 1.0 +import org.kde.kirigami 1.0 as Kirigami + +Kirigami.Page { + id: diveComputerDownloadWindow + anchors.top:parent.top + width: parent.width + height: parent.height + Layout.fillWidth: true; + title: "Dive Computer" + +/* this can be done by hitting the back key + contextualActions: [ + Action { + text: "Close Preferences" + iconName: "dialog-cancel" + onTriggered: { + stackView.pop() + contextDrawer.close() + } + } + ] + */ + ColumnLayout { + anchors.top: parent.top + height: parent.height + width: parent.width + Layout.fillWidth: true + RowLayout { + anchors.top:parent.top + Layout.fillWidth: true + Text { text: " Vendor name : " } + ComboBox { Layout.fillWidth: true } + } + RowLayout { + Text { text: " Dive Computer:" } + ComboBox { Layout.fillWidth: true } + } + RowLayout { + Text { text: " Progress:" } + Layout.fillWidth: true + ProgressBar { Layout.fillWidth: true } + } + RowLayout { + SubsurfaceButton { + text: "Download" + onClicked: { + text: "Retry" + stackView.pop(); + } + } + SubsurfaceButton { + id:quitbutton + text: "Quit" + onClicked: { + stackView.pop(); + } + } + } + RowLayout { + Text { + text: " Downloaded dives" + } + } + TableView { + width: parent.width + Layout.fillWidth: true // The tableview should fill + Layout.fillHeight: true // all remaining vertical space + height: parent.height // on this screen + TableViewColumn { + width: parent.width / 2 + role: "datetime" + title: "Date / Time" + } + TableViewColumn { + width: parent.width / 4 + role: "duration" + title: "Duration" + } + TableViewColumn { + width: parent.width / 4 + role: "depth" + title: "Depth" + } + } + RowLayout { + Layout.fillWidth: true + SubsurfaceButton { + text: "Accept" + onClicked: { + stackView.pop(); + } + } + SubsurfaceButton { + text: "Quit" + onClicked: { + stackView.pop(); + } + } + Text { + text: "" // Spacer between 2 button groups + Layout.fillWidth: true + } + SubsurfaceButton { + text: "Select All" + } + SubsurfaceButton { + id: unselectbutton + text: "Unselect All" + } + } + RowLayout { // spacer to make space for silly button + Layout.minimumHeight: 1.2 * unselectbutton.height + Text { + text:"" + } + } + } +} diff --git a/mobile-widgets/qml/GpsList.qml b/mobile-widgets/qml/GpsList.qml new file mode 100644 index 000000000..6903acd80 --- /dev/null +++ b/mobile-widgets/qml/GpsList.qml @@ -0,0 +1,128 @@ +import QtQuick 2.3 +import QtQuick.Controls 1.2 +import QtQuick.Controls.Styles 1.2 +import QtQuick.Window 2.2 +import QtQuick.Dialogs 1.2 +import QtQuick.Layouts 1.1 +import QtQuick.Window 2.2 +import org.subsurfacedivelog.mobile 1.0 +import org.kde.kirigami 1.0 as Kirigami + +Kirigami.ScrollablePage { + id: gpsListWindow + width: parent.width - Kirigami.Units.gridUnit + anchors.margins: Kirigami.Units.gridUnit / 2 + objectName: "gpsList" + title: "GPS Fixes" + +/* this can be done by hitting the back key + contextualActions: [ + Action { + text: "Close GPS list" + iconName: "dialog-cancel" + onTriggered: { + stackView.pop() + contextDrawer.close() + } + } + ] + */ + Component { + id: gpsDelegate + Kirigami.SwipeListItem { + id: gpsFix + enabled: true + width: parent.width + property int horizontalPadding: Kirigami.Units.gridUnit / 2 - Kirigami.Units.smallSpacing + 1 + + Kirigami.BasicListItem { + supportsMouseEvents: true + width: parent.width - Kirigami.Units.gridUnit + height: childrenRect.height - Kirigami.Units.smallSpacing + GridLayout { + columns: 4 + id: timeAndName + anchors { + left: parent.left + leftMargin: horizontalPadding + right: parent.right + rightMargin: horizontalPadding + } + Kirigami.Label { + text: 'Date: ' + opacity: 0.6 + font.pointSize: subsurfaceTheme.smallPointSize + } + Kirigami.Label { + text: date + Layout.preferredWidth: Math.max(parent.width / 5, paintedWidth) + font.pointSize: subsurfaceTheme.smallPointSize + } + Kirigami.Label { + text: 'Name: ' + opacity: 0.6 + font.pointSize: subsurfaceTheme.smallPointSize + } + Kirigami.Label { + text: name + Layout.preferredWidth: Math.max(parent.width / 5, paintedWidth) + font.pointSize: subsurfaceTheme.smallPointSize + } + Kirigami.Label { + text: 'Latitude: ' + opacity: 0.6 + font.pointSize: subsurfaceTheme.smallPointSize + } + Kirigami.Label { + text: latitude + font.pointSize: subsurfaceTheme.smallPointSize + } + Kirigami.Label { + text: 'Longitude: ' + opacity: 0.6 + font.pointSize: subsurfaceTheme.smallPointSize + } + Kirigami.Label { + text: longitude + font.pointSize: subsurfaceTheme.smallPointSize + } + } + } + actions: [ + Kirigami.Action { + iconName: "trash-empty" + onTriggered: { + print("delete this!") + manager.deleteGpsFix(when) + } + }, + Kirigami.Action { + iconName: "gps" + onTriggered: { + showMap(latitude + " " + longitude) + } + } + + ] + } + } + + ListView { + id: gpsListView + anchors.fill: parent + model: gpsModel + currentIndex: -1 + delegate: gpsDelegate + boundsBehavior: Flickable.StopAtBounds + maximumFlickVelocity: parent.height * 5 + cacheBuffer: Math.max(5000, parent.height * 5) + focus: true + clip: true + header: Kirigami.Heading { + x: Kirigami.Units.gridUnit / 2 + height: paintedHeight + Kirigami.Units.gridUnit / 2 + verticalAlignment: Text.AlignBottom + text: "List of stored GPS fixes" + } + } +} diff --git a/mobile-widgets/qml/Log.qml b/mobile-widgets/qml/Log.qml new file mode 100644 index 000000000..d617901de --- /dev/null +++ b/mobile-widgets/qml/Log.qml @@ -0,0 +1,40 @@ +import QtQuick 2.3 +import QtQuick.Controls 1.2 +import QtQuick.Controls.Styles 1.2 +import QtQuick.Window 2.2 +import QtQuick.Dialogs 1.2 +import QtQuick.Layouts 1.1 +import QtQuick.Window 2.2 +import org.subsurfacedivelog.mobile 1.0 +import org.kde.kirigami 1.0 as Kirigami + +Kirigami.ScrollablePage { + id: logWindow + width: parent.width - Kirigami.Units.gridUnit + anchors.margins: Kirigami.Units.gridUnit / 2 + objectName: "Log" + title: "Application Log" + + property int pageWidth: subsurfaceTheme.columnWidth - Kirigami.Units.smallSpacing + + ColumnLayout { + width: pageWidth + spacing: Kirigami.Units.smallSpacing + Kirigami.Heading { + text: "Application Log" + } + Kirigami.Label { + id: logContent + width: parent.width + Layout.preferredWidth: parent.width + Layout.maximumWidth: parent.width + wrapMode: TextEdit.WrapAtWordBoundaryOrAnywhere + text: manager.logText + } + Rectangle { + color: "transparent" + height: Kirigami.Units.gridUnit * 2 + width: pageWidth + } + } +} diff --git a/mobile-widgets/qml/Preferences.qml b/mobile-widgets/qml/Preferences.qml new file mode 100644 index 000000000..3ec96d198 --- /dev/null +++ b/mobile-widgets/qml/Preferences.qml @@ -0,0 +1,74 @@ +import QtQuick 2.3 +import QtQuick.Controls 1.2 +import QtQuick.Window 2.2 +import QtQuick.Dialogs 1.2 +import QtQuick.Layouts 1.1 +import org.kde.kirigami 1.0 as Kirigami +import org.subsurfacedivelog.mobile 1.0 + +Kirigami.Page { + + title: "Preferences" + mainAction: Action { + text: "Save" + iconName: "document-save" + onTriggered: { + manager.distanceThreshold = distanceThreshold.text + manager.timeThreshold = timeThreshold.text + manager.savePreferences() + stackView.pop() + } + } + + GridLayout { + + signal accept + + columns: 2 + width: parent.width - Kirigami.Units.gridUnit + anchors { + fill: parent + margins: Kirigami.Units.gridUnit / 2 + } + + Kirigami.Heading { + text: "Preferences" + Layout.bottomMargin: Kirigami.Units.largeSpacing / 2 + Layout.columnSpan: 2 + } + + Kirigami.Heading { + text: "Subsurface GPS data webservice" + level: 3 + Layout.topMargin: Kirigami.Units.largeSpacing + Layout.bottomMargin: Kirigami.Units.largeSpacing / 2 + Layout.columnSpan: 2 + } + + Kirigami.Label { + text: "Distance threshold (meters)" + Layout.alignment: Qt.AlignRight + } + + TextField { + id: distanceThreshold + text: manager.distanceThreshold + Layout.fillWidth: true + } + + Kirigami.Label { + text: "Time threshold (minutes)" + Layout.alignment: Qt.AlignRight + } + + TextField { + id: timeThreshold + text: manager.timeThreshold + Layout.fillWidth: true + } + + Item { + Layout.fillHeight: true + } + } +} diff --git a/mobile-widgets/qml/StartPage.qml b/mobile-widgets/qml/StartPage.qml new file mode 100644 index 000000000..2d70cfcb3 --- /dev/null +++ b/mobile-widgets/qml/StartPage.qml @@ -0,0 +1,42 @@ +import QtQuick 2.5 +import QtQuick.Controls 1.2 +import QtQuick.Controls.Styles 1.2 +import QtQuick.Layouts 1.1 +import org.kde.kirigami 1.0 as Kirigami +import org.subsurfacedivelog.mobile 1.0 + +ColumnLayout { + id: startpage + width: subsurfaceTheme.columnWidth + + function saveCredentials() { cloudCredentials.saveCredentials() } + + Kirigami.Heading { + Layout.margins: Kirigami.Units.gridUnit + text: "Subsurface-mobile" + } + Kirigami.Label { + id: explanationText + Layout.fillWidth: true + Layout.margins: Kirigami.Units.gridUnit + Layout.topMargin: 0 + text: "In order to use Subsurface-mobile you need to have a Subsurface cloud storage account " + + "(which can be created with the Subsurface desktop application)." + wrapMode: Text.WordWrap + } + Kirigami.Label { + id: messageArea + Layout.fillWidth: true + Layout.margins: Kirigami.Units.gridUnit + Layout.topMargin: 0 + text: manager.startPageText + wrapMode: Text.WordWrap + } + CloudCredentials { + id: cloudCredentials + Layout.fillWidth: true + Layout.margins: Kirigami.Units.gridUnit + Layout.topMargin: 0 + property int headingLevel: 3 + } +} diff --git a/mobile-widgets/qml/SubsurfaceButton.qml b/mobile-widgets/qml/SubsurfaceButton.qml new file mode 100644 index 000000000..174d44659 --- /dev/null +++ b/mobile-widgets/qml/SubsurfaceButton.qml @@ -0,0 +1,26 @@ +import QtQuick 2.5 +import QtQuick.Controls 1.2 +import QtQuick.Controls.Styles 1.2 +import org.kde.kirigami 1.0 as Kirigami + +Button { + style: ButtonStyle { + padding { + top: Kirigami.Units.smallSpacing * 2 + left: Kirigami.Units.smallSpacing * 4 + right: Kirigami.Units.smallSpacing * 4 + bottom: Kirigami.Units.smallSpacing * 2 + } + background: Rectangle { + border.width: 1 + radius: height / 3 + color: control.pressed ? subsurfaceTheme.shadedColor : subsurfaceTheme.accentColor + } + label: Text{ + text: control.text + color: subsurfaceTheme.accentTextColor + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + } + } +} diff --git a/mobile-widgets/qml/TextButton.qml b/mobile-widgets/qml/TextButton.qml new file mode 100644 index 000000000..3e5a36735 --- /dev/null +++ b/mobile-widgets/qml/TextButton.qml @@ -0,0 +1,37 @@ +import QtQuick 2.3 + +Rectangle { + id: container + + property alias text: label.text + + signal clicked + + width: label.width + 20; height: label.height + 6 + smooth: true + radius: 10 + + gradient: Gradient { + GradientStop { id: gradientStop; position: 0.0; color: palette.light } + GradientStop { position: 1.0; color: palette.button } + } + + SystemPalette { id: palette } + + MouseArea { + id: mouseArea + anchors.fill: parent + onClicked: { container.clicked() } + } + + Text { + id: label + anchors.centerIn: parent + } + + states: State { + name: "pressed" + when: mouseArea.pressed + PropertyChanges { target: gradientStop; color: palette.dark } + } +} diff --git a/mobile-widgets/qml/ThemeTest.qml b/mobile-widgets/qml/ThemeTest.qml new file mode 100644 index 000000000..c0916aea0 --- /dev/null +++ b/mobile-widgets/qml/ThemeTest.qml @@ -0,0 +1,115 @@ +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Layouts 1.1 +import QtQuick.Window 2.2 +import org.kde.kirigami 1.0 as Kirigami + +Kirigami.Page { + + title: "Theme Information" +/* this can be done by hitting the back key + contextualActions: [ + Action { + text: "Close Theme info" + iconName: "dialog-cancel" + onTriggered: { + stackView.pop() + contextDrawer.close() + } + } + ] + */ + GridLayout { + id: themetest + columns: 2 + anchors.margins: Kirigami.Units.gridUnit / 2 + + Kirigami.Heading { + Layout.columnSpan: 2 + text: "Theme Information" + } + + Kirigami.Heading { + text: "Screen" + Layout.columnSpan: 2 + level: 3 + } + FontMetrics { + id: fm + } + + Kirigami.Label { + text: "Geometry (pixels):" + } + Kirigami.Label { + text: rootItem.width + "x" + rootItem.height + } + + Kirigami.Label { + text: "Geometry (gridUnits):" + } + Kirigami.Label { + text: Math.round(rootItem.width / Kirigami.Units.gridUnit) + "x" + Math.round(rootItem.height / Kirigami.Units.gridUnit) + } + + Kirigami.Label { + text: "Units.gridUnit:" + } + Kirigami.Label { + text: Kirigami.Units.gridUnit + } + + Kirigami.Label { + text: "Units.devicePixelRatio:" + } + Kirigami.Label { + text: Screen.devicePixelRatio + } + + Kirigami.Heading { + text: "Font Metrics" + level: 3 + Layout.columnSpan: 2 + } + + Kirigami.Label { + text: "FontMetrics pointSize:" + } + Kirigami.Label { + text: fm.font.pointSize + } + + Kirigami.Label { + text: "FontMetrics pixelSize:" + } + Kirigami.Label { + text: fm.height + } + + Kirigami.Label { + text: "FontMetrics devicePixelRatio:" + } + Kirigami.Label { + text: fm.height / fm.font.pointSize + } + + Kirigami.Label { + text: "Text item pixelSize:" + } + Text { + text: font.pixelSize + } + + Kirigami.Label { + text: "Text item pointSize:" + } + Text { + text: font.pointSize + } + + Kirigami.Label { + Layout.columnSpan: 2 + Layout.fillHeight: true + } + } +} diff --git a/mobile-widgets/qml/TopBar.qml b/mobile-widgets/qml/TopBar.qml new file mode 100644 index 000000000..024b818b0 --- /dev/null +++ b/mobile-widgets/qml/TopBar.qml @@ -0,0 +1,59 @@ +import QtQuick 2.3 +import QtQuick.Controls 1.2 +import QtQuick.Controls.Styles 1.2 +import QtQuick.Window 2.2 +import QtQuick.Dialogs 1.2 +import QtQuick.Layouts 1.1 +import QtQuick.Window 2.2 +import org.kde.kirigami 1.0 as Kirigami +import org.subsurfacedivelog.mobile 1.0 + +Rectangle { + id: topPart + + color: subsurfaceTheme.accentColor + Layout.minimumHeight: Math.round(Kirigami.Units.gridUnit * 1.5) + Layout.fillWidth: true + Layout.margins: 0 + RowLayout { + anchors.verticalCenter: topPart.verticalCenter + Item { + Layout.preferredHeight: subsurfaceLogo.height + Layout.leftMargin: Kirigami.Units.gridUnit / 4 + Image { + id: subsurfaceLogo + source: "qrc:/qml/subsurface-mobile-icon.png" + anchors { + verticalCenter: parent.Center + left: parent.left + } + width: Math.round(Kirigami.Units.gridUnit) + height: width + } + Kirigami.Label { + text: qsTr("Subsurface-mobile") + font.pointSize: Math.round(Kirigami.Theme.defaultFont.pointSize) + height: subsurfaceLogo.height + anchors { + left: subsurfaceLogo.right + leftMargin: Math.round(Kirigami.Units.gridUnit / 2) + } + font.weight: Font.Light + verticalAlignment: Text.AlignVCenter + Layout.fillWidth: false + color: subsurfaceTheme.accentTextColor + } + } + Item { + Layout.fillWidth: true + } + } + MouseArea { + anchors.fill: topPart + onClicked: { + if (stackView.depth == 1 && showingDiveList) { + scrollToTop() + } + } + } +} diff --git a/mobile-widgets/qml/dive.jpg b/mobile-widgets/qml/dive.jpg Binary files differnew file mode 100644 index 000000000..56445648a --- /dev/null +++ b/mobile-widgets/qml/dive.jpg diff --git a/mobile-widgets/qml/icons/context-menu.png b/mobile-widgets/qml/icons/context-menu.png Binary files differnew file mode 100644 index 000000000..df34cfd4f --- /dev/null +++ b/mobile-widgets/qml/icons/context-menu.png diff --git a/mobile-widgets/qml/icons/context-menu.svg b/mobile-widgets/qml/icons/context-menu.svg new file mode 100644 index 000000000..e0750c57e --- /dev/null +++ b/mobile-widgets/qml/icons/context-menu.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><path d="M24 16c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 4c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm0 12c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4z"/></svg>
\ No newline at end of file diff --git a/mobile-widgets/qml/icons/main-menu.png b/mobile-widgets/qml/icons/main-menu.png Binary files differnew file mode 100644 index 000000000..20729b8f5 --- /dev/null +++ b/mobile-widgets/qml/icons/main-menu.png diff --git a/mobile-widgets/qml/icons/main-menu.svg b/mobile-widgets/qml/icons/main-menu.svg new file mode 100644 index 000000000..1e89193f5 --- /dev/null +++ b/mobile-widgets/qml/icons/main-menu.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><path d="M6 36h36v-4H6v4zm0-10h36v-4H6v4zm0-14v4h36v-4H6z"/></svg> diff --git a/mobile-widgets/qml/icons/menu-back.png b/mobile-widgets/qml/icons/menu-back.png Binary files differnew file mode 100644 index 000000000..dc96b7728 --- /dev/null +++ b/mobile-widgets/qml/icons/menu-back.png diff --git a/mobile-widgets/qml/icons/menu-edit.png b/mobile-widgets/qml/icons/menu-edit.png Binary files differnew file mode 100644 index 000000000..ea7dd055a --- /dev/null +++ b/mobile-widgets/qml/icons/menu-edit.png diff --git a/mobile-widgets/qml/main.qml b/mobile-widgets/qml/main.qml new file mode 100644 index 000000000..f4f6ea28b --- /dev/null +++ b/mobile-widgets/qml/main.qml @@ -0,0 +1,360 @@ +import QtQuick 2.4 +import QtQuick.Controls 1.2 +import QtQuick.Controls.Styles 1.2 +import QtQuick.Window 2.2 +import QtQuick.Dialogs 1.2 +import QtQuick.Layouts 1.1 +import QtQuick.Window 2.2 +import org.subsurfacedivelog.mobile 1.0 +import org.kde.kirigami 1.0 as Kirigami + +Kirigami.ApplicationWindow { + id: rootItem + title: qsTr("Subsurface-mobile") + + header.minimumHeight: 0 + header.preferredHeight: Kirigami.Units.gridUnit + header.maximumHeight: Kirigami.Units.gridUnit * 2 + property bool fullscreen: true + property int oldStatus: -1 + property alias accessingCloud: manager.accessingCloud + property QtObject notification: null + property bool showingDiveList: false + property alias syncToCloud: manager.syncToCloud + onAccessingCloudChanged: { + if (accessingCloud >= 0) { + // we now keep updating this to show progress, so timing out after 30 seconds is more useful + // but should still be very conservative + showPassiveNotification("Accessing Subsurface Cloud Storage " + accessingCloud +"%", 30000); + } else { + hidePassiveNotification(); + } + } + + FontMetrics { + id: fontMetrics + } + + visible: false + opacity: 0 + + function returnTopPage() { + for (var i=stackView.depth; i>1; i--) { + stackView.pop() + } + detailsWindow.endEditMode() + } + + function scrollToTop() { + diveList.scrollToTop() + } + + function showMap(location) { + var urlPrefix = "https://www.google.com/maps/place/" + var locationPair = location + "/@" + location + var urlSuffix = ",5000m/data=!3m1!1e3!4m2!3m1!1s0x0:0x0" + Qt.openUrlExternally(urlPrefix + locationPair + urlSuffix) + + } + + function startAddDive() { + detailsWindow.state = "add" + detailsWindow.dive_id = manager.addDive(); + detailsWindow.number = manager.getNumber(detailsWindow.dive_id) + detailsWindow.date = manager.getDate(detailsWindow.dive_id) + detailsWindow.airtemp = "" + detailsWindow.watertemp = "" + detailsWindow.buddy = "" + detailsWindow.depth = "" + detailsWindow.divemaster = "" + detailsWindow.notes = "" + detailsWindow.location = "" + detailsWindow.duration = "" + detailsWindow.suit = "" + detailsWindow.weight = "" + detailsWindow.gasmix = "" + detailsWindow.startpressure = "" + detailsWindow.endpressure = "" + stackView.push(detailsWindow) + } + + globalDrawer: Kirigami.GlobalDrawer { + title: "Subsurface" + titleIcon: "qrc:/qml/subsurface-mobile-icon.png" + + bannerImageSource: "dive.jpg" + actions: [ + Kirigami.Action { + text: "Dive list" + onTriggered: { + manager.appendTextToLog("requested dive list with credential status " + manager.credentialStatus) + if (manager.credentialStatus == QMLManager.UNKNOWN) { + // the user has asked to change credentials - if the credentials before that + // were valid, go back to dive list + if (oldStatus == QMLManager.VALID || oldStatus == QMLManager.VALID_EMAIL) { + manager.credentialStatus = oldStatus + } + } + returnTopPage() + globalDrawer.close() + } + }, + Kirigami.Action { + text: "Cloud credentials" + onTriggered: { + returnTopPage() + oldStatus = manager.credentialStatus + if (diveList.numDives > 0) { + manager.startPageText = "Enter different credentials or return to dive list" + } else { + manager.startPageText = "Enter valid cloud storage credentials" + } + + manager.credentialStatus = QMLManager.UNKNOWN + } + }, + Kirigami.Action { + text: "Manage dives" + enabled: manager.credentialStatus === QMLManager.VALID || manager.credentialStatus === QMLManager.VALID_EMAIL + /* + * disable for the beta to avoid confusion + Action { + text: "Download from computer" + onTriggered: { + detailsWindow.endEditMode() + stackView.push(downloadDivesWindow) + } + } + */ + Kirigami.Action { + text: "Add dive manually" + onTriggered: { + returnTopPage() // otherwise odd things happen with the page stack + startAddDive() + } + } + Kirigami.Action { + text: "Manual sync with cloud" + onTriggered: { + globalDrawer.close() + detailsWindow.endEditMode() + manager.saveChanges(); + } + } + Kirigami.Action { + text: syncToCloud ? "Disable auto cloud sync" : "Enable auto cloud sync" + onTriggered: { + syncToCloud = !syncToCloud + if (!syncToCloud) { + var alertText = "Turning off automatic sync to cloud causes all data to only be stored locally.\n" + alertText += "This can be very useful in situations with limited or no network access.\n" + alertText += "Please chose 'Manual sync with cloud' if you have network connectivity\n" + alertText += "and want to sync your data to cloud storage." + showPassiveNotification(alertText, 10000) + } + } + } + }, + + Kirigami.Action { + text: "GPS" + enabled: manager.credentialStatus === QMLManager.VALID || manager.credentialStatus === QMLManager.VALID_EMAIL + Kirigami.Action { + text: "GPS-tag dives" + onTriggered: { + manager.applyGpsData(); + } + } + + Kirigami.Action { + text: "Upload GPS data" + onTriggered: { + manager.sendGpsData(); + } + } + + Kirigami.Action { + text: "Download GPS data" + onTriggered: { + manager.downloadGpsData(); + } + } + + Kirigami.Action { + text: "Show GPS fixes" + onTriggered: { + returnTopPage() + manager.populateGpsData(); + stackView.push(gpsWindow) + } + } + + Kirigami.Action { + text: "Clear GPS cache" + onTriggered: { + manager.clearGpsData(); + } + } + Kirigami.Action { + text: "Preferences" + onTriggered: { + stackView.push(prefsWindow) + detailsWindow.endEditMode() + } + } + }, + + Kirigami.Action { + text: "Developer" + Kirigami.Action { + text: "App log" + onTriggered: { + stackView.push(logWindow) + } + } + + Kirigami.Action { + text: "Theme information" + onTriggered: { + stackView.push(themetest) + } + } + }, + Kirigami.Action { + text: "User manual" + onTriggered: { + Qt.openUrlExternally("https://subsurface-divelog.org/documentation/subsurface-mobile-user-manual/") + } + }, + Kirigami.Action { + text: "About" + onTriggered: { + stackView.push(aboutWindow) + detailsWindow.endEditMode() + } + } + ] // end actions + + MouseArea { + height: childrenRect.height + width: Kirigami.Units.gridUnit * 10 + CheckBox { + //text: "Run location service" + id: locationCheckbox + anchors { + left: parent.left + top: parent.top + } + checked: manager.locationServiceEnabled + onCheckedChanged: { + manager.locationServiceEnabled = checked; + } + } + Kirigami.Label { + x: Kirigami.Units.gridUnit * 1.5 + anchors { + left: locationCheckbox.right + //leftMargin: units.smallSpacing + verticalCenter: locationCheckbox.verticalCenter + } + text: "Run location service" + } + onClicked: { + print("Click.") + locationCheckbox.checked = !locationCheckbox.checked + } + } + } + + contextDrawer: Kirigami.ContextDrawer { + id: contextDrawer + actions: rootItem.pageStack.currentPage ? rootItem.pageStack.currentPage.contextualActions : null + title: "Actions" + } + + QtObject { + id: subsurfaceTheme + property int titlePointSize: Math.round(fontMetrics.font.pointSize * 1.5) + property int smallPointSize: Math.round(fontMetrics.font.pointSize * 0.8) + property color accentColor: "#2d5b9a" + property color shadedColor: "#132744" + property color accentTextColor: "#ececec" + property color diveListTextColor: "#000000" // the Kirigami theme text color is too light + property int columnWidth: Math.round(rootItem.width/(Kirigami.Units.gridUnit*30)) > 0 ? Math.round(rootItem.width / Math.round(rootItem.width/(Kirigami.Units.gridUnit*30))) : rootItem.width + } +/* + toolBar: TopBar { + width: parent.width + height: Layout.minimumHeight + } + */ + + property Item stackView: pageStack + pageStack.initialPage: DiveList { + anchors.fill: detailsPage + id: diveList + opacity: 0 + Behavior on opacity { + NumberAnimation { + duration: 200 + easing.type: Easing.OutQuad + } + } + + } + + QMLManager { + id: manager + } + + Preferences { + id: prefsWindow + visible: false + } + + About { + id: aboutWindow + visible: false + } + + DiveDetails { + id: detailsWindow + visible: false + width: parent.width + height: parent.height + } + + DownloadFromDiveComputer { + id: downloadDivesWindow + visible: false + } + + Log { + id: logWindow + visible: false + } + + GpsList { + id: gpsWindow + visible: false + } + + ThemeTest { + id: themetest + visible: false + } + + Component.onCompleted: { + Kirigami.Theme.highlightColor = subsurfaceTheme.accentColor + manager.finishSetup(); + rootItem.visible = true + diveList.opacity = 1 + rootItem.opacity = 1 + } + Behavior on opacity { + NumberAnimation { + duration: 200 + easing.type: Easing.OutQuad + } + } +} diff --git a/mobile-widgets/qml/mobile-resources.qrc b/mobile-widgets/qml/mobile-resources.qrc new file mode 100644 index 000000000..e6c1fba65 --- /dev/null +++ b/mobile-widgets/qml/mobile-resources.qrc @@ -0,0 +1,66 @@ +<RCC> + <qresource prefix="/qml"> + <file>main.qml</file> + <file>TextButton.qml</file> + <file>Preferences.qml</file> + <file>About.qml</file> + <file>CloudCredentials.qml</file> + <file>DiveList.qml</file> + <file>DiveDetails.qml</file> + <file>DiveDetailsEdit.qml</file> + <file>DiveDetailsView.qml</file> + <file>DownloadFromDiveComputer.qml</file> + <file>GpsList.qml</file> + <file>Log.qml</file> + <file>TopBar.qml</file> + <file>ThemeTest.qml</file> + <file>StartPage.qml</file> + <file>dive.jpg</file> + <file>SubsurfaceButton.qml</file> + <file alias="subsurface-mobile-icon.png">../../icons/subsurface-mobile-icon.png</file> + <file alias="main-menu.png">icons/main-menu.png</file> + <file alias="context-menu.png">icons/context-menu.png</file> + <file alias="menu-edit.png">icons/menu-edit.png</file> + <file alias="menu-back.png">icons/menu-back.png</file> + </qresource> + <qresource prefix="/imports"> + <file alias="org/kde/kirigami/qmldir">kirigami/qmldir</file> + <file alias="org/kde/kirigami/Action.qml">kirigami/Action.qml</file> + <file alias="org/kde/kirigami/ApplicationWindow.qml">kirigami/ApplicationWindow.qml</file> + <file alias="org/kde/kirigami/BasicListItem.qml">kirigami/BasicListItem.qml</file> + <file alias="org/kde/kirigami/GlobalDrawer.qml">kirigami/GlobalDrawer.qml</file> + <file alias="org/kde/kirigami/ContextDrawer.qml">kirigami/ContextDrawer.qml</file> + <file alias="org/kde/kirigami/Page.qml">kirigami/Page.qml</file> + <file alias="org/kde/kirigami/ScrollablePage.qml">kirigami/ScrollablePage.qml</file> + <file alias="org/kde/kirigami/Icon.qml">kirigami/Icon.qml</file> + <file alias="org/kde/kirigami/Heading.qml">kirigami/Heading.qml</file> + <file alias="org/kde/kirigami/OverlaySheet.qml">kirigami/OverlaySheet.qml</file> + <file alias="org/kde/kirigami/ApplicationHeader.qml">kirigami/ApplicationHeader.qml</file> + <file alias="org/kde/kirigami/private/PageRow.qml">kirigami/private/PageRow.qml</file> + <file alias="org/kde/kirigami/Label.qml">kirigami/Label.qml</file> + <file alias="org/kde/kirigami/AbstractListItem.qml">kirigami/AbstractListItem.qml</file> + <file alias="org/kde/kirigami/SwipeListItem.qml">kirigami/SwipeListItem.qml</file> + <file alias="org/kde/kirigami/OverlayDrawer.qml">kirigami/OverlayDrawer.qml</file> + <file alias="org/kde/kirigami/Theme.qml">kirigami/Theme.qml</file> + <file alias="org/kde/kirigami/Units.qml">kirigami/Units.qml</file> + <file alias="org/kde/kirigami/private/RefreshableScrollView.qml">kirigami/private/RefreshableScrollView.qml</file> + <file alias="org/kde/kirigami/private/ActionButton.qml">kirigami/private/ActionButton.qml</file> + <file alias="org/kde/kirigami/private/BackButton.qml">kirigami/private/BackButton.qml</file> + <file alias="org/kde/kirigami/private/MenuIcon.qml">kirigami/private/MenuIcon.qml</file> + <file alias="org/kde/kirigami/private/ContextIcon.qml">kirigami/private/ContextIcon.qml</file> + <file alias="org/kde/kirigami/private/AbstractDrawer.qml">kirigami/private/AbstractDrawer.qml</file> + <file alias="org/kde/kirigami/private/PageStack.js">kirigami/private/PageStack.js</file> + <file alias="org/kde/kirigami/private/PassiveNotification.qml">kirigami/private/PassiveNotification.qml</file> + <file alias="org/kde/kirigami/icons/go-next.svg">kirigami/icons/go-next.svg</file> + <file alias="org/kde/kirigami/icons/go-previous.svg">kirigami/icons/go-previous.svg</file> + <file alias="org/kde/kirigami/icons/distribute-horizontal-x.svg">kirigami/icons/distribute-horizontal-x.svg</file> + <file alias="org/kde/kirigami/icons/document-edit.svg">kirigami/icons/document-edit.svg</file> + <file alias="org/kde/kirigami/icons/document-save.svg">kirigami/icons/document-save.svg</file> + <file alias="org/kde/kirigami/icons/view-readermode.svg">kirigami/icons/view-readermode.svg</file> + <file alias="org/kde/kirigami/icons/dialog-cancel.svg">kirigami/icons/dialog-cancel.svg</file> + <file alias="org/kde/kirigami/icons/application-menu.svg">kirigami/icons/application-menu.svg</file> + <file alias="org/kde/kirigami/icons/gps.svg">kirigami/icons/gps.svg</file> + <file alias="org/kde/kirigami/icons/trash-empty.svg">kirigami/icons/trash-empty.svg</file> + <file alias="org/kde/kirigami/icons/list-add.svg">kirigami/icons/list-add.svg</file> + </qresource> +</RCC> diff --git a/mobile-widgets/qml/theme/Theme.qml b/mobile-widgets/qml/theme/Theme.qml new file mode 100644 index 000000000..2c51ae00f --- /dev/null +++ b/mobile-widgets/qml/theme/Theme.qml @@ -0,0 +1,57 @@ +/* + * Copyright 2015 Marco Martin <mart@kde.org> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Library General Public License for more details + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import QtQuick 2.0 + +//pragma Singleton + +/*! + \qmltype Theme + \inqmlmodule Material 0.1 + + \brief Provides access to standard colors that follow the Material Design specification. + + See \l {http://www.google.com/design/spec/style/color.html#color-ui-color-application} for + details about choosing a color scheme for your application. + */ +QtObject { + id: theme + + property color textColor: Qt.rgba(0,0,0, 0.54) + + property color highlightColor: "#2196F3" + property color backgroundColor: "#f3f3f3" + property color linkColor: "#2196F3" + property color visitedLinkColor: "#2196F3" + + property color buttonTextColor: Qt.rgba(0,0,0, 0.54) + property color buttonBackgroundColor: "#f3f3f3" + property color buttonHoverColor: "#2196F3" + property color buttonFocusColor: "#2196F3" + + property color viewTextColor: Qt.rgba(0,0,0, 0.54) + property color viewBackgroundColor: "#f3f3f3" + property color viewHoverColor: "#2196F3" + property color viewFocusColor: "#2196F3" + + property color complementaryTextColor: "#f3f3f3" + property color complementaryBackgroundColor: Qt.rgba(0,0,0, 0.54) + property color complementaryHoverColor: "#2196F3" + property color complementaryFocusColor: "#2196F3" +} diff --git a/mobile-widgets/qml/theme/Units.qml b/mobile-widgets/qml/theme/Units.qml new file mode 100644 index 000000000..7cfa5c23b --- /dev/null +++ b/mobile-widgets/qml/theme/Units.qml @@ -0,0 +1,99 @@ +/* + * Copyright 2015 Marco Martin <mart@kde.org> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Library General Public License for more details + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import QtQuick 2.5 +import QtQuick.Window 2.2 + +//pragma Singleton + + +QtObject { + id: units + + /** + * The fundamental unit of space that should be used for sizes, expressed in pixels. + * Given the screen has an accurate DPI settings, it corresponds to a width of + * the capital letter M + */ + property int gridUnit: fontMetrics.height + + /** + * units.iconSizes provides access to platform-dependent icon sizing + * + * The icon sizes provided are normalized for different DPI, so icons + * will scale depending on the DPI. + * + * Icon sizes from KIconLoader, adjusted to devicePixelRatio: + * * small + * * smallMedium + * * medium + * * large + * * huge + * * enormous + * + * Not devicePixelRation-adjusted:: + * * desktop + */ + property QtObject iconSizes: QtObject { + property int small: 16 * devicePixelRatio + property int smallMedium: 22 * devicePixelRatio + property int medium: 32 * devicePixelRatio + property int large: 48 * devicePixelRatio + property int huge: 64 * devicePixelRatio + property int enormous: 128 * devicePixelRatio + } + + /** + * units.smallSpacing is the amount of spacing that should be used around smaller UI elements, + * for example as spacing in Columns. Internally, this size depends on the size of + * the default font as rendered on the screen, so it takes user-configured font size and DPI + * into account. + */ + property int smallSpacing: gridUnit/4 + + /** + * units.largeSpacing is the amount of spacing that should be used inside bigger UI elements, + * for example between an icon and the corresponding text. Internally, this size depends on + * the size of the default font as rendered on the screen, so it takes user-configured font + * size and DPI into account. + */ + property int largeSpacing: gridUnit + + /** + * The ratio between physical and device-independent pixels. This value does not depend on the \ + * size of the configured font. If you want to take font sizes into account when scaling elements, + * use theme.mSize(theme.defaultFont), units.smallSpacing and units.largeSpacing. + * The devicePixelRatio follows the definition of "device independent pixel" by Microsoft. + */ + property real devicePixelRatio: Screen.devicePixelRatio + + /** + * units.longDuration should be used for longer, screen-covering animations, for opening and + * closing of dialogs and other "not too small" animations + */ + property int longDuration: 250 + + /** + * units.shortDuration should be used for short animations, such as accentuating a UI event, + * hover events, etc.. + */ + property int shortDuration: 150 + + property QtObject fontMetrics: FontMetrics {} +} diff --git a/mobile-widgets/qml/theme/qmldir b/mobile-widgets/qml/theme/qmldir new file mode 100644 index 000000000..c654dbad6 --- /dev/null +++ b/mobile-widgets/qml/theme/qmldir @@ -0,0 +1,2 @@ +#singleton Units Units.qml +#//singleton Theme Theme.qml diff --git a/mobile-widgets/qmlmanager.cpp b/mobile-widgets/qmlmanager.cpp new file mode 100644 index 000000000..0bfde62aa --- /dev/null +++ b/mobile-widgets/qmlmanager.cpp @@ -0,0 +1,1078 @@ +#include "qmlmanager.h" +#include <QUrl> +#include <QSettings> +#include <QDebug> +#include <QNetworkAccessManager> +#include <QAuthenticator> +#include <QDesktopServices> +#include <QTextDocument> +#include <QRegularExpression> +#include <QApplication> +#include <QElapsedTimer> + +#include "qt-models/divelistmodel.h" +#include "qt-models/gpslistmodel.h" +#include "core/divelist.h" +#include "core/device.h" +#include "core/pref.h" +#include "core/qthelper.h" +#include "core/qt-gui.h" +#include "core/git-access.h" +#include "core/cloudstorage.h" + +QMLManager *QMLManager::m_instance = NULL; + +#define RED_FONT QLatin1Literal("<font color=\"red\">") +#define END_FONT QLatin1Literal("</font>") + +static void appendTextToLogStandalone(const char *text) +{ + QMLManager *self = QMLManager::instance(); + if (self) + self->appendTextToLog(QString(text)); +} + +extern "C" int gitProgressCB(int percent, const char *text) +{ + static QElapsedTimer timer; + static qint64 lastTime = 0; + static int lastPercent = -100; + + if (!timer.isValid() || percent == 0) { + timer.restart(); + lastTime = 0; + lastPercent = -100; + } + QMLManager *self = QMLManager::instance(); + if (self) { + qint64 elapsed = timer.elapsed(); + // don't show the same status twice in 200ms + if (percent == lastPercent && elapsed - lastTime < 200) + return 0; + self->loadDiveProgress(percent); + QString logText = QString::number(elapsed / 1000.0, 'f', 1) + " / " + QString::number((elapsed - lastTime) / 1000.0, 'f', 3) + + QString(" : git progress %1 (%2)").arg(percent).arg(text); + self->appendTextToLog(logText); + qDebug() << logText; + qApp->processEvents(); + qApp->flush(); + lastTime = elapsed; + } + // return 0 so that we don't end the download + return 0; +} + +QMLManager::QMLManager() : m_locationServiceEnabled(false), + m_verboseEnabled(false), + reply(0), + deletedDive(0), + deletedTrip(0), + m_credentialStatus(UNKNOWN), + m_lastDevicePixelRatio(1.0), + alreadySaving(false) +{ + m_instance = this; + connect(qobject_cast<QApplication *>(QApplication::instance()), &QApplication::applicationStateChanged, this, &QMLManager::applicationStateChanged); + appendTextToLog(getUserAgent()); + appendTextToLog(QStringLiteral("build with Qt Version %1, runtime from Qt Version %2").arg(QT_VERSION_STR).arg(qVersion())); + qDebug() << "Starting" << getUserAgent(); + qDebug() << QStringLiteral("build with Qt Version %1, runtime from Qt Version %2").arg(QT_VERSION_STR).arg(qVersion()); + setStartPageText(tr("Starting...")); + setAccessingCloud(-1); + setSyncToCloud(true); + // create location manager service + locationProvider = new GpsLocation(&appendTextToLogStandalone, this); + set_git_update_cb(&gitProgressCB); + + // make sure we know if the current cloud repo has been successfully synced + syncLoadFromCloud(); +} + +void QMLManager::applicationStateChanged(Qt::ApplicationState state) +{ + if (!timer.isValid()) + timer.start(); + QString stateText; + switch (state) { + case Qt::ApplicationActive: stateText = "active"; break; + case Qt::ApplicationHidden: stateText = "hidden"; break; + case Qt::ApplicationSuspended: stateText = "suspended"; break; + case Qt::ApplicationInactive: stateText = "inactive"; break; + default: stateText = QString("none of the four: 0x") + QString::number(state, 16); + } + stateText.prepend(QString::number(timer.elapsed() / 1000.0,'f', 3) + ": AppState changed to "); + stateText.append(" with "); + stateText.append((alreadySaving ? QLatin1Literal("") : QLatin1Literal("no ")) + QLatin1Literal("save ongoing")); + stateText.append(" and "); + stateText.append((unsaved_changes() ? QLatin1Literal("") : QLatin1Literal("no ")) + QLatin1Literal("unsaved changes")); + appendTextToLog(stateText); + qDebug() << stateText; + + if (!alreadySaving && state == Qt::ApplicationInactive && unsaved_changes()) { + // FIXME + // make sure the user sees that we are saving data if they come back + // while this is running + alreadySaving = true; + saveChanges(); + alreadySaving = false; + appendTextToLog(QString::number(timer.elapsed() / 1000.0,'f', 3) + ": done saving to git local / remote"); + mark_divelist_changed(false); + } +} + +void QMLManager::openLocalThenRemote(QString url) +{ + clear_dive_file_data(); + QByteArray fileNamePrt = QFile::encodeName(url); + bool glo = prefs.git_local_only; + prefs.git_local_only = true; + int error = parse_file(fileNamePrt.data()); + setAccessingCloud(-1); + prefs.git_local_only = glo; + if (error) { + appendTextToLog(QStringLiteral("loading dives from cache failed %1").arg(error)); + } else { + // if we can load from the cache, we know that we have at least a valid email + if (credentialStatus() == UNKNOWN) + setCredentialStatus(VALID_EMAIL); + prefs.unit_system = informational_prefs.unit_system; + if (informational_prefs.unit_system == IMPERIAL) + informational_prefs.units = IMPERIAL_units; + prefs.units = informational_prefs.units; + int i; + struct dive *d; + process_dives(false, false); + DiveListModel::instance()->clear(); + for_each_dive (i, d) { + DiveListModel::instance()->addDive(d); + } + appendTextToLog(QStringLiteral("%1 dives loaded from cache").arg(i)); + } + appendTextToLog(QStringLiteral("have cloud credentials, trying to connect")); + tryRetrieveDataFromBackend(); +} + +void QMLManager::finishSetup() +{ + // Initialize cloud credentials. + setCloudUserName(prefs.cloud_storage_email); + setCloudPassword(prefs.cloud_storage_password); + // if the cloud credentials are valid, we should get the GPS Webservice ID as well + QString url; + if (!cloudUserName().isEmpty() && + !cloudPassword().isEmpty() && + getCloudURL(url) == 0) { + openLocalThenRemote(url); + } else { + setCredentialStatus(INCOMPLETE); + appendTextToLog(QStringLiteral("no cloud credentials")); + setStartPageText(RED_FONT + tr("Please enter valid cloud credentials.") + END_FONT); + } + setDistanceThreshold(prefs.distance_threshold); + setTimeThreshold(prefs.time_threshold / 60); +} + +QMLManager::~QMLManager() +{ + m_instance = NULL; +} + +QMLManager *QMLManager::instance() +{ + return m_instance; +} + +void QMLManager::savePreferences() +{ + QSettings s; + s.beginGroup("LocationService"); + s.setValue("time_threshold", timeThreshold() * 60); + prefs.time_threshold = timeThreshold() * 60; + s.setValue("distance_threshold", distanceThreshold()); + prefs.distance_threshold = distanceThreshold(); + s.sync(); +} + +#define CLOUDURL QString(prefs.cloud_base_url) +#define CLOUDREDIRECTURL CLOUDURL + "/cgi-bin/redirect.pl" + +void QMLManager::saveCloudCredentials() +{ + QSettings s; + bool cloudCredentialsChanged = false; + s.beginGroup("CloudStorage"); + s.setValue("email", cloudUserName()); + s.setValue("password", cloudPassword()); + s.sync(); + if (!same_string(prefs.cloud_storage_email, qPrintable(cloudUserName()))) { + free(prefs.cloud_storage_email); + prefs.cloud_storage_email = strdup(qPrintable(cloudUserName())); + cloudCredentialsChanged = true; + } + + cloudCredentialsChanged |= !same_string(prefs.cloud_storage_password, qPrintable(cloudPassword())); + + if (!same_string(prefs.cloud_storage_password, qPrintable(cloudPassword()))) { + free(prefs.cloud_storage_password); + prefs.cloud_storage_password = strdup(qPrintable(cloudPassword())); + } + if (cloudUserName().isEmpty() || cloudPassword().isEmpty()) { + setStartPageText(RED_FONT + tr("Please enter valid cloud credentials.") + END_FONT); + } else if (cloudCredentialsChanged) { + free(prefs.userid); + prefs.userid = NULL; + syncLoadFromCloud(); + QString url; + getCloudURL(url); + manager()->clearAccessCache(); // remove any chached credentials + clear_git_id(); // invalidate our remembered GIT SHA + clear_dive_file_data(); + DiveListModel::instance()->clear(); + GpsListModel::instance()->clear(); + setStartPageText(tr("Attempting to open cloud storage with new credentials")); + openLocalThenRemote(url); + } +} + +void QMLManager::checkCredentialsAndExecute(execute_function_type execute) +{ + // if the cloud credentials are present, we should try to get the GPS Webservice ID + // and (if we haven't done so) load the dive list + if (!same_string(prefs.cloud_storage_email, "") && + !same_string(prefs.cloud_storage_password, "")) { + setAccessingCloud(0); + setStartPageText(tr("Testing cloud credentials")); + appendTextToLog("Have credentials, let's see if they are valid"); + connect(manager(), &QNetworkAccessManager::authenticationRequired, this, &QMLManager::provideAuth, Qt::UniqueConnection); + connect(manager(), &QNetworkAccessManager::finished, this, execute, Qt::UniqueConnection); + QUrl url(CLOUDREDIRECTURL); + request = QNetworkRequest(url); + request.setRawHeader("User-Agent", getUserAgent().toUtf8()); + request.setRawHeader("Accept", "text/html"); + reply = manager()->get(request); + connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(handleError(QNetworkReply::NetworkError))); + connect(reply, &QNetworkReply::sslErrors, this, &QMLManager::handleSslErrors); + } +} + +void QMLManager::tryRetrieveDataFromBackend() +{ + checkCredentialsAndExecute(&QMLManager::retrieveUserid); +} + +void QMLManager::provideAuth(QNetworkReply *reply, QAuthenticator *auth) +{ + if (auth->user() == QString(prefs.cloud_storage_email) && + auth->password() == QString(prefs.cloud_storage_password)) { + // OK, credentials have been tried and didn't work, so they are invalid + appendTextToLog("Cloud credentials are invalid"); + setStartPageText(RED_FONT + tr("Cloud credentials are invalid") + END_FONT); + setCredentialStatus(INVALID); + reply->disconnect(); + reply->abort(); + reply->deleteLater(); + return; + } + auth->setUser(prefs.cloud_storage_email); + auth->setPassword(prefs.cloud_storage_password); +} + +void QMLManager::handleSslErrors(const QList<QSslError> &errors) +{ + setStartPageText(RED_FONT + tr("Cannot open cloud storage: Error creating https connection") + END_FONT); + Q_FOREACH (QSslError e, errors) { + qDebug() << e.errorString(); + } + reply->abort(); + reply->deleteLater(); + setAccessingCloud(-1); +} + +void QMLManager::handleError(QNetworkReply::NetworkError nError) +{ + QString errorString = reply->errorString(); + qDebug() << "handleError" << nError << errorString; + setStartPageText(RED_FONT + tr("Cannot open cloud storage: %1").arg(errorString) + END_FONT); + reply->abort(); + reply->deleteLater(); + setAccessingCloud(-1); +} + +void QMLManager::retrieveUserid() +{ + if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute) != 302) { + appendTextToLog(QStringLiteral("Cloud storage connection not working correctly: %1").arg(QString(reply->readAll()))); + setAccessingCloud(-1); + return; + } + setCredentialStatus(VALID); + QString userid(prefs.userid); + if (userid.isEmpty()) { + if (same_string(prefs.cloud_storage_email, "") || same_string(prefs.cloud_storage_password, "")) { + appendTextToLog("cloud user name or password are empty, can't retrieve web user id"); + setAccessingCloud(-1); + return; + } + appendTextToLog(QStringLiteral("calling getUserid with user %1").arg(prefs.cloud_storage_email)); + userid = locationProvider->getUserid(prefs.cloud_storage_email, prefs.cloud_storage_password); + } + if (!userid.isEmpty()) { + // overwrite the existing userid + free(prefs.userid); + prefs.userid = strdup(qPrintable(userid)); + QSettings s; + s.setValue("subsurface_webservice_uid", prefs.userid); + s.sync(); + } + loadDivesWithValidCredentials(); +} + +void QMLManager::loadDiveProgress(int percent) +{ + QString text(tr("Loading dive list from cloud storage.")); + setAccessingCloud(percent); + while (percent > 0) { + text.append("."); + percent -= 10; + } + setStartPageText(text); +} + +void QMLManager::loadDivesWithValidCredentials() +{ + if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute) != 302) { + appendTextToLog(QStringLiteral("Cloud storage connection not working correctly: ") + reply->readAll()); + setStartPageText(RED_FONT + tr("Cannot connect to cloud storage") + END_FONT); + setAccessingCloud(-1); + return; + } + setCredentialStatus(VALID); + appendTextToLog("Cloud credentials valid, loading dives..."); + setStartPageText("Cloud credentials valid, loading dives..."); + git_storage_update_progress(0, "load dives with valid credentials"); + QString url; + if (getCloudURL(url)) { + QString errorString(get_error_string()); + appendTextToLog(errorString); + setStartPageText(RED_FONT + tr("Cloud storage error: %1").arg(errorString) + END_FONT); + setAccessingCloud(-1); + return; + } + QByteArray fileNamePrt = QFile::encodeName(url); + if (check_git_sha(fileNamePrt.data()) == 0) { + qDebug() << "local cache was current, no need to modify dive list"; + appendTextToLog("Cloud sync shows local cache was current"); + setLoadFromCloud(true); + setAccessingCloud(-1); + return; + } + clear_dive_file_data(); + DiveListModel::instance()->clear(); + + int error = parse_file(fileNamePrt.data()); + setAccessingCloud(-1); + if (!error) { + report_error("filename is now %s", fileNamePrt.data()); + const char *error_string = get_error_string(); + appendTextToLog(error_string); + set_filename(fileNamePrt.data(), true); + } else { + report_error("failed to open file %s", fileNamePrt.data()); + QString errorString(get_error_string()); + appendTextToLog(errorString); + setStartPageText(RED_FONT + tr("Cloud storage error: %1").arg(errorString) + END_FONT); + return; + } + prefs.unit_system = informational_prefs.unit_system; + if (informational_prefs.unit_system == IMPERIAL) + informational_prefs.units = IMPERIAL_units; + prefs.units = informational_prefs.units; + process_dives(false, false); + + int i; + struct dive *d; + + for_each_dive (i, d) { + DiveListModel::instance()->addDive(d); + } + appendTextToLog(QStringLiteral("%1 dives loaded").arg(i)); + if (dive_table.nr == 0) + setStartPageText(tr("Cloud storage open successfully. No dives in dive list.")); + setLoadFromCloud(true); +} + +void QMLManager::refreshDiveList() +{ + int i; + struct dive *d; + DiveListModel::instance()->clear(); + for_each_dive (i, d) { + DiveListModel::instance()->addDive(d); + } +} + +// update the dive and return the notes field, stripped of the HTML junk +void QMLManager::commitChanges(QString diveId, QString date, QString location, QString gps, QString duration, QString depth, + QString airtemp, QString watertemp, QString suit, QString buddy, QString diveMaster, QString weight, QString notes, + QString startpressure, QString endpressure, QString gasmix) +{ +#define DROP_EMPTY_PLACEHOLDER(_s) if ((_s) == QLatin1Literal("--")) (_s).clear() + + DROP_EMPTY_PLACEHOLDER(location); + DROP_EMPTY_PLACEHOLDER(duration); + DROP_EMPTY_PLACEHOLDER(depth); + DROP_EMPTY_PLACEHOLDER(airtemp); + DROP_EMPTY_PLACEHOLDER(watertemp); + DROP_EMPTY_PLACEHOLDER(suit); + DROP_EMPTY_PLACEHOLDER(buddy); + DROP_EMPTY_PLACEHOLDER(diveMaster); + DROP_EMPTY_PLACEHOLDER(weight); + DROP_EMPTY_PLACEHOLDER(gasmix); + DROP_EMPTY_PLACEHOLDER(startpressure); + DROP_EMPTY_PLACEHOLDER(endpressure); + DROP_EMPTY_PLACEHOLDER(notes); + +#undef DROP_EMPTY_PLACEHOLDER + + struct dive *d = get_dive_by_uniq_id(diveId.toInt()); + // notes comes back as rich text - let's convert this into plain text + QTextDocument doc; + doc.setHtml(notes); + notes = doc.toPlainText(); + + if (!d) { + qDebug() << "don't touch this... no dive"; + return; + } + bool diveChanged = false; + bool needResort = false; + + invalidate_dive_cache(d); + if (date != get_dive_date_string(d->when)) { + diveChanged = needResort = true; + QDateTime newDate; + // what a pain - Qt will not parse dates if the day of the week is incorrect + // so if the user changed the date but didn't update the day of the week (most likely behavior, actually), + // we need to make sure we don't try to parse that + QString format(QString(prefs.date_format) + QChar(' ') + prefs.time_format); + if (format.contains(QLatin1String("ddd")) || format.contains(QLatin1String("dddd"))) { + QString dateFormatToDrop = format.contains(QLatin1String("ddd")) ? QStringLiteral("ddd") : QStringLiteral("dddd"); + QDateTime ts; + QLocale loc = getLocale(); + ts.setMSecsSinceEpoch(d->when * 1000L); + QString drop = loc.toString(ts.toUTC(), dateFormatToDrop); + format.replace(dateFormatToDrop, ""); + date.replace(drop, ""); + } + newDate = QDateTime::fromString(date, format); + if (!newDate.isValid()) { + qDebug() << "unable to parse date" << date << "with the given format" << format; + QRegularExpression isoDate("\\d+-\\d+-\\d+[^\\d]+\\d+:\\d+"); + if (date.contains(isoDate)) { + newDate = QDateTime::fromString(date, "yyyy-M-d h:m:s"); + if (newDate.isValid()) + goto parsed; + newDate = QDateTime::fromString(date, "yy-M-d h:m:s"); + if (newDate.isValid()) + goto parsed; + } + QRegularExpression isoDateNoSecs("\\d+-\\d+-\\d+[^\\d]+\\d+"); + if (date.contains(isoDateNoSecs)) { + newDate = QDateTime::fromString(date, "yyyy-M-d h:m"); + if (newDate.isValid()) + goto parsed; + newDate = QDateTime::fromString(date, "yy-M-d h:m"); + if (newDate.isValid()) + goto parsed; + } + QRegularExpression usDate("\\d+/\\d+/\\d+[^\\d]+\\d+:\\d+:\\d+"); + if (date.contains(usDate)) { + newDate = QDateTime::fromString(date, "M/d/yyyy h:m:s"); + if (newDate.isValid()) + goto parsed; + newDate = QDateTime::fromString(date, "M/d/yy h:m:s"); + if (newDate.isValid()) + goto parsed; + newDate = QDateTime::fromString(date.toLower(), "M/d/yyyy h:m:sap"); + if (newDate.isValid()) + goto parsed; + newDate = QDateTime::fromString(date.toLower(), "M/d/yy h:m:sap"); + if (newDate.isValid()) + goto parsed; + } + QRegularExpression usDateNoSecs("\\d+/\\d+/\\d+[^\\d]+\\d+:\\d+"); + if (date.contains(usDateNoSecs)) { + newDate = QDateTime::fromString(date, "M/d/yyyy h:m"); + if (newDate.isValid()) + goto parsed; + newDate = QDateTime::fromString(date, "M/d/yy h:m"); + if (newDate.isValid()) + goto parsed; + newDate = QDateTime::fromString(date.toLower(), "M/d/yyyy h:map"); + if (newDate.isValid()) + goto parsed; + newDate = QDateTime::fromString(date.toLower(), "M/d/yy h:map"); + if (newDate.isValid()) + goto parsed; + } + QRegularExpression leDate("\\d+\\.\\d+\\.\\d+[^\\d]+\\d+:\\d+:\\d+"); + if (date.contains(leDate)) { + newDate = QDateTime::fromString(date, "d.M.yyyy h:m:s"); + if (newDate.isValid()) + goto parsed; + newDate = QDateTime::fromString(date, "d.M.yy h:m:s"); + if (newDate.isValid()) + goto parsed; + } + QRegularExpression leDateNoSecs("\\d+\\.\\d+\\.\\d+[^\\d]+\\d+:\\d+"); + if (date.contains(leDateNoSecs)) { + newDate = QDateTime::fromString(date, "d.M.yyyy h:m"); + if (newDate.isValid()) + goto parsed; + newDate = QDateTime::fromString(date, "d.M.yy h:m"); + if (newDate.isValid()) + goto parsed; + } + } +parsed: + if (newDate.isValid()) { + // stupid Qt... two digit years are always 19xx - WTF??? + // so if adding a hundred years gets you into something before a year from now... + // add a hundred years. + if (newDate.addYears(100) < QDateTime::currentDateTime().addYears(1)) + newDate = newDate.addYears(100); + d->dc.when = d->when = newDate.toMSecsSinceEpoch() / 1000 + gettimezoneoffset(newDate.toMSecsSinceEpoch() / 1000); + } else { + qDebug() << "none of our parsing attempts worked for the date string"; + } + } + struct dive_site *ds = get_dive_site_by_uuid(d->dive_site_uuid); + char *locationtext = NULL; + if (ds) + locationtext = ds->name; + if (!same_string(locationtext, qPrintable(location))) { + diveChanged = true; + // this is not ideal - and it's missing the gps information + // but for now let's just create a new dive site + ds = get_dive_site_by_uuid(create_dive_site(qPrintable(location), d->when)); + d->dive_site_uuid = ds->uuid; + } + if (!gps.isEmpty()) { + QString gpsString = getCurrentPosition(); + if (gpsString != QString("waiting for the next gps location")) { + qDebug() << "from commitChanges call to getCurrentPosition returns" << gpsString; + double lat, lon; + if (parseGpsText(qPrintable(gpsString), &lat, &lon)) { + struct dive_site *ds = get_dive_site_by_uuid(d->dive_site_uuid); + if (ds) { + ds->latitude.udeg = lat * 1000000; + ds->longitude.udeg = lon * 1000000; + } else { + degrees_t latData, lonData; + latData.udeg = lat; + lonData.udeg = lon; + d->dive_site_uuid = create_dive_site_with_gps("new site", latData, lonData, d->when); + } + qDebug() << "set up dive site with new GPS data"; + } + } else { + qDebug() << "still don't have a position - will need to implement some sort of callback"; + } + } + if (get_dive_duration_string(d->duration.seconds, tr("h:"), tr("min")) != duration) { + diveChanged = true; + int h = 0, m = 0, s = 0; + QRegExp r1(QStringLiteral("(\\d*)\\s*%1[\\s,:]*(\\d*)\\s*%2[\\s,:]*(\\d*)\\s*%3").arg(tr("h")).arg(tr("min")).arg(tr("sec")), Qt::CaseInsensitive); + QRegExp r2(QStringLiteral("(\\d*)\\s*%1[\\s,:]*(\\d*)\\s*%2").arg(tr("h")).arg(tr("min")), Qt::CaseInsensitive); + QRegExp r3(QStringLiteral("(\\d*)\\s*%1").arg(tr("min")), Qt::CaseInsensitive); + QRegExp r4(QStringLiteral("(\\d*):(\\d*):(\\d*)")); + QRegExp r5(QStringLiteral("(\\d*):(\\d*)")); + QRegExp r6(QStringLiteral("(\\d*)")); + if (r1.indexIn(duration) >= 0) { + h = r1.cap(1).toInt(); + m = r1.cap(2).toInt(); + s = r1.cap(3).toInt(); + } else if (r2.indexIn(duration) >= 0) { + h = r2.cap(1).toInt(); + m = r2.cap(2).toInt(); + } else if (r3.indexIn(duration) >= 0) { + m = r3.cap(1).toInt(); + } else if (r4.indexIn(duration) >= 0) { + h = r4.cap(1).toInt(); + m = r4.cap(2).toInt(); + s = r4.cap(3).toInt(); + } else if (r5.indexIn(duration) >= 0) { + h = r5.cap(1).toInt(); + m = r5.cap(2).toInt(); + } else if (r6.indexIn(duration) >= 0) { + m = r6.cap(1).toInt(); + } + d->dc.duration.seconds = d->duration.seconds = h * 3600 + m * 60 + s; + if (same_string(d->dc.model, "manually added dive")) { + free(d->dc.sample); + d->dc.sample = 0; + d->dc.samples = 0; + } else { + qDebug() << "changing the duration on a dive that wasn't manually added - Uh-oh"; + } + + } + if (get_depth_string(d->maxdepth.mm, true, true) != depth) { + int depthValue = parseLengthToMm(depth); + // the QML code should stop negative depth, but massively huge depth can make + // the profile extremely slow or even run out of memory and crash, so keep + // the depth <= 500m + if (0 <= depthValue && depthValue <= 500000) { + diveChanged = true; + d->maxdepth.mm = depthValue; + if (same_string(d->dc.model, "manually added dive")) { + d->dc.maxdepth.mm = d->maxdepth.mm; + free(d->dc.sample); + d->dc.sample = 0; + d->dc.samples = 0; + } + } + } + if (get_temperature_string(d->airtemp, true) != airtemp) { + diveChanged = true; + d->airtemp.mkelvin = parseTemperatureToMkelvin(airtemp); + } + if (get_temperature_string(d->watertemp, true) != watertemp) { + diveChanged = true; + d->watertemp.mkelvin = parseTemperatureToMkelvin(watertemp); + } + // not sure what we'd do if there was more than one weight system + // defined - for now just ignore that case + if (weightsystem_none((void *)&d->weightsystem[1])) { + if (get_weight_string(d->weightsystem[0].weight, true) != weight) { + diveChanged = true; + d->weightsystem[0].weight.grams = parseWeightToGrams(weight); + } + } + // start and end pressures for first cylinder only + if (get_pressure_string(d->cylinder[0].start, true) != startpressure || get_pressure_string(d->cylinder[0].end, true) != endpressure) { + diveChanged = true; + d->cylinder[0].start.mbar = parsePressureToMbar(startpressure); + d->cylinder[0].end.mbar = parsePressureToMbar(endpressure); + if (d->cylinder[0].end.mbar > d->cylinder[0].start.mbar) + d->cylinder[0].end.mbar = d->cylinder[0].start.mbar; + } + // gasmix for first cylinder + if (get_gas_string(d->cylinder[0].gasmix) != gasmix) { + int o2 = parseGasMixO2(gasmix); + int he = parseGasMixHE(gasmix); + // the QML code SHOULD only accept valid gas mixes, but just to make sure + if (o2 >= 0 && o2 <= 1000 && + he >= 0 && he <= 1000 && + o2 + he <= 1000) { + diveChanged = true; + d->cylinder[0].gasmix.o2.permille = o2; + d->cylinder[0].gasmix.he.permille = he; + } + } + if (!same_string(d->suit, qPrintable(suit))) { + diveChanged = true; + free(d->suit); + d->suit = strdup(qPrintable(suit)); + } + if (!same_string(d->buddy, qPrintable(buddy))) { + diveChanged = true; + free(d->buddy); + d->buddy = strdup(qPrintable(buddy)); + } + if (!same_string(d->divemaster, qPrintable(diveMaster))) { + diveChanged = true; + free(d->divemaster); + d->divemaster = strdup(qPrintable(diveMaster)); + } + if (!same_string(d->notes, qPrintable(notes))) { + diveChanged = true; + free(d->notes); + d->notes = strdup(qPrintable(notes)); + } + // now that we have it all figured out, let's see what we need + // to update + DiveListModel *dm = DiveListModel::instance(); + int oldModelIdx = dm->getDiveIdx(d->id); + int oldIdx = get_idx_by_uniq_id(d->id); + if (needResort) { + // we know that the only thing that might happen in a resort is that + // this one dive moves to a different spot in the dive list + sort_table(&dive_table); + int newIdx = get_idx_by_uniq_id(d->id); + if (newIdx != oldIdx) { + DiveObjectHelper *newDive = new DiveObjectHelper(d); + DiveListModel::instance()->removeDive(oldModelIdx); + DiveListModel::instance()->insertDive(oldModelIdx - (newIdx - oldIdx), newDive); + diveChanged = false; // because we already modified things + } + } + if (diveChanged) { + if (d->maxdepth.mm == d->dc.maxdepth.mm && + d->maxdepth.mm > 0 && + same_string(d->dc.model, "manually added dive") && + d->dc.samples == 0) { + // so we have depth > 0, a manually added dive and no samples + // let's create an actual profile so the desktop version can work it + // first clear out the mean depth (or the fake_dc() function tries + // to be too clever + d->meandepth.mm = d->dc.meandepth.mm = 0; + d->dc = *fake_dc(&d->dc, true); + } + DiveListModel::instance()->updateDive(oldModelIdx, d); + } + if (diveChanged || needResort) + // we no longer save right away, but only the next time the app is not + // in the foreground (or when explicitly requested) + mark_divelist_changed(true); + +} + +void QMLManager::saveChanges() +{ + if (!loadFromCloud()) { + appendTextToLog("Don't save dives without loading from the cloud, first."); + return; + } + appendTextToLog("Saving dives."); + git_storage_update_progress(0, "saveChanges"); // reset the timers + QString fileName; + if (getCloudURL(fileName)) { + appendTextToLog(get_error_string()); + return; + } + if (prefs.git_local_only == false) { + setAccessingCloud(0); + qApp->processEvents(); // make sure that the notification is actually shown + } + if (save_dives(fileName.toUtf8().data())) { + appendTextToLog(get_error_string()); + setAccessingCloud(-1); + return; + } + setAccessingCloud(-1); + appendTextToLog("Updated dive list saved."); + set_filename(fileName.toUtf8().data(), true); + mark_divelist_changed(false); +} + +void QMLManager::undoDelete(int id) +{ + if (!deletedDive || deletedDive->id != id) { + qDebug() << "can't find the deleted dive"; + return; + } + if (deletedTrip) + insert_trip(&deletedTrip); + if (deletedDive->divetrip) { + struct dive_trip *trip = deletedDive->divetrip; + tripflag_t tripflag = deletedDive->tripflag; // this gets overwritten in add_dive_to_trip() + deletedDive->divetrip = NULL; + deletedDive->next = NULL; + deletedDive->pprev = NULL; + add_dive_to_trip(deletedDive, trip); + deletedDive->tripflag = tripflag; + } + record_dive(deletedDive); + DiveListModel::instance()->addDive(deletedDive); + // make sure the changes get saved if the app is no longer in the foreground + // or if the user requests a save + mark_divelist_changed(true); + deletedDive = NULL; + deletedTrip = NULL; +} + +void QMLManager::deleteDive(int id) +{ + struct dive *d = get_dive_by_uniq_id(id); + if (!d) { + qDebug() << "oops, trying to delete non-existing dive"; + return; + } + // clean up (or create) the storage for the deleted dive and trip (if applicable) + if (!deletedDive) + deletedDive = alloc_dive(); + else + clear_dive(deletedDive); + copy_dive(d, deletedDive); + if (!deletedTrip) { + deletedTrip = (struct dive_trip *)calloc(1, sizeof(struct dive_trip)); + } else { + free(deletedTrip->location); + free(deletedTrip->notes); + memset(deletedTrip, 0, sizeof(struct dive_trip)); + } + // if this is the last dive in that trip, remember the trip as well + if (d->divetrip && d->divetrip->nrdives == 1) { + *deletedTrip = *d->divetrip; + deletedTrip->location = copy_string(d->divetrip->location); + deletedTrip->notes = copy_string(d->divetrip->notes); + deletedTrip->nrdives = 0; + deletedDive->divetrip = deletedTrip; + } + DiveListModel::instance()->removeDiveById(id); + delete_single_dive(get_idx_by_uniq_id(id)); + // make sure the changes get saved if the app is no longer in the foreground + // or if the user requests a save + mark_divelist_changed(true); +} + +QString QMLManager::addDive() +{ + appendTextToLog("Adding new dive."); + return DiveListModel::instance()->startAddDive(); +} + +void QMLManager::addDiveAborted(int id) +{ + DiveListModel::instance()->removeDiveById(id); +} + +QString QMLManager::getCurrentPosition() +{ + return locationProvider->currentPosition(); +} + +void QMLManager::applyGpsData() +{ + if (locationProvider->applyLocations()) + refreshDiveList(); +} + +void QMLManager::sendGpsData() +{ + locationProvider->uploadToServer(); +} + +void QMLManager::downloadGpsData() +{ + locationProvider->downloadFromServer(); + populateGpsData(); +} + +void QMLManager::populateGpsData() +{ + if (GpsListModel::instance()) + GpsListModel::instance()->update(); +} + +void QMLManager::clearGpsData() +{ + locationProvider->clearGpsData(); + populateGpsData(); +} + +void QMLManager::deleteGpsFix(quint64 when) +{ + locationProvider->deleteGpsFix(when); + populateGpsData(); +} + + +QString QMLManager::logText() const +{ + QString logText = m_logText + QString("\nNumer of GPS fixes: %1").arg(locationProvider->getGpsNum()); + return logText; +} + +void QMLManager::setLogText(const QString &logText) +{ + m_logText = logText; + emit logTextChanged(); +} + +void QMLManager::appendTextToLog(const QString &newText) +{ + m_logText += "\n" + newText; + emit logTextChanged(); +} + +bool QMLManager::locationServiceEnabled() const +{ + return m_locationServiceEnabled; +} + +void QMLManager::setLocationServiceEnabled(bool locationServiceEnabled) +{ + m_locationServiceEnabled = locationServiceEnabled; + locationProvider->serviceEnable(m_locationServiceEnabled); +} + +bool QMLManager::verboseEnabled() const +{ + return m_verboseEnabled; +} + +void QMLManager::setVerboseEnabled(bool verboseMode) +{ + m_verboseEnabled = verboseMode; + verbose = verboseMode; + qDebug() << "verbose is" << verbose; + emit verboseEnabledChanged(); +} + +QString QMLManager::cloudPassword() const +{ + return m_cloudPassword; +} + +void QMLManager::setCloudPassword(const QString &cloudPassword) +{ + m_cloudPassword = cloudPassword; + emit cloudPasswordChanged(); +} + +QString QMLManager::cloudUserName() const +{ + return m_cloudUserName; +} + +void QMLManager::setCloudUserName(const QString &cloudUserName) +{ + m_cloudUserName = cloudUserName.toLower(); + emit cloudUserNameChanged(); +} + +int QMLManager::distanceThreshold() const +{ + return m_distanceThreshold; +} + +void QMLManager::setDistanceThreshold(int distance) +{ + m_distanceThreshold = distance; + emit distanceThresholdChanged(); +} + +int QMLManager::timeThreshold() const +{ + return m_timeThreshold; +} + +void QMLManager::setTimeThreshold(int time) +{ + m_timeThreshold = time; + emit timeThresholdChanged(); +} + +bool QMLManager::loadFromCloud() const +{ + return m_loadFromCloud; +} + +void QMLManager::syncLoadFromCloud() +{ + QSettings s; + QString cloudMarker = QLatin1Literal("loadFromCloud") + QString(prefs.cloud_storage_email); + m_loadFromCloud = s.contains(cloudMarker) && s.value(cloudMarker).toBool(); +} + +void QMLManager::setLoadFromCloud(bool done) +{ + QSettings s; + QString cloudMarker = QLatin1Literal("loadFromCloud") + QString(prefs.cloud_storage_email); + s.setValue(cloudMarker, done); + m_loadFromCloud = done; + emit loadFromCloudChanged(); +} + +QString QMLManager::startPageText() const +{ + return m_startPageText; +} + +void QMLManager::setStartPageText(const QString& text) +{ + m_startPageText = text; + emit startPageTextChanged(); +} + +// this is an enum, but I don't know how to do enums in QML +QMLManager::credentialStatus_t QMLManager::credentialStatus() const +{ + return m_credentialStatus; +} + +void QMLManager::setCredentialStatus(const credentialStatus_t value) +{ + if (m_credentialStatus != value) { + m_credentialStatus = value; + emit credentialStatusChanged(); + } +} + +// where in the QML dive list is that dive? +int QMLManager::getIndex(const QString &diveId) +{ + int dive_id = diveId.toInt(); + int idx = DiveListModel::instance()->getDiveIdx(dive_id); + return idx; +} + +QString QMLManager::getNumber(const QString& diveId) +{ + int dive_id = diveId.toInt(); + struct dive *d = get_dive_by_uniq_id(dive_id); + QString number; + if (d) + number = QString::number(d->number); + return number; +} + +QString QMLManager::getDate(const QString& diveId) +{ + int dive_id = diveId.toInt(); + struct dive *d = get_dive_by_uniq_id(dive_id); + QString datestring; + if (d) + datestring = get_dive_date_string(d->when); + return datestring; +} + +QString QMLManager::getVersion() const +{ + QRegExp versionRe(".*:([()\\.,\\d]+).*"); + if (!versionRe.exactMatch(getUserAgent())) + return QString(); + + return versionRe.cap(1); +} + +int QMLManager::accessingCloud() const +{ + return m_accessingCloud; +} + +void QMLManager::setAccessingCloud(int status) +{ + m_accessingCloud = status; + emit accessingCloudChanged(); +} + +bool QMLManager::syncToCloud() const +{ + return m_syncToCloud; +} + +void QMLManager::setSyncToCloud(bool status) +{ + m_syncToCloud = status; + prefs.git_local_only = !status; + prefs.cloud_background_sync = status; + QSettings s; + s.beginGroup("CloudStorage"); + s.setValue("git_local_only", prefs.git_local_only); + s.setValue("cloud_background_sync", prefs.cloud_background_sync); + emit syncToCloudChanged(); +} + +qreal QMLManager::lastDevicePixelRatio() +{ + return m_lastDevicePixelRatio; +} + +void QMLManager::screenChanged(QScreen *screen) +{ + m_lastDevicePixelRatio = screen->devicePixelRatio(); + emit sendScreenChanged(screen); +} diff --git a/mobile-widgets/qmlmanager.h b/mobile-widgets/qmlmanager.h new file mode 100644 index 000000000..bd55f68e4 --- /dev/null +++ b/mobile-widgets/qmlmanager.h @@ -0,0 +1,162 @@ +#ifndef QMLMANAGER_H +#define QMLMANAGER_H + +#include <QObject> +#include <QString> +#include <QNetworkAccessManager> +#include <QScreen> +#include <QElapsedTimer> + +#include "core/gpslocation.h" + +class QMLManager : public QObject { + Q_OBJECT + Q_ENUMS(credentialStatus_t) + Q_PROPERTY(QString cloudUserName READ cloudUserName WRITE setCloudUserName NOTIFY cloudUserNameChanged) + Q_PROPERTY(QString cloudPassword READ cloudPassword WRITE setCloudPassword NOTIFY cloudPasswordChanged) + Q_PROPERTY(QString logText READ logText WRITE setLogText NOTIFY logTextChanged) + Q_PROPERTY(bool locationServiceEnabled READ locationServiceEnabled WRITE setLocationServiceEnabled NOTIFY locationServiceEnabledChanged) + Q_PROPERTY(int distanceThreshold READ distanceThreshold WRITE setDistanceThreshold NOTIFY distanceThresholdChanged) + Q_PROPERTY(int timeThreshold READ timeThreshold WRITE setTimeThreshold NOTIFY timeThresholdChanged) + Q_PROPERTY(bool loadFromCloud READ loadFromCloud WRITE setLoadFromCloud NOTIFY loadFromCloudChanged) + Q_PROPERTY(QString startPageText READ startPageText WRITE setStartPageText NOTIFY startPageTextChanged) + Q_PROPERTY(bool verboseEnabled READ verboseEnabled WRITE setVerboseEnabled NOTIFY verboseEnabledChanged) + Q_PROPERTY(credentialStatus_t credentialStatus READ credentialStatus WRITE setCredentialStatus NOTIFY credentialStatusChanged) + Q_PROPERTY(int accessingCloud READ accessingCloud WRITE setAccessingCloud NOTIFY accessingCloudChanged) + Q_PROPERTY(bool syncToCloud READ syncToCloud WRITE setSyncToCloud NOTIFY syncToCloudChanged) + +public: + QMLManager(); + ~QMLManager(); + + enum credentialStatus_t { + INCOMPLETE, + UNKNOWN, + INVALID, + VALID_EMAIL, + VALID + }; + + static QMLManager *instance(); + + QString cloudUserName() const; + void setCloudUserName(const QString &cloudUserName); + + QString cloudPassword() const; + void setCloudPassword(const QString &cloudPassword); + + bool locationServiceEnabled() const; + void setLocationServiceEnabled(bool locationServiceEnable); + + bool verboseEnabled() const; + void setVerboseEnabled(bool verboseMode); + + int distanceThreshold() const; + void setDistanceThreshold(int distance); + + int timeThreshold() const; + void setTimeThreshold(int time); + + bool loadFromCloud() const; + void setLoadFromCloud(bool done); + void syncLoadFromCloud(); + + QString startPageText() const; + void setStartPageText(const QString& text); + + credentialStatus_t credentialStatus() const; + void setCredentialStatus(const credentialStatus_t value); + + QString logText() const; + void setLogText(const QString &logText); + + int accessingCloud() const; + void setAccessingCloud(int status); + + bool syncToCloud() const; + void setSyncToCloud(bool status); + + typedef void (QMLManager::*execute_function_type)(); + +public slots: + void applicationStateChanged(Qt::ApplicationState state); + void savePreferences(); + void saveCloudCredentials(); + void checkCredentialsAndExecute(execute_function_type execute); + void tryRetrieveDataFromBackend(); + void handleError(QNetworkReply::NetworkError nError); + void handleSslErrors(const QList<QSslError> &errors); + void retrieveUserid(); + void loadDivesWithValidCredentials(); + void loadDiveProgress(int percent); + void provideAuth(QNetworkReply *reply, QAuthenticator *auth); + void commitChanges(QString diveId, QString date, QString location, + QString gps, QString duration, QString depth, + QString airtemp, QString watertemp, QString suit, + QString buddy, QString diveMaster, QString weight, QString notes, + QString startpressure, QString endpressure, QString gasmix); + + void saveChanges(); + void deleteDive(int id); + void undoDelete(int id); + QString addDive(); + void addDiveAborted(int id); + void applyGpsData(); + void sendGpsData(); + void downloadGpsData(); + void populateGpsData(); + void clearGpsData(); + void finishSetup(); + void openLocalThenRemote(QString url); + int getIndex(const QString& diveId); + QString getNumber(const QString& diveId); + QString getDate(const QString& diveId); + QString getCurrentPosition(); + QString getVersion() const; + void deleteGpsFix(quint64 when); + void refreshDiveList(); + void screenChanged(QScreen *screen); + qreal lastDevicePixelRatio(); + void appendTextToLog(const QString &newText); + +private: + QString m_cloudUserName; + QString m_cloudPassword; + QString m_ssrfGpsWebUserid; + QString m_startPageText; + QString m_logText; + bool m_locationServiceEnabled; + bool m_verboseEnabled; + int m_distanceThreshold; + int m_timeThreshold; + GpsLocation *locationProvider; + bool m_loadFromCloud; + static QMLManager *m_instance; + QNetworkReply *reply; + QNetworkRequest request; + struct dive *deletedDive; + struct dive_trip *deletedTrip; + int m_accessingCloud; + bool m_syncToCloud; + credentialStatus_t m_credentialStatus; + qreal m_lastDevicePixelRatio; + QElapsedTimer timer; + bool alreadySaving; + +signals: + void cloudUserNameChanged(); + void cloudPasswordChanged(); + void locationServiceEnabledChanged(); + void verboseEnabledChanged(); + void logTextChanged(); + void timeThresholdChanged(); + void distanceThresholdChanged(); + void loadFromCloudChanged(); + void startPageTextChanged(); + void credentialStatusChanged(); + void accessingCloudChanged(); + void syncToCloudChanged(); + void sendScreenChanged(QScreen *screen); +}; + +#endif diff --git a/mobile-widgets/qmlprofile.cpp b/mobile-widgets/qmlprofile.cpp new file mode 100644 index 000000000..b023741ef --- /dev/null +++ b/mobile-widgets/qmlprofile.cpp @@ -0,0 +1,111 @@ +#include "qmlprofile.h" +#include "qmlmanager.h" +#include "profile-widget/profilewidget2.h" +#include "core/dive.h" +#include "core/metrics.h" +#include <QTransform> +#include <QScreen> + +QMLProfile::QMLProfile(QQuickItem *parent) : + QQuickPaintedItem(parent), + m_devicePixelRatio(1.0), + m_margin(0) +{ + setAntialiasing(true); + m_profileWidget = new ProfileWidget2(0); + m_profileWidget->setProfileState(); + m_profileWidget->setPrintMode(true); + m_profileWidget->setFontPrintScale(0.8); + connect(QMLManager::instance(), &QMLManager::sendScreenChanged, this, &QMLProfile::screenChanged); + setDevicePixelRatio(QMLManager::instance()->lastDevicePixelRatio()); +} + +QMLProfile::~QMLProfile() +{ + m_profileWidget->deleteLater(); +} + +void QMLProfile::paint(QPainter *painter) +{ + // let's look at the intended size of the content and scale our scene accordingly + QRect painterRect = painter->viewport(); + QRect profileRect = m_profileWidget->viewport()->rect(); + // qDebug() << "profile viewport and painter viewport" << profileRect << painterRect; + qreal sceneSize = 104; // that should give us 2% margin all around (100x100 scene) + qreal dprComp = devicePixelRatio() * painterRect.width() / profileRect.width(); + qreal sx = painterRect.width() / sceneSize / dprComp; + qreal sy = painterRect.height() / sceneSize / dprComp; + + // next figure out the weird magic by which we need to shift the painter so the profile is shown + int dpr = rint(devicePixelRatio()); + qreal magicShiftFactor = (dpr == 2 ? 0.25 : (dpr == 3 ? 0.33 : 0.0)); + + // now set up the transformations scale the profile and + // shift the painter (taking its existing transformation into account) + QTransform profileTransform = QTransform(); + profileTransform.scale(sx, sy); + QTransform painterTransform = painter->transform(); + painterTransform.translate(-painterRect.width() * magicShiftFactor ,-painterRect.height() * magicShiftFactor); + +#if PROFILE_SCALING_DEBUG + // some debugging messages to help adjust this in case the magic above is insufficient + QMLManager::instance()->appendTextToLog(QString("dpr %1 profile viewport %2 %3 painter viewport %4 %5").arg(dpr).arg(profileRect.width()).arg(profileRect.height()) + .arg(painterRect.width()).arg(painterRect.height())); + QMLManager::instance()->appendTextToLog(QString("profile matrix %1 %2 %3 %4 %5 %6 %7 %8 %9").arg(profileTransform.m11()).arg(profileTransform.m12()).arg(profileTransform.m13()) + .arg(profileTransform.m21()).arg(profileTransform.m22()).arg(profileTransform.m23()) + .arg(profileTransform.m31()).arg(profileTransform.m32()).arg(profileTransform.m33())); + QMLManager::instance()->appendTextToLog(QString("painter matrix %1 %2 %3 %4 %5 %6 %7 %8 %9").arg(painterTransform.m11()).arg(painterTransform.m12()).arg(painterTransform.m13()) + .arg(painterTransform.m21()).arg(painterTransform.m22()).arg(painterTransform.m23()) + .arg(painterTransform.m31()).arg(painterTransform.m32()).arg(painterTransform.m33())); + qDebug() << "profile scaled by" << profileTransform.m11() << profileTransform.m22() << "and translated by" << profileTransform.m31() << profileTransform.m32(); + qDebug() << "exist profile transform" << m_profileWidget->transform() << "painter transform" << painter->transform(); +#endif + // apply the transformation + painter->setTransform(painterTransform); + m_profileWidget->setTransform(profileTransform); + + // finally, render the profile + m_profileWidget->render(painter); +} + +void QMLProfile::setMargin(int margin) +{ + m_margin = margin; +} + +QString QMLProfile::diveId() const +{ + return m_diveId; +} + +void QMLProfile::setDiveId(const QString &diveId) +{ + m_diveId = diveId; + struct dive *d = get_dive_by_uniq_id(m_diveId.toInt()); + if (m_diveId.toInt() < 1) + return; + if (!d) + return; + qDebug() << "setDiveId called with valid dive" << d->number; + m_profileWidget->plotDive(d, true); +} + +qreal QMLProfile::devicePixelRatio() const +{ + return m_devicePixelRatio; +} + +void QMLProfile::setDevicePixelRatio(qreal dpr) +{ + if (dpr != m_devicePixelRatio) { + m_devicePixelRatio = dpr; + m_profileWidget->setFontPrintScale(0.8 * dpr); + updateDevicePixelRatio(dpr); + emit devicePixelRatioChanged(); + } +} + +void QMLProfile::screenChanged(QScreen *screen) +{ + setDevicePixelRatio(screen->devicePixelRatio()); +} diff --git a/mobile-widgets/qmlprofile.h b/mobile-widgets/qmlprofile.h new file mode 100644 index 000000000..c8a77d700 --- /dev/null +++ b/mobile-widgets/qmlprofile.h @@ -0,0 +1,40 @@ +#ifndef QMLPROFILE_H +#define QMLPROFILE_H + +#include <QQuickPaintedItem> + +class ProfileWidget2; + +class QMLProfile : public QQuickPaintedItem +{ + Q_OBJECT + Q_PROPERTY(QString diveId READ diveId WRITE setDiveId NOTIFY diveIdChanged) + Q_PROPERTY(qreal devicePixelRatio READ devicePixelRatio WRITE setDevicePixelRatio NOTIFY devicePixelRatioChanged) + +public: + explicit QMLProfile(QQuickItem *parent = 0); + virtual ~QMLProfile(); + + void paint(QPainter *painter); + + QString diveId() const; + void setDiveId(const QString &diveId); + qreal devicePixelRatio() const; + void setDevicePixelRatio(qreal dpr); + +public slots: + void setMargin(int margin); + void screenChanged(QScreen *screen); +private: + QString m_diveId; + qreal m_devicePixelRatio; + int m_margin; + ProfileWidget2 *m_profileWidget; + +signals: + void rightAlignedChanged(); + void diveIdChanged(); + void devicePixelRatioChanged(); +}; + +#endif // QMLPROFILE_H |