// SPDX-License-Identifier: GPL-3.0-or-later /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Copyright (C) 2013 - 2024, nymea GmbH * Copyright (C) 2024 - 2025, chargebyte austria GmbH * * This file is part of nymea-app. * * nymea-app is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * nymea-app 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 * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with nymea-app. If not, see . * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ import QtQuick import QtQuick.Controls import QtQuick.Controls.Material import QtQuick.Layouts import QtQuick.Window import QtCore import Qt.labs.folderlistmodel import Qt5Compat.GraphicalEffects import Nymea import "components" import "delegates" import "mainviews" Page { id: root // Removing the background from this page only because the MainViewBase adds it again in // a deepter layer as we need to include it in the blurring of the header and footer. // We don't want to paint the background on the entire screen twice (overdraw is costly) background: null function configureViews() { if (Configuration.hasOwnProperty("mainViewsFilter")) { console.warn("Main views configuration is disabled by app configuration") return } PlatformHelper.vibrate(PlatformHelper.HapticsFeedbackSelection) d.configOverlay = configComponent.createObject(contentContainer) } function goToView(viewName, data) { // We allow separating the target by : and pass more stuff to console.log("Going to main view", viewName, filteredContentModel.count, data) for (var i = 0; i < filteredContentModel.count; i++) { console.log("got", i, filteredContentModel.modelData(i, "name")) if (filteredContentModel.modelData(i, "name") === viewName) { console.log("activating", i) // mainViewSettings.currentIndex = i; // tabBar.currentIndex = i; swipeView.setCurrentIndex(i) swipeView.currentItem.item.handleEvent(data) break; } } } header: Item { id: mainHeader height: 0 HeaderButton { id: menuButton imageSource: "qrc:/icons/navigation-menu.svg" anchors { left: parent.left; top: parent.top } onClicked: { if (d.configOverlay != null) { d.configOverlay.destroy(); } app.mainMenu.open() } } Row { id: additionalIcons anchors { right: parent.right; top: parent.top } visible: !d.configOverlay width: visible ? implicitWidth : 0 HeaderButton { id: button imageSource: "qrc:/icons/system-update.svg" color: Style.accentColor visible: updatesModel.count > 0 || engine.systemController.updateRunning onClicked: pageStack.push(Qt.resolvedUrl("system/SystemUpdatePage.qml")) RotationAnimation on rotation { from: 0 to: 360 duration: 2000 loops: Animation.Infinite running: engine.systemController.updateRunning onStopped: button.rotation = 0; } PackagesFilterModel { id: updatesModel packages: engine.systemController.packages updatesOnly: true } } Repeater { model: swipeView.currentItem != null && swipeView.currentItem.item.hasOwnProperty("headerButtons") ? swipeView.currentItem.item.headerButtons : 0 delegate: HeaderButton { imageSource: swipeView.currentItem.item.headerButtons[index].iconSource onClicked: swipeView.currentItem.item.headerButtons[index].trigger() visible: swipeView.currentItem.item.headerButtons[index].visible color: swipeView.currentItem.item.headerButtons[index].color } } } } Connections { target: engine.ruleManager onAddRuleReply: (commandId, ruleError, ruleId) => { d.editRulePage.busy = false if (d.editRulePage) { pageStack.pop(); d.editRulePage = null } } } QtObject { id: d property bool blurEnabled: PlatformHelper.deviceManufacturer !== "raspbian" property var editRulePage: null property var configOverlay: null } Settings { id: mainViewSettings category: engine.jsonRpcClient.currentHost.uuid property string mainMenuContent: "" property var sortOrder: [] // Priority for main view config: // 1. Settings made by the user // 2. Style mainViewsFilter as that comes with branding (for now, if a style defines main views, all of them are active by default) // 3. Command line args // 4. Just show "things" alone by default property var filterList: Configuration.hasOwnProperty("mainViewsFilter") ? Configuration.mainViewsFilter : defaultMainViewFilter.length > 0 ? defaultMainViewFilter.split(',') : [Configuration.defaultMainView] property int currentIndex: 0 } ListModel { id: mainMenuBaseModel ListElement { name: "things"; source: "ThingsView"; displayName: qsTr("Things"); icon: "things"; minVersion: "0.0" } ListElement { name: "favorites"; source: "FavoritesView"; displayName: qsTr("Favorites"); icon: "starred"; minVersion: "2.0" } ListElement { name: "groups"; source: "GroupsView"; displayName: qsTr("Groups"); icon: "groups"; minVersion: "2.0" } ListElement { name: "scenes"; source: "ScenesView"; displayName: qsTr("Scenes"); icon: "slideshow"; minVersion: "2.0" } ListElement { name: "garages"; source: "GaragesView"; displayName: qsTr("Garages"); icon: "garage/garage-100"; minVersion: "2.0" } ListElement { name: "energy"; source: "EnergyView"; displayName: qsTr("Energy"); icon: "smartmeter"; minVersion: "2.0" } ListElement { name: "media"; source: "MediaView"; displayName: qsTr("Media"); icon: "media"; minVersion: "2.0" } ListElement { name: "dashboard"; source: "DashboardView"; displayName: qsTr("Dashboard"); icon: "dashboard"; minVersion: "5.5" } ListElement { name: "airconditioning"; source: "AirConditioningView"; displayName: qsTr("AC"); icon: "sensors"; minVersion: "6.2" } } ListModel { id: mainMenuModel ListElement { name: "dummy"; source: "Dummy"; displayName: ""; icon: "" } Component.onCompleted: { var configList = {} var newList = {} var newItems = 0 // Add extra views first to make them appear first in the list unless the config says otherwise if (Configuration.hasOwnProperty("additionalMainViews")) { for (var i = 0; i < Configuration.additionalMainViews.count; i++) { var item = Configuration.additionalMainViews.get(i); var idx = mainViewSettings.sortOrder.indexOf(item.name); if (idx === -1) { newList[newItems++] = item; } else { configList[idx] = item; } } } for (var i = 0; i < mainMenuBaseModel.count; i++) { var item = mainMenuBaseModel.get(i); if (!engine.jsonRpcClient.ensureServerVersion(item.minVersion)) { console.log("Skipping main view", item.name, "as the minimum required server version isn't met:", engine.jsonRpcClient.jsonRpcVersion, "<", item.minVersion) continue; } var idx = mainViewSettings.sortOrder.indexOf(item.name); if (idx === -1) { newList[newItems++] = item; } else { configList[idx] = item; } } clear(); var brandingFilter = Configuration.hasOwnProperty("mainViewsFilter") ? Configuration.mainViewsFilter : [] for (idx in configList) { item = configList[idx]; if (brandingFilter.length === 0 || brandingFilter.indexOf(item.name) >= 0) { mainMenuModel.append(item) } } for (idx in newList) { item = newList[idx]; if (brandingFilter.length === 0 || brandingFilter.indexOf(item.name) >= 0) { mainMenuModel.append(item) } } swipeView.currentIndex = mainViewSettings.currentIndex mainViewSettings.currentIndex = Qt.binding(function() { return swipeView.currentIndex; }) } } SortFilterProxyModel { id: filteredContentModel sourceModel: mainMenuModel filterList: mainViewSettings.filterList filterRoleName: "name" } Item { id: contentContainer anchors.fill: parent clip: true property int headerSize: 48 property int footerSize: app.landscape ? 48 : 64 readonly property int scrollOffset: swipeView.currentItem ? swipeView.currentItem.item.contentY : 0 readonly property int headerBlurSize: Math.min(headerSize, scrollOffset * 2) Background { anchors.fill: parent } SwipeView { id: swipeView anchors.fill: parent opacity: d.configOverlay === null ? 1 : 0 visible: !engine.thingManager.fetchingData Behavior on opacity { NumberAnimation { duration: 200; easing.type: Easing.InOutQuad } } Repeater { id: mainViewsRepeater model: d.configOverlay != null ? null : filteredContentModel delegate: Loader { id: mainViewLoader width: swipeView.width height: swipeView.height clip: true source: "mainviews/" + model.source + ".qml" visible: SwipeView.isCurrentItem || SwipeView.isNextItem || SwipeView.isPreviousItem Binding { target: mainViewLoader.item property: "isCurrentItem" value: swipeView.currentIndex == index } Binding { target: mainViewLoader.item property: "bottomMargin" value: footer.visible ? contentContainer.footerSize : 0 } Image { source: "qrc:/styles/%1/logo-wide.svg".arg(styleController.currentStyle) anchors { top: parent.top; topMargin: -contentContainer.scrollOffset + (contentContainer.headerSize - height) / 2 horizontalCenter: parent.horizontalCenter; } fillMode: Image.PreserveAspectFit height: 28 sourceSize.height: height antialiasing: true z: 2 } } } } ColumnLayout { anchors { left: parent.left; right: parent.right; verticalCenter: parent.verticalCenter; margins: Style.margins } spacing: Style.margins visible: engine.thingManager.fetchingData BusyIndicator { Layout.alignment: Qt.AlignHCenter running: parent.visible } Label { text: qsTr("Loading data...") font: Style.bigFont Layout.fillWidth: true horizontalAlignment: Text.AlignHCenter } } } ShaderEffectSource { id: headerBlurSource width: contentContainer.width height: d.configOverlay ? contentContainer.headerSize : contentContainer.headerBlurSize sourceItem: d.blurEnabled ? contentContainer : null sourceRect: Qt.rect(0, 0, contentContainer.width, d.configOverlay ? contentContainer.headerSize : contentContainer.headerBlurSize) visible: false } FastBlur { anchors { left: parent.left; top: parent.top; right: parent.right; } height: d.configOverlay ? contentContainer.headerSize : contentContainer.headerBlurSize radius: 40 transparentBorder: true source: d.blurEnabled ? headerBlurSource : null visible: d.blurEnabled } Rectangle { id: headerOpacityMask anchors { left: parent.left top: parent.top right: parent.right } height: d.configOverlay ? contentContainer.headerSize : contentContainer.headerBlurSize gradient: Gradient { GradientStop { position: 0.1; color: Style.backgroundColor } GradientStop { position: 0.6; color: Qt.rgba(Style.backgroundColor.r, Style.backgroundColor.g, Style.backgroundColor.b, 0.3) } GradientStop { position: 1; color: "transparent" } } } ShaderEffectSource { id: footerBlurSource width: contentContainer.width height: contentContainer.footerSize sourceItem: d.blurEnabled ? contentContainer : null sourceRect: Qt.rect(0, contentContainer.height - height, contentContainer.width, contentContainer.footerSize) visible: false enabled: d.blurEnabled && footer.shown } FastBlur { anchors { left: parent.left; bottom: parent.bottom; right: parent.right; } height: contentContainer.footerSize radius: 40 transparentBorder: false source: d.blurEnabled ? footerBlurSource : null visible: d.blurEnabled && footer.shown } Rectangle { id: footer readonly property bool shown: tabsRepeater.count > 1 || d.configOverlay visible: shown anchors { left: parent.left bottom: parent.bottom right: parent.right } height: contentContainer.footerSize Behavior on height { NumberAnimation { duration: 200; easing.type: Easing.InOutQuad }} // color: "transparent" gradient: Gradient { GradientStop { position: 0; color: "transparent" } GradientStop { position: 0.4; color: Qt.rgba(Style.backgroundColor.r, Style.backgroundColor.g, Style.backgroundColor.b, 0.7) } GradientStop { position: 1; color: Style.backgroundColor } } RowLayout { id: tabsLayout anchors.fill: parent spacing: 0 opacity: d.configOverlay ? 0 : 1 Behavior on opacity { NumberAnimation { duration: 200; easing.type: Easing.InOutQuad } } Repeater { id: tabsRepeater model: d.configOverlay != null ? null : filteredContentModel // model: filteredContentModel delegate: MainPageTabButton { Layout.fillWidth: true Layout.fillHeight: true alignment: app.landscape ? Qt.Horizontal : Qt.Vertical checked: index === swipeView.currentIndex // anchors.verticalCenter: parent.verticalCenter text: model.displayName iconSource: "qrc:/icons/" + model.icon + ".svg" onClicked: swipeView.currentIndex = index onPressAndHold: { root.configureViews(); } } } } MainPageTabButton { anchors.fill: parent alignment: app.landscape ? Qt.Horizontal : Qt.Vertical text: d.configOverlay ? qsTr("Done") : qsTr("Configure") iconSource: "qrc:/icons/configure.svg" opacity: d.configOverlay ? 1 : 0 Behavior on opacity { NumberAnimation { duration: 200; easing.type: Easing.InOutQuad } } visible: opacity > 0 checked: true onClicked: { if (d.configOverlay) { d.configOverlay.destroy() } else { PlatformHelper.vibrate(PlatformHelper.HapticsFeedbackSelection) d.configOverlay = configComponent.createObject(contentContainer) } } } } Component { id: configComponent Background { id: configOverlay width: contentContainer.width height: contentContainer.height ListView { id: configListView anchors.fill: parent model: mainMenuModel topMargin: contentContainer.headerSize bottomMargin: contentContainer.footerSize property bool dragging: draggingIndex >= 0 property int draggingIndex : -1 moveDisplaced: Transition { NumberAnimation { properties: "y" } } delegate: NymeaItemDelegate { id: viewConfigDelegate width: parent.width text: model.displayName iconName: Qt.resolvedUrl("qrc:/icons/" + model.icon + ".svg") progressive: false checked: mainViewSettings.filterList.indexOf(model.name) >= 0 visible: index !== configListView.draggingIndex additionalItem: CheckBox { checked: viewConfigDelegate.checked anchors.verticalCenter: parent.verticalCenter onClicked: { var newList = [] for (var i = 0; i < mainMenuModel.count; i++) { var entry = mainMenuModel.get(i).name; if (entry === model.name) { if (!isEnabled) { newList.push(model.name) } } else { if (mainViewSettings.filterList.indexOf(entry) >= 0) { newList.push(entry) } } } if (newList.length === 0) { newList.push(Configuration.defaultMainView) } mainViewSettings.filterList = newList } } } MouseArea { id: dndArea anchors.fill: parent preventStealing: configListView.dragging property int dragOffset: 0 onPressAndHold: { mouse.accepted = true var mouseYInListView = configListView.contentItem.mapFromItem(dndArea, mouseX, mouseY).y; configListView.draggingIndex = configListView.indexAt(mouseX, mouseYInListView) var item = mainMenuModel.get(configListView.draggingIndex) print("draggingIndex", configListView.draggingIndex) dndItem.text = item.displayName dndItem.iconName = item.icon var visualItem = configListView.itemAt(mouseX, mouseYInListView) dndItem.checked = visualItem.checked dndArea.dragOffset = configListView.mapToItem(visualItem, mouseX, mouseY).y PlatformHelper.vibrate(PlatformHelper.HapticsFeedbackImpact) } onMouseYChanged: { if (configListView.dragging) { var mouseYInListView = configListView.contentItem.mapFromItem(dndArea, mouseX, mouseY).y; var indexUnderMouse = configListView.indexAt(mouseX, mouseYInListView - dndArea.dragOffset / 2) if (indexUnderMouse < 0) { return; } indexUnderMouse = Math.min(Math.max(0, indexUnderMouse), configListView.count - 1) if (configListView.draggingIndex !== indexUnderMouse) { print("moving to", indexUnderMouse) PlatformHelper.vibrate(PlatformHelper.HapticsFeedbackSelection) mainMenuModel.move(configListView.draggingIndex, indexUnderMouse, 1) configListView.draggingIndex = indexUnderMouse; } } } onReleased: { print("released!") var mouseYInListView = configListView.contentItem.mapFromItem(dndArea, mouseX, mouseY).y; var clickedIndex = configListView.indexAt(mouseX, mouseYInListView) var item = mainMenuModel.get(clickedIndex) var isEnabled = mainViewSettings.filterList.indexOf(item.name) >= 0; if (!configListView.dragging) { var newList = [] for (var i = 0; i < mainMenuModel.count; i++) { var entry = mainMenuModel.get(i).name; if (entry === item.name) { if (!isEnabled) { newList.push(item.name) } } else { if (mainViewSettings.filterList.indexOf(entry) >= 0) { newList.push(entry) } } } if (newList.length === 0) { newList.push(Configuration.defaultMainView) } mainViewSettings.filterList = newList } configListView.draggingIndex = -1; var newSortOrder = [] for (var i = 0; i < mainMenuModel.count; i++) { newSortOrder.push(mainMenuModel.get(i).name) } mainViewSettings.sortOrder = newSortOrder; } // Timer { // id: scroller // interval: 2 // repeat: true // running: direction != 0 // property int direction: { // if (!configListView.dragging) { // return 0; // } // return dndArea.mouseY < 50 ? -2 : dndArea.mouseY > dndArea.height - 50 ? 2 : 0 // } // onTriggered: { // configListView.contentY = Math.min(Math.max(0, configListView.contentY + direction), configListView.contentHeight - configListView.height) // } // } } NymeaItemDelegate { id: dndItem visible: configListView.dragging y: dndArea.mouseY - dndArea.dragOffset width: configListView.width progressive: false additionalItem: CheckBox { checked: dndItem.checked anchors.verticalCenter: parent.verticalCenter } } } // NumberAnimation { // target: configOverlay // property: "scale" // duration: 200 // easing.type: Easing.InOutQuad // from: 2 // to: 1 // running: true // } // NumberAnimation { // target: configOverlay // property: "opacity" // duration: 200 // easing.type: Easing.InOutQuad // from: 0 // to: 1 // running: true // } // ListView { // id: configListView // model: mainMenuModel // width: parent.width // height: parent.height / 3 // anchors.centerIn: parent // orientation: ListView.Horizontal // moveDisplaced: Transition { // NumberAnimation { properties: "x,y"; duration: 200 } // } // property int delegateWidth: width / 3 // property bool dragging: draggingIndex >= 0 // property int draggingIndex : -1 // MouseArea { // id: dndArea // anchors.fill: parent // preventStealing: configListView.dragging // property int dragOffset: 0 // onPressAndHold: { // mouse.accepted = true // var mouseXInListView = configListView.contentItem.mapFromItem(dndArea, mouseX, mouseY).x; // configListView.draggingIndex = configListView.indexAt(mouseXInListView, mouseY) // var item = mainMenuModel.get(configListView.draggingIndex) // dndItem.displayName = item.displayName // dndItem.icon = item.icon // var visualItem = configListView.itemAt(mouseXInListView, mouseY) // dndItem.isEnabled = visualItem.isEnabled // dndArea.dragOffset = configListView.mapToItem(visualItem, mouseX, mouseY).x // PlatformHelper.vibrate(PlatformHelper.HapticsFeedbackImpact) // } // onMouseYChanged: { // if (configListView.dragging) { // var mouseXInListView = configListView.contentItem.mapFromItem(dndArea, mouseX, mouseY).x; // var indexUnderMouse = configListView.indexAt(mouseXInListView - dndArea.dragOffset / 2, mouseY) // indexUnderMouse = Math.min(Math.max(0, indexUnderMouse), configListView.count - 1) // if (configListView.draggingIndex !== indexUnderMouse) { // PlatformHelper.vibrate(PlatformHelper.HapticsFeedbackSelection) // mainMenuModel.move(configListView.draggingIndex, indexUnderMouse, 1) // configListView.draggingIndex = indexUnderMouse; // } // } // } // onReleased: { // print("released!") // var mouseXInListView = configListView.contentItem.mapFromItem(dndArea, mouseX, mouseY).x; // var clickedIndex = configListView.indexAt(mouseXInListView, mouseY) // var item = mainMenuModel.get(clickedIndex) // var isEnabled = mainViewSettings.filterList.indexOf(item.name) >= 0; // if (!configListView.dragging) { // var newList = [] // for (var i = 0; i < mainMenuModel.count; i++) { // var entry = mainMenuModel.get(i).name; // if (entry === item.name) { // if (!isEnabled) { // newList.push(item.name) // } // } else { // if (mainViewSettings.filterList.indexOf(entry) >= 0) { // newList.push(entry) // } // } // } // if (newList.length === 0) { // newList.push(Configuration.defaultMainView) // } // mainViewSettings.filterList = newList // } // configListView.draggingIndex = -1; // var newSortOrder = [] // for (var i = 0; i < mainMenuModel.count; i++) { // newSortOrder.push(mainMenuModel.get(i).name) // } // mainViewSettings.sortOrder = newSortOrder; // } // Timer { // id: scroller // interval: 2 // repeat: true // running: direction != 0 // property int direction: { // if (!configListView.dragging) { // return 0; // } // return dndArea.mouseX < 50 ? -2 : dndArea.mouseX > dndArea.width - 50 ? 2 : 0 // } // onTriggered: { // configListView.contentX = Math.min(Math.max(0, configListView.contentX + direction), configListView.contentWidth - configListView.width) // } // } // } // delegate: BigTile { // id: configDelegate // width: configListView.delegateWidth // height: configListView.height // property bool isEnabled: mainViewSettings.filterList.indexOf(model.name) >= 0 // visible: configListView.draggingIndex !== index // leftPadding: 0 // rightPadding: 0 // topPadding: 0 // bottomPadding: 0 // header: RowLayout { // id: headerRow // width: parent.width // Label { // text: model.displayName // Layout.fillWidth: true // elide: Text.ElideRight // } // } // contentItem: Item { // Layout.fillWidth: true // implicitHeight: configListView.height - headerRow.height - Style.margins * 2 // ColorIcon { // anchors.centerIn: parent // width: Math.min(parent.width, parent.height) * .6 // height: width // name: Qt.resolvedUrl("qrc:/icons/" + model.icon + ".svg") // color: configDelegate.isEnabled ? Style.accentColor : Style.iconColor // } // } // } // Item { // id: dndItem // width: configListView.delegateWidth // height: configListView.height // property bool isEnabled: false // property string displayName: "" // property string icon: "things" // visible: configListView.dragging // x: dndArea.mouseX - dndArea.dragOffset // onVisibleChanged: { // if (visible) { // dragStartAnimation.start(); // } // } // NumberAnimation { // id: dragStartAnimation // target: dndItem // property: "scale" // from: 1 // to: 0.95 // duration: 200 // } // BigTile { // id: dndTile // anchors.fill: parent // // anchors.margins: app.margins / 2 // Material.elevation: 2 // leftPadding: 0 // rightPadding: 0 // topPadding: 0 // bottomPadding: 0 // header: RowLayout { // Label { // text: dndItem.displayName // } // } // contentItem: Item { // Layout.fillWidth: true // implicitHeight: configListView.height - header.height // ColorIcon { // anchors.centerIn: parent // width: Math.min(parent.width, parent.height) * .6 // height: width // name: Qt.resolvedUrl("qrc:/icons/" + dndItem.icon + ".svg") // color: dndItem.isEnabled ? Style.accentColor : Style.iconColor // } // } // } // } // } } } Component { id: connectionDialogComponent NymeaDialog { id: connectionDialog title: engine.jsonRpcClient.currentHost.name standardButtons: Dialog.NoButton headerIcon: { switch (engine.jsonRpcClient.currentConnection.bearerType) { case Connection.BearerTypeLan: case Connection.BearerTypeWan: if (engine.jsonRpcClient.availableBearerTypes & NymeaConnection.BearerTypeEthernet != NymeaConnection.BearerTypeNone) { return "qrc:/icons/connections/network-wired.svg" } return "qrc:/icons/connections/network-wifi.svg"; case Connection.BearerTypeBluetooth: return "qrc:/icons/connections/bluetooth.svg"; case Connection.BearerTypeCloud: return "qrc:/icons/connections/cloud.svg" case Connection.BearerTypeLoopback: return "qrc:/icons/connections/network-wired.svg" } return "" } Label { Layout.fillWidth: true text: qsTr("Connected to") font.pixelSize: app.smallFont elide: Text.ElideRight wrapMode: Text.WrapAtWordBoundaryOrAnywhere horizontalAlignment: Text.AlignHCenter } Label { Layout.fillWidth: true text: engine.jsonRpcClient.currentHost.name elide: Text.ElideRight wrapMode: Text.WrapAtWordBoundaryOrAnywhere horizontalAlignment: Text.AlignHCenter } Item { Layout.fillWidth: true Layout.preferredHeight: app.margins } RowLayout { ColumnLayout { Label { Layout.fillWidth: true text: engine.jsonRpcClient.currentHost.uuid font.pixelSize: app.smallFont elide: Text.ElideRight color: Material.color(Material.Grey) // horizontalAlignment: Text.AlignHCenter } Label { Layout.fillWidth: true text: engine.jsonRpcClient.currentConnection.url font.pixelSize: app.smallFont elide: Text.ElideRight color: Material.color(Material.Grey) // horizontalAlignment: Text.AlignHCenter } } ColorIcon { Layout.preferredHeight: Style.iconSize Layout.preferredWidth: Style.iconSize name: engine.jsonRpcClient.currentConnection.secure ? "qrc:/icons/lock-closed.svg" : "qrc:/icons/lock-open.svg" MouseArea { anchors.fill: parent onClicked: { var component = Qt.createComponent(Qt.resolvedUrl("connection/CertificateDialog.qml")); var popup = component.createObject(app, {serverUuid: engine.jsonRpcClient.serverUuid, issuerInfo: engine.jsonRpcClient.certificateIssuerInfo}); popup.open(); } } } } Item { Layout.fillWidth: true Layout.preferredHeight: app.margins } RowLayout { Layout.fillWidth: true Button { id: disconnectButton text: qsTr("Disconnect") Layout.preferredWidth: Math.max(cancelButton.implicitWidth, disconnectButton.implicitWidth) onClicked: { engine.jsonRpcClient.disconnectFromHost(); } } Item { Layout.fillWidth: true } Button { id: cancelButton text: qsTr("OK") Layout.preferredWidth: Math.max(cancelButton.implicitWidth, disconnectButton.implicitWidth) onClicked: connectionDialog.close() } } } } }