// 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 2.8 import QtQuick.Controls 2.2 import QtQuick.Controls.Material 2.1 import QtQuick.Layouts 1.2 import QtQuick.Window 2.3 import Qt.labs.settings 1.0 import Qt.labs.folderlistmodel 2.2 import QtGraphicalEffects 1.0 import Nymea 1.0 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: { 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() } } } } }