774 lines
25 KiB
QML
774 lines
25 KiB
QML
/*
|
|
Speed Climbing Stopwatch - Simple Stopwatch for Climbers
|
|
Copyright (C) 2018 Itsblue Development - Dorian Zeder
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU Affero General Public License as published
|
|
by the Free Software Foundation, version 3 of the License.
|
|
|
|
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 Affero General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
import QtQuick 2.9
|
|
import QtMultimedia 5.8
|
|
import QtQuick.Window 2.2
|
|
import QtQuick.Controls 2.2
|
|
import QtGraphicalEffects 1.0
|
|
import "."
|
|
import "./components"
|
|
//import QtQuick.Layouts 1.11
|
|
|
|
import com.itsblue.speedclimbingstopwatch 2.0
|
|
|
|
Window {
|
|
visible: true
|
|
width: 540
|
|
height: 960
|
|
title: qsTr("Speedclimbing stw")
|
|
property date currentTime: new Date()
|
|
property int millis: 0
|
|
|
|
Page {
|
|
id:root
|
|
anchors.fill: parent
|
|
|
|
//set default state to IDLE
|
|
state: "IDLE"
|
|
|
|
Rectangle {
|
|
id: backgroundRect
|
|
anchors.fill: parent
|
|
color: appTheme.style.backgroundColor
|
|
|
|
Behavior on color {
|
|
ColorAnimation {
|
|
duration: 200
|
|
}
|
|
}
|
|
}
|
|
|
|
SpeedBackend {
|
|
id: speedBackend
|
|
|
|
onStateChanged: {
|
|
var stateString
|
|
console.log("race state changed to: " + state)
|
|
switch (state){
|
|
case 0:
|
|
stateString = "IDLE"
|
|
break;
|
|
case 1:
|
|
stateString = "STARTING"
|
|
settingsDialog.close()
|
|
break;
|
|
case 2:
|
|
stateString = "WAITING"
|
|
settingsDialog.close()
|
|
break;
|
|
case 3:
|
|
stateString = "RUNNING"
|
|
settingsDialog.close()
|
|
break;
|
|
case 4:
|
|
stateString = "STOPPED"
|
|
settingsDialog.close()
|
|
}
|
|
root.state = stateString
|
|
}
|
|
}
|
|
|
|
AppTheme {
|
|
id: appTheme
|
|
}
|
|
|
|
/*------------------------
|
|
Timer text an upper line
|
|
------------------------*/
|
|
RectangularGlow {
|
|
id: effect_2
|
|
glowRadius: 7
|
|
spread: 0.02
|
|
color: "black"
|
|
opacity: 0.18
|
|
anchors.fill: topContainerItm
|
|
scale: 1
|
|
}
|
|
|
|
Item {
|
|
id: topContainerItm
|
|
|
|
anchors {
|
|
top: parent.top
|
|
left: parent.left
|
|
right: root.landscape() ? startButt.left:parent.right
|
|
bottom: root.landscape() ? parent.bottom:startButt.top
|
|
bottomMargin: root.landscape() ? undefined:parent.height * 0.1
|
|
rightMargin: root.landscape() ? parent.width * 0.05:0
|
|
}
|
|
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
color: appTheme.style.menuColor
|
|
|
|
Behavior on color {
|
|
ColorAnimation {
|
|
duration: 200
|
|
}
|
|
}
|
|
}
|
|
|
|
Text {
|
|
id: topLa
|
|
|
|
anchors.centerIn: parent
|
|
|
|
opacity: ( speedBackend.state < 3 ) ? 1:0
|
|
|
|
width: parent.width * 0.7
|
|
|
|
text: qsTr("Click Start to start")
|
|
|
|
color: appTheme.style.textColor
|
|
|
|
fontSizeMode: Text.Fit
|
|
|
|
verticalAlignment: Text.AlignVCenter
|
|
horizontalAlignment: Text.AlignHCenter
|
|
|
|
font.pixelSize: root.landscape() ? parent.width * 0.15 : parent.height * 0.4
|
|
|
|
minimumPixelSize: 0
|
|
|
|
Behavior on text {
|
|
FadeAnimation{
|
|
target: topLa
|
|
fadeDuration: 100
|
|
}
|
|
}
|
|
}
|
|
|
|
Column {
|
|
id: timerCol
|
|
|
|
anchors.fill: parent
|
|
|
|
opacity: ( speedBackend.state < 3 ) ? 0:1
|
|
|
|
spacing: height * 0.05
|
|
|
|
Repeater {
|
|
id: timerRep
|
|
|
|
model: speedBackend.timers.length
|
|
|
|
delegate: Item {
|
|
id: timerDel
|
|
|
|
width: parent.width
|
|
height: timerRep.model > 1 ? ( timerCol.height * 0.9 ) / timerRep.model:timerCol.height
|
|
|
|
Label {
|
|
id: timerTextLa
|
|
|
|
anchors.centerIn: parent
|
|
|
|
width: ( parent.width * 0.8 )
|
|
height: parent.height
|
|
|
|
elide: "ElideRight"
|
|
color: appTheme.style.textColor
|
|
|
|
text: speedBackend.timers[index]["text"]
|
|
|
|
fontSizeMode: Text.Fit
|
|
|
|
verticalAlignment: Text.AlignVCenter
|
|
horizontalAlignment: Text.AlignHCenter
|
|
|
|
font.pixelSize: root.landscape() ? parent.width * 0.15 : parent.height * 0.4
|
|
|
|
minimumPixelSize: 0
|
|
|
|
Behavior on text {
|
|
enabled: root.state !== "RUNNING"
|
|
FadeAnimation {
|
|
target: timerTextLa
|
|
}
|
|
}
|
|
}
|
|
|
|
Label {
|
|
id: react_time
|
|
|
|
property int rtime: speedBackend.timers[index]["reacttime"]
|
|
|
|
anchors {
|
|
centerIn: parent
|
|
verticalCenterOffset: parent.height * 0.25
|
|
}
|
|
|
|
width: ( parent.width * 0.6 )
|
|
height: parent.height
|
|
|
|
fontSizeMode: Text.Fit
|
|
|
|
verticalAlignment: Text.AlignVCenter
|
|
horizontalAlignment: Text.AlignHCenter
|
|
|
|
text: qsTr("reaction time (ms): ") + Math.round(rtime)
|
|
|
|
opacity: (root.state === "RUNNING" || root.state === "STOPPED") && rtime !== 0 ? 1:0
|
|
|
|
color: appTheme.style.textColor
|
|
|
|
font.pixelSize: timerTextLa.font.pixelSize * 0.5
|
|
}
|
|
}
|
|
}
|
|
|
|
Behavior on opacity {
|
|
NumberAnimation {
|
|
duration: 200
|
|
}
|
|
}
|
|
}
|
|
|
|
Behavior on opacity {
|
|
NumberAnimation {
|
|
duration: 200
|
|
}
|
|
}
|
|
}
|
|
|
|
Item {
|
|
id: connectionIconContainer
|
|
|
|
anchors {
|
|
top: parent.top
|
|
left: parent.left
|
|
right: parent.right
|
|
bottom: parent.bottom
|
|
bottomMargin: root.landscape() ? 0:parent.height * 0.8
|
|
rightMargin: root.landscape() ? parent.width * 0.8:0
|
|
}
|
|
|
|
ConnectionIcon {
|
|
id: baseConnConnIcon
|
|
status: speedBackend.baseStationState
|
|
|
|
source: appTheme.style.baseStationIcon
|
|
anchors {
|
|
top: parent.top
|
|
topMargin: 10
|
|
left: parent.left
|
|
leftMargin: 10
|
|
}
|
|
scale: 1.3
|
|
height: !root.landscape()? parent.height*0.3:parent.width*0.3
|
|
}
|
|
|
|
Row {
|
|
id: connectedExtensionsRow
|
|
|
|
anchors {
|
|
top: parent.top
|
|
topMargin: 10
|
|
left: baseConnConnIcon.right
|
|
leftMargin: 1
|
|
}
|
|
|
|
height: parent.height
|
|
width: parent.width
|
|
|
|
Repeater {
|
|
id: connectedExtensionsRep
|
|
anchors.fill: parent
|
|
model: speedBackend.baseStationConnections.length
|
|
delegate: ConnectionIcon {
|
|
id: buzzerConnIcon
|
|
status: speedBackend.baseStationConnections[index]["state"]
|
|
|
|
source: {
|
|
var source
|
|
switch(speedBackend.baseStationConnections[index]["type"]){
|
|
case "STARTPAD":
|
|
source = appTheme.style.startpadIcon
|
|
break
|
|
case "TOPPAD":
|
|
source = appTheme.style.buzzerIcon
|
|
break
|
|
}
|
|
}
|
|
|
|
scale: 0
|
|
|
|
height: !root.landscape()? parent.height*0.17:parent.width*0.17
|
|
width: status === "disconnected" ? 0:height
|
|
|
|
Component.onCompleted: {
|
|
scale = 1
|
|
}
|
|
|
|
Behavior on scale {
|
|
NumberAnimation {
|
|
duration: 200
|
|
}
|
|
}
|
|
|
|
Behavior on width {
|
|
NumberAnimation {
|
|
duration: 200
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
id: upper_line
|
|
width: root.landscape() ? 1:parent.width
|
|
height: root.landscape() ? parent.height:1
|
|
color: appTheme.style.lineColor
|
|
anchors.left: root.landscape() ? topContainerItm.right:parent.left
|
|
anchors.top: root.landscape() ? parent.top:topContainerItm.bottom
|
|
anchors.bottom: root.landscape() ? parent.bottom:undefined
|
|
visible: false
|
|
}
|
|
|
|
// ----------------------------------
|
|
// -- Start / Stop / Reset button ---
|
|
// ----------------------------------
|
|
FancyButton {
|
|
id : startButt
|
|
|
|
text: qsTr("start")
|
|
property int size: root.landscape() ? parent.width * 0.5:parent.height * 0.5
|
|
anchors {
|
|
bottom: parent.bottom
|
|
bottomMargin: root.height * 0.5 - height * 0.5
|
|
right: parent.right
|
|
rightMargin: root.width * 0.5 - width * 0.5
|
|
}
|
|
contentItem: Text {
|
|
//make text disappear
|
|
}
|
|
height: root.landscape() ? (size > parent.height * 0.9 ? parent.height * 0.9:size) : (size > parent.width * 0.9 ? parent.width * 0.9:size)
|
|
width: height
|
|
|
|
Label {
|
|
id: startButt_text
|
|
text: startButt.text
|
|
anchors.centerIn: parent
|
|
font.pixelSize: parent.height * 0.16
|
|
font.family: "Helvetica"
|
|
color: enabled ? appTheme.style.textColor:appTheme.style.disabledTextColor
|
|
}
|
|
|
|
backgroundColor: appTheme.style.buttonColor
|
|
|
|
Behavior on text {
|
|
//animate a text change
|
|
enabled: true
|
|
FadeAnimation {
|
|
target: startButt_text
|
|
}
|
|
}
|
|
|
|
onClicked: {
|
|
switch(root.state) {
|
|
case "IDLE":
|
|
root.start()
|
|
break
|
|
case "RUNNING":
|
|
root.stop(0)
|
|
break
|
|
case "STOPPED":
|
|
root.reset()
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
ProgressCircle {
|
|
id: prog
|
|
anchors.fill: startButt
|
|
opacity: root.state === "STARTING" || root.state === "IDLE" ? 1:0
|
|
|
|
scale: startButt.scale
|
|
|
|
lineWidth: 5
|
|
|
|
arcBegin: 0
|
|
arcEnd: 360 * speedBackend.nextStartActionDelayProgress
|
|
|
|
colorCircle: "grey"
|
|
|
|
Behavior on opacity {
|
|
NumberAnimation {
|
|
duration: 200
|
|
}
|
|
}
|
|
|
|
animationDuration: speedBackend.mode === 1 ? 150:0
|
|
}
|
|
|
|
/*----------------------
|
|
Cancel button
|
|
----------------------*/
|
|
FancyButton {
|
|
id: cancelButt
|
|
|
|
text: qsTr("cancel")
|
|
anchors {
|
|
right: startButt.right
|
|
bottom: startButt.bottom
|
|
}
|
|
contentItem: Text {
|
|
//make text disappear
|
|
}
|
|
height: startButt.height * 0.3
|
|
scale: 0
|
|
width: height
|
|
|
|
enabled: root.state === "STARTING"
|
|
|
|
onClicked: {
|
|
root.stop(1)
|
|
}
|
|
|
|
Behavior on scale {
|
|
PropertyAnimation {
|
|
duration: 200
|
|
}
|
|
}
|
|
|
|
Label {
|
|
id: cancelButt_text
|
|
text: cancelButt.text
|
|
anchors.centerIn: parent
|
|
font.pixelSize: parent.height * 0.16
|
|
font.family: "Helvetica"
|
|
color: appTheme.style.textColor
|
|
}
|
|
|
|
backgroundColor: appTheme.style.buttonColor
|
|
}
|
|
|
|
/*------
|
|
Popups
|
|
------*/
|
|
SettingsDialog{
|
|
id: settingsDialog
|
|
connections: root.connections
|
|
onConnectRequested: {
|
|
switch(type){
|
|
case "buzzer":
|
|
buzzerConn.connect()
|
|
break
|
|
case "startpad":
|
|
startpadConn.connect()
|
|
break
|
|
case "baseStation":
|
|
baseConn.connectToHost()
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
ProfilesDialog {
|
|
id: profilesDialog
|
|
|
|
property int margin: root.landscape() ? root.height * 0.05:root.width * 0.05
|
|
|
|
x: root.landscape() ? topContainerItm.width + margin:topContainerItm.x + margin
|
|
y: !root.landscape() ? topContainerItm.height + margin:topContainerItm.x + margin
|
|
width: root.landscape() ? root.width - topContainerItm.width - menu_container.width - margin * 2 : root.width - margin * 2
|
|
height: !root.landscape() ? root.height - topContainerItm.height - menu_container.height - margin * 2 : root.height - margin * 2
|
|
|
|
|
|
}
|
|
|
|
/*-------------------
|
|
lower line and menu
|
|
-------------------*/
|
|
|
|
Rectangle {
|
|
id: lowerLine
|
|
width: root.landscape() ? 1:parent.width
|
|
height: root.landscape() ? parent.height:1
|
|
color: appTheme.style.lineColor
|
|
anchors.right: root.landscape() ? menu_container.left:parent.right
|
|
anchors.bottom: root.landscape() ? parent.bottom:menu_container.top
|
|
anchors.top: root.landscape() ? parent.top:undefined
|
|
visible: false
|
|
}
|
|
|
|
RectangularGlow {
|
|
id: effect
|
|
glowRadius: 7
|
|
spread: 0.02
|
|
color: "black"
|
|
opacity: 0.18
|
|
anchors.fill: menu_container
|
|
scale: 1
|
|
}
|
|
|
|
Item {
|
|
id: menu_container
|
|
|
|
anchors {
|
|
bottom: parent.bottom
|
|
right: parent.right
|
|
left: root.landscape() ? startButt.right:parent.left
|
|
top: root.landscape() ? parent.top:startButt.bottom
|
|
topMargin: root.landscape() ? undefined:parent.height * 0.1
|
|
leftMargin: root.landscape() ? parent.width * 0.05:0
|
|
}
|
|
|
|
Rectangle {
|
|
id: lowerMenuBackground
|
|
anchors.fill: parent
|
|
color: appTheme.style.menuColor
|
|
|
|
Behavior on color {
|
|
ColorAnimation {
|
|
duration: 200
|
|
}
|
|
}
|
|
}
|
|
|
|
Grid {
|
|
id: loweMenuGrd
|
|
|
|
property int spacingMultiplier: 200 * (getActiveChildren() - 1)
|
|
property int activeChildren: getActiveChildren()
|
|
|
|
function getActiveChildren() {
|
|
var childrenCount = 0
|
|
for (var i = 0; i < children.length; i++)
|
|
{
|
|
if(children[i].enabled){
|
|
childrenCount ++
|
|
}
|
|
}
|
|
|
|
return childrenCount
|
|
}
|
|
|
|
anchors.centerIn: parent
|
|
|
|
height: childrenRect.height
|
|
width: childrenRect.width
|
|
|
|
rows: root.landscape() ? activeChildren:1
|
|
columns: root.landscape() ? 1:activeChildren
|
|
|
|
spacing: 0// root.landscape() ? parent.height * spacingMultiplier * 0.001:parent.width * spacingMultiplier * 0.001
|
|
|
|
Behavior on spacingMultiplier {
|
|
NumberAnimation {
|
|
duration: 200
|
|
}
|
|
}
|
|
|
|
FancyButton {
|
|
id: settingsButt
|
|
|
|
height: root.landscape() ? menu_container.width * 0.7:menu_container.height * 0.7
|
|
width: height
|
|
|
|
onClicked: {
|
|
settingsDialog.open()
|
|
}
|
|
|
|
image: appTheme.style.settIcon
|
|
|
|
backgroundColor: parent.pressed ? appTheme.style.buttonPressedColor:appTheme.style.buttonColor
|
|
|
|
}
|
|
|
|
Item {
|
|
height: profilesButt.height
|
|
width: profilesButt.height
|
|
}
|
|
|
|
FancyButton {
|
|
id: profilesButt
|
|
|
|
enabled: height > 0
|
|
|
|
height: speedBackend.baseStationState === "connected" ? root.landscape() ? menu_container.width * 0.7:menu_container.height * 0.7:0
|
|
width: height
|
|
|
|
onClicked: {
|
|
profilesDialog.open()
|
|
}
|
|
|
|
image: appTheme.style.profilesIcon
|
|
|
|
backgroundColor: parent.pressed ? appTheme.style.buttonPressedColor:appTheme.style.buttonColor
|
|
|
|
Behavior on height {
|
|
NumberAnimation {
|
|
duration: 200
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
}
|
|
|
|
/*----------------------
|
|
Timer states
|
|
----------------------*/
|
|
states: [
|
|
State {
|
|
name: "IDLE"
|
|
//state for the start page
|
|
PropertyChanges {
|
|
target: topContainerItm;
|
|
anchors.bottomMargin: root.landscape() ? undefined:parent.height * 0.1;
|
|
anchors.rightMargin: root.landscape() ? parent.height * 0.05:0
|
|
}
|
|
PropertyChanges {
|
|
target: startButt;
|
|
enabled: true; text: qsTr("start");
|
|
size: root.landscape() ? parent.width * 0.5:parent.height * 0.5
|
|
anchors.bottomMargin: parent.height * 0.5 - startButt.height * 0.5
|
|
anchors.rightMargin: parent.width * 0.5 - startButt.width * 0.5
|
|
}
|
|
PropertyChanges {
|
|
target: topLa
|
|
text: qsTr("Click Start to start")
|
|
}
|
|
|
|
},
|
|
State {
|
|
name: "WAITING"
|
|
//state when a false start occured and waiting for time calculation
|
|
PropertyChanges {
|
|
target: startButt; enabled: false; text: qsTr("waiting...");
|
|
anchors.rightMargin: root.landscape() ? parent.width * 0.05:parent.width * 0.5 - startButt.width * 0.5 //put the button more to the right to hide the menu (only in landscape mode)
|
|
anchors.bottomMargin: root.landscape() ? parent.height * 0.5 - startButt.height * 0.5:parent.height * 0.1 //put the button lower to hide the menu (only in portrait mode)
|
|
}
|
|
PropertyChanges { target: cancelButt; scale: 0; enabled: false}
|
|
PropertyChanges { target: menu_container; }
|
|
PropertyChanges {
|
|
target: topLa
|
|
text: qsTr("please wait...")
|
|
}
|
|
},
|
|
State {
|
|
name: "STARTING"
|
|
//state for the start sequence
|
|
PropertyChanges { target: startButt; enabled: false; text: qsTr("starting...");
|
|
anchors.rightMargin: root.landscape() ? parent.width * 0.05:parent.width * 0.5 - startButt.width * 0.5 //put the button more to the right to hide the menu (only in landscape mode)
|
|
anchors.bottomMargin: root.landscape() ? parent.height * 0.5 - startButt.height * 0.5:parent.height * 0.1 //put the button lower to hide the menu (only in portrait mode)
|
|
}
|
|
PropertyChanges { target: cancelButt; scale: 1}
|
|
PropertyChanges { target: menu_container; }
|
|
PropertyChanges {
|
|
target: topLa
|
|
text: qsTr("starting...")
|
|
}
|
|
|
|
},
|
|
State {
|
|
name: "RUNNING"
|
|
//state when the timer is running
|
|
PropertyChanges { target: startButt; enabled: true;
|
|
text: "stop"
|
|
anchors.rightMargin: root.landscape() ? parent.width * 0.05:parent.width * 0.5 - startButt.width * 0.5 //put the button more to the right to hide the menu (only in landscape mode)
|
|
anchors.bottomMargin: root.landscape() ? parent.height * 0.5 - startButt.height * 0.5:parent.height * 0.1 //put the button lower to hide the menu (only in portrait mode)
|
|
}
|
|
|
|
},
|
|
|
|
State {
|
|
name: "STOPPED"
|
|
//state when the meassuring is over
|
|
PropertyChanges {
|
|
target: startButt;
|
|
enabled: true; text: qsTr("reset");
|
|
size: root.landscape() ? parent.height * 0.35:parent.height * 0.2;
|
|
anchors.bottomMargin: root.landscape() ? parent.height * 0.5 - startButt.height * 0.5:parent.height * 0.2 - startButt.height * 0.5
|
|
anchors.rightMargin: root.landscape() ? parent.height * 0.2 - startButt.height * 0.5:parent.width * 0.5 - startButt.width * 0.5
|
|
}
|
|
PropertyChanges {
|
|
target: topContainerItm;
|
|
anchors.rightMargin: root.landscape() ? 0-startButt.width/2:undefined
|
|
anchors.bottomMargin: root.landscape() ? undefined:0-startButt.height/2
|
|
}
|
|
}
|
|
]
|
|
|
|
/*----------------------
|
|
Timer animations
|
|
----------------------*/
|
|
transitions: [
|
|
Transition {
|
|
NumberAnimation { properties: "size,rightMargin,height,width,bottomMargin,font.pixelSize,pixelSize"; easing.type: Easing.InOutQuad; duration: 700 }
|
|
},
|
|
Transition {
|
|
to: "STOPPED"
|
|
NumberAnimation { properties: "size,rightMargin,height,width,bottomMargin,font.pixelSize,pixelSize"; easing.type: Easing.InOutQuad; duration: 700 }
|
|
},
|
|
Transition {
|
|
to: "IDLE"
|
|
NumberAnimation { properties: "size,rightMargin,height,width,bottomMargin,font.pixelSize,pixelSize"; easing.type: Easing.InOutQuad; duration: 700 }
|
|
},
|
|
|
|
Transition {
|
|
from: "STARTING"
|
|
to: "RUNNING"
|
|
//disable transitions for the RUNNING state
|
|
},
|
|
Transition {
|
|
from: "RUNNING"
|
|
to: "WAITING"
|
|
//disable transitions for the RUNNING state
|
|
}
|
|
]
|
|
|
|
/*----------------------
|
|
Timer functions
|
|
----------------------*/
|
|
function landscape(){
|
|
return(root.height < root.width)
|
|
}
|
|
|
|
/*----Functions to control the stopwatch----*/
|
|
function start(){
|
|
var ret = speedBackend.startRace()
|
|
if(ret !== 200){
|
|
console.log("+ --- error starting race: "+ret)
|
|
}
|
|
}
|
|
|
|
function stop(type){
|
|
|
|
var ret = speedBackend.stopRace(type)
|
|
|
|
if(ret !== 200){
|
|
console.log("+ --- error stopping race: "+ret)
|
|
}
|
|
}
|
|
|
|
function reset(){
|
|
|
|
var ret = speedBackend.resetRace()
|
|
|
|
if(ret !== 200){
|
|
console.log("+ --- error resetting race: "+ret)
|
|
}
|
|
}
|
|
}
|
|
}
|