/* 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 . */ 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 "./ProfilesDialog" import "./SettingsDialog" //import QtQuick.Layouts 1.11 import de.itsblue.ScStw 2.0 import de.itsblue.ScStw.Styling 2.0 import de.itsblue.ScStw.Styling.Components 1.0 import de.itsblue.ScStwApp 2.0 Window { visible: true width: 540 height: 960 title: "Speedclimbing stw" property date currentTime: new Date() property int millis: 0 Page { id:app anchors.fill: parent //set default state to IDLE state: "IDLE" Rectangle { id: backgroundRect anchors.fill: parent color: appTheme.theme.colors.background Behavior on color { ColorAnimation { duration: 200 } } } ScStw { id: scStw } SpeedBackend { id: speedBackend } Connections { target: speedBackend.race onStateChanged: { var stateString console.log("race state changed to: " + speedBackend.race.state) switch (speedBackend.race.state){ case ScStwRace.IDLE: stateString = "IDLE" break; case ScStwRace.STARTING: stateString = "STARTING" settingsDialog.close() profilesDialog.close() break; case ScStwRace.WAITING: stateString = "WAITING" settingsDialog.close() profilesDialog.close() break; case ScStwRace.RUNNING: stateString = "RUNNING" settingsDialog.close() profilesDialog.close() break; case ScStwRace.STOPPED: stateString = "STOPPED" settingsDialog.close() profilesDialog.close() } app.state = stateString } } ScStwAppThemeManager { id: appTheme Component.onCompleted: { appTheme.setTheme(speedBackend.readSetting("theme")) } } /*------------------------ 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: app.landscape() ? startButt.left:parent.right bottom: app.landscape() ? parent.bottom:startButt.top bottomMargin: app.landscape() ? undefined:parent.height * 0.1 rightMargin: app.landscape() ? parent.width * 0.05:0 } Rectangle { anchors.fill: parent color: appTheme.theme.colors.menu Behavior on color { ColorAnimation { duration: 200 } } } Text { id: topLa property string implicitText: "" anchors.centerIn: parent opacity: ( speedBackend.race.state < ScStwRace.RUNNING ) ? 1:0 width: parent.width * 0.7 height: parent.height * 0.7 text: implicitText === "NEXT_START_ACTION" ? ["", "at your \nmarks", "ready", "starting..."][speedBackend.race.nextStartActionDetails[ScStwRace.NextStartAction]+1]:implicitText color: appTheme.theme.colors.text fontSizeMode: Text.Fit verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignHCenter font.pixelSize: app.landscape() ? parent.width * 0.15 : parent.height * 0.4 minimumPixelSize: 1 Behavior on text { FadeAnimation{ target: topLa fadeDuration: 100 } } } TimerColumn { anchors.fill: parent anchors.topMargin: app.landscape() ? 0:parent.height * 0.1 anchors.bottomMargin: app.landscape() ? 0:parent.height * 0.1 timers: speedBackend.race.timers colors: appTheme.theme.colors fontName: appTheme.theme.fonts.timers showTimerLetter: speedBackend.race.state === ScStwRace.STOPPED // make text smaller for much better performance textScale: 0.7 opacity: ( speedBackend.race.state < ScStwRace.RUNNING ) ? 0:1 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: app.landscape() ? 0:parent.height * 0.8 rightMargin: app.landscape() ? parent.width * 0.8:0 } opacity: speedBackend.race.state === ScStwRace.IDLE ? 1:0 ConnectionIcon { id: baseConnConnIcon function clientStateToString(state) { switch(state) { case ScStwClient.DISCONNECTED: return "disconnected" case ScStwClient.CONNECTING: return "connecting" case ScStwClient.INITIALISING: return "connecting" case ScStwClient.CONNECTED: return "connected" } } status: clientStateToString(speedBackend.scStwClient.state) source: appTheme.theme.images.baseStationIcon anchors { top: parent.top topMargin: 10 left: parent.left leftMargin: 10 } scale: 1.3 height: !app.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.scStwClient.extensions.length delegate: ConnectionIcon { id: buzzerConnIcon status: speedBackend.scStwClient.extensions[index]["state"] source: { var source switch(speedBackend.scStwClient.extensions[index]["type"]){ case "STARTPAD": source = appTheme.theme.images.startpadIcon break case "TOPPAD": source = appTheme.theme.images.buzzerIcon break } } scale: 0 height: !app.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 } } } } } Behavior on opacity { NumberAnimation { duration: 200 } } } Rectangle { id: upper_line width: app.landscape() ? 1:parent.width height: app.landscape() ? parent.height:1 color: appTheme.theme.colors.line anchors.left: app.landscape() ? topContainerItm.right:parent.left anchors.top: app.landscape() ? parent.top:topContainerItm.bottom anchors.bottom: app.landscape() ? parent.bottom:undefined visible: false } // ---------------------------------- // -- Start / Stop / Reset button --- // ---------------------------------- DelayButton { id : startButt text: "start" property int size: app.landscape() ? parent.width * 0.5:parent.height * 0.5 property color backgroundColor: appTheme.theme.colors.button property bool progressControlActivated: speedBackend.scStwClient.state === ScStwClient.CONNECTED && app.state === "RUNNING" delay: progressControlActivated ? 2000:0 anchors { bottom: parent.bottom bottomMargin: app.height * 0.5 - height * 0.5 right: parent.right rightMargin: app.width * 0.5 - width * 0.5 } height: app.landscape() ? Math.min(size, parent.height * 0.9) : Math.min(size, parent.width * 0.9) width: height Text { id: startButt_text text: startButt.text anchors.centerIn: parent font.pixelSize: parent.height * 0.16 font.family: "Helvetica" color: enabled ? appTheme.theme.colors.text:appTheme.theme.colors.disabledText } Behavior on text { //animate a text change enabled: true FadeAnimation { target: startButt_text } } onClicked: { if(startButt.progressControlActivated && progress < 1.0) return startButt.progress = 0 switch(app.state) { case "IDLE": app.start() break case "RUNNING": app.stop() break case "STOPPED": app.reset() break } } contentItem: Text { } background: Item { RectangularGlow { glowRadius: 0.001 spread: 0.2 color: "black" visible: true cornerRadius: startButtBackground.radius anchors.fill: startButtBackground scale: 0.75 opacity: Math.pow( startButt.opacity, 100 ) } Rectangle { id: startButtBackground implicitWidth: 100 implicitHeight: 100 color: startButt.down ? Qt.darker(startButt.backgroundColor, 1.2) : startButt.backgroundColor radius: size / 2 readonly property real size: Math.min(startButt.width, startButt.height) width: size height: size anchors.fill: parent Behavior on color { ColorAnimation { duration: 200 } } Canvas { id: canvas anchors.fill: parent visible: startButt.progressControlActivated Connections { target: startButt onProgressChanged: canvas.requestPaint() } onPaint: { var ctx = getContext("2d") ctx.clearRect(0, 0, width, height) ctx.strokeStyle = "grey" ctx.lineWidth = parent.width * 0.02 ctx.beginPath() var startAngle = Math.PI * 0.5 var endAngle = startAngle + startButt.progress * Math.PI * 2 ctx.arc(width / 2, height / 2, width / 2 - ctx.lineWidth / 2 - 2, startAngle, endAngle) ctx.stroke() } } } } } ProgressCircle { id: prog property double progress: speedBackend.race.nextStartActionDetails[ScStwRace.NextStartActionDelayProgress] anchors.fill: startButt opacity: app.state === "STARTING" ? 1:0 scale: startButt.scale lineWidth: prog.width * 0.02 arcBegin: 0 arcEnd: 360 * (1 - (progress > 0 ? progress:1)) colorCircle: "grey" Behavior on opacity { NumberAnimation { duration: 200 } } animationDuration: 0 } /*---------------------- Cancel button ----------------------*/ FancyButton { id: cancelButt text: "cancel" anchors { right: startButt.right bottom: startButt.bottom } contentItem: Text { //make text disappear } height: startButt.height * 0.3 scale: 0 width: height enabled: app.state === "STARTING" onClicked: { app.cancel() } Behavior on scale { PropertyAnimation { duration: 200 } } Text { id: cancelButt_text text: cancelButt.text anchors.centerIn: parent font.pixelSize: parent.height * 0.16 font.family: "Helvetica" color: appTheme.theme.colors.text } backgroundColor: appTheme.theme.colors.button } /*------ Popups ------*/ SettingsDialog{ id: settingsDialog x: startButt.x y: startButt.y width: startButt.width height: startButt.height } ProfilesDialog { id: profilesDialog property int margin: app.landscape() ? app.height * 0.05:app.width * 0.05 x: app.landscape() ? topContainerItm.width + margin:topContainerItm.x + margin y: !app.landscape() ? topContainerItm.height + margin:topContainerItm.x + margin width: app.landscape() ? app.width - topContainerItm.width - menu_container.width - margin * 2 : app.width - margin * 2 height: !app.landscape() ? app.height - topContainerItm.height - menu_container.height - margin * 2 : app.height - margin * 2 } /*------------------- lower line and menu -------------------*/ Rectangle { id: lowerLine width: app.landscape() ? 1:parent.width height: app.landscape() ? parent.height:1 color: appTheme.theme.colors.line anchors.right: app.landscape() ? menu_container.left:parent.right anchors.bottom: app.landscape() ? parent.bottom:menu_container.top anchors.top: app.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: app.landscape() ? startButt.right:parent.left top: app.landscape() ? parent.top:startButt.bottom topMargin: app.landscape() ? undefined:parent.height * 0.1 leftMargin: app.landscape() ? parent.width * 0.05:0 } Rectangle { id: lowerMenuBackground anchors.fill: parent color: appTheme.theme.colors.menu 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: app.landscape() ? activeChildren:1 columns: app.landscape() ? 1:activeChildren spacing: 0// app.landscape() ? parent.height * spacingMultiplier * 0.001:parent.width * spacingMultiplier * 0.001 Behavior on spacingMultiplier { NumberAnimation { duration: 200 } } FancyButton { id: settingsButt height: app.landscape() ? menu_container.width * 0.7:menu_container.height * 0.7 width: height onClicked: { settingsDialog.open() } image: appTheme.theme.images.settIcon backgroundColor: parent.pressed ? appTheme.theme.colors.buttonPressed:appTheme.theme.colors.button } Item { height: profilesButt.height width: profilesButt.height } FancyButton { id: profilesButt enabled: height > 0 state: speedBackend.scStwClient.state === ScStwClient.CONNECTED ? "visible":"hidden" width: height onClicked: { profilesDialog.open() } image: appTheme.theme.images.profilesIcon backgroundColor: parent.pressed ? appTheme.theme.colors.buttonPressed:appTheme.theme.colors.button states: [ State { name: "hidden" PropertyChanges { target: profilesButt height: 0 } }, State { name: "visible" PropertyChanges { target: profilesButt height: app.landscape() ? menu_container.width * 0.7:menu_container.height * 0.7 } } ] transitions: [ Transition { NumberAnimation { properties: "height" } } ] } } } /*---------------------- Timer states ----------------------*/ states: [ State { name: "IDLE" //state for the start page PropertyChanges { target: topContainerItm; anchors.bottomMargin: app.landscape() ? undefined:parent.height * 0.1; anchors.rightMargin: app.landscape() ? parent.height * 0.05:0 } PropertyChanges { target: startButt; enabled: true; text: "start"; size: app.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 implicitText: "click start to start" } }, State { name: "WAITING" //state when a false start occured and waiting for time calculation PropertyChanges { target: startButt; enabled: false; text: "waiting..."; anchors.rightMargin: app.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: app.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 implicitText: "please wait..." } }, State { name: "STARTING" //state for the start sequence PropertyChanges { target: startButt; enabled: false; text: "starting..."; anchors.rightMargin: app.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: app.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 implicitText: "NEXT_START_ACTION" } }, State { name: "RUNNING" //state when the timer is running PropertyChanges { target: startButt; enabled: true; text: speedBackend.scStwClient.state === ScStwClient.CONNECTED ? "cancel":"stop" anchors.rightMargin: app.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: app.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: topLa implicitText: "" } }, State { name: "STOPPED" //state when the meassuring is over PropertyChanges { target: startButt; enabled: true; text: "reset"; size: app.landscape() ? parent.height * 0.35:parent.height * 0.2; anchors.bottomMargin: app.landscape() ? parent.height * 0.5 - startButt.height * 0.5:parent.height * 0.2 - startButt.height * 0.5 anchors.rightMargin: app.landscape() ? parent.height * 0.2 - startButt.height * 0.5:parent.width * 0.5 - startButt.width * 0.5 } PropertyChanges { target: topContainerItm; anchors.rightMargin: app.landscape() ? 0-startButt.width/2:undefined anchors.bottomMargin: app.landscape() ? undefined:0-startButt.height/2 } PropertyChanges { target: topLa text: "" } } ] /*---------------------- 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(app.height < app.width) } /*----Functions to control the stopwatch----*/ function start(){ var ret = speedBackend.race.start() if(ret !== 200){ console.log("+ --- error starting race: " + ret) } } function cancel() { var ret = speedBackend.race.cancel() if(ret !== 200){ console.log("+ --- error canellingr race: " + ret) } } function stop(){ var ret = speedBackend.race.stop() if(ret !== 200){ console.log("+ --- error stopping race: " + ret) } } function reset(){ var ret = speedBackend.race.reset() if(ret !== 200){ console.log("+ --- error resetting race: " + ret) } } } }