import QtQml 2.1 import QtQml.Models 2.1 import QtQuick 2.9 import QtQuick.Window 2.3 import QtQuick.Controls 2.3 import QtQuick.Layouts 1.2 import QtGraphicalEffects 1.0 // Custom qml modules are in /theme (and included by resources.qrc) import Style 1.0 import com.nextcloud.desktopclient 1.0 Window { id: trayWindow width: Style.trayWindowWidth height: Style.trayWindowHeight color: "transparent" flags: Qt.Dialog | Qt.FramelessWindowHint readonly property int maxMenuHeight: Style.trayWindowHeight - Style.trayWindowHeaderHeight - 2 * Style.trayWindowBorderWidth Accessible.role: Accessible.Application Accessible.name: "Nextcloud client tray window" // Close tray window when focus is lost (e.g. click somewhere else on the screen) onActiveChanged: { if(!active) { trayWindow.hide(); Systray.setClosed(); } } onVisibleChanged: { currentAccountAvatar.source = "" currentAccountAvatar.source = "image://avatars/currentUser" currentAccountStateIndicator.source = "" currentAccountStateIndicator.source = UserModel.isUserConnected(UserModel.currentUserId()) ? "qrc:///client/theme/colored/astate-ok.svg" : "qrc:///client/theme/colored/state-offline.svg" // HACK: reload account Instantiator immediately by restting it - could be done better I guess // see also id:accountMenu below userLineInstantiator.active = false; userLineInstantiator.active = true; } Connections { target: UserModel onRefreshCurrentUserGui: { currentAccountAvatar.source = "" currentAccountAvatar.source = "image://avatars/currentUser" currentAccountStateIndicator.source = "" currentAccountStateIndicator.source = UserModel.isUserConnected(UserModel.currentUserId()) ? "qrc:///client/theme/colored/state-ok.svg" : "qrc:///client/theme/colored/state-offline.svg" } onNewUserSelected: { accountMenu.close(); } } Connections { target: Systray onShowWindow: { accountMenu.close(); appsMenu.close(); Systray.positionWindow(trayWindow); trayWindow.show(); trayWindow.raise(); trayWindow.requestActivate(); Systray.setOpened(); UserModel.fetchCurrentActivityModel(); } onHideWindow: { trayWindow.hide(); Systray.setClosed(); } } OpacityMask { anchors.fill: parent source: ShaderEffectSource { sourceItem: trayWindowBackground hideSource: true } maskSource: Rectangle { width: trayWindowBackground.width height: trayWindowBackground.height radius: trayWindowBackground.radius } } Rectangle { id: trayWindowBackground anchors.fill: parent radius: Style.trayWindowRadius border.width: Style.trayWindowBorderWidth border.color: Style.menuBorder Accessible.role: Accessible.Grouping Accessible.name: "Nextcloud client tray window" Rectangle { id: trayWindowHeaderBackground anchors.left: trayWindowBackground.left anchors.top: trayWindowBackground.top height: Style.trayWindowHeaderHeight width: Style.trayWindowWidth color: Style.ncBlue // The overlay rectangle below eliminates the rounded corners from the bottom of the header // as Qt only allows setting the radius for all corners right now, not specific ones Rectangle { id: trayWindowHeaderButtomHalfBackground anchors.left: trayWindowHeaderBackground.left anchors.bottom: trayWindowHeaderBackground.bottom height: Style.trayWindowHeaderHeight / 2 width: Style.trayWindowWidth color: Style.ncBlue } RowLayout { id: trayWindowHeaderLayout spacing: 0 anchors.fill: parent Button { id: currentAccountButton Layout.preferredWidth: Style.currentAccountButtonWidth Layout.preferredHeight: Style.trayWindowHeaderHeight display: AbstractButton.IconOnly flat: true Accessible.role: Accessible.ButtonMenu Accessible.name: "Current account" Accessible.description: "Shows the currently selected account and opens a dropdown list for all accounts when clicked" Accessible.onPressAction: { syncPauseButton.text = Systray.syncIsPaused() ? qsTr("Resume sync for all") : qsTr("Pause sync for all") accountMenu.open() } MouseArea { id: accountBtnMouseArea anchors.fill: parent hoverEnabled: Style.hoverEffectsEnabled // HACK: Imitate Qt hover effect brightness (which is not accessible as property) // so that indicator background also flicks when hovered onContainsMouseChanged: { currentAccountStateIndicatorBackground.color = (containsMouse ? Style.ncBlueHover : Style.ncBlue) } // We call open() instead of popup() because we want to position it // exactly below the dropdown button, not the mouse onClicked: { syncPauseButton.text = Systray.syncIsPaused() ? qsTr("Resume sync for all") : qsTr("Pause sync for all") if (accountMenu.visible) { accountMenu.close() } else { accountMenu.open() } } Menu { id: accountMenu // x coordinate grows towards the right // y coordinate grows towards the bottom x: (currentAccountButton.x + 2) y: (currentAccountButton.y + Style.trayWindowHeaderHeight + 2) width: (Style.currentAccountButtonWidth - 2) height: Math.min(implicitHeight, maxMenuHeight) closePolicy: Menu.CloseOnPressOutsideParent | Menu.CloseOnEscape background: Rectangle { border.color: Style.menuBorder radius: Style.currentAccountButtonRadius } Accessible.role: PopupMenu Accessible.name: qsTr("Account menu") Accessible.description: qsTr("Popup menu showing a list of accounts and general client actions") onClosed: { // HACK: reload account Instantiator immediately by restting it - could be done better I guess // see also onVisibleChanged above userLineInstantiator.active = false; userLineInstantiator.active = true; } Instantiator { id: userLineInstantiator model: UserModel delegate: UserLine {} onObjectAdded: accountMenu.insertItem(index, object) onObjectRemoved: accountMenu.removeItem(object) } MenuItem { id: addAccountButton height: Style.addAccountButtonHeight hoverEnabled: true background: Item { height: parent.height width: parent.menu.width Rectangle { anchors.fill: parent anchors.margins: 1 color: parent.parent.hovered ? Style.lightHover : "transparent" } } RowLayout { anchors.fill: parent spacing: 0 Image { Layout.leftMargin: 12 verticalAlignment: Qt.AlignCenter source: "qrc:///client/theme/black/add.svg" sourceSize.width: Style.headerButtonIconSize sourceSize.height: Style.headerButtonIconSize } Label { Layout.leftMargin: 14 text: qsTr("Add account") color: "black" font.pixelSize: Style.topLinePixelSize } // Filler on the right Item { Layout.fillWidth: true Layout.fillHeight: true } } onClicked: UserModel.addAccount() Accessible.role: Accessible.MenuItem Accessible.name: qsTr("Add new account") Accessible.description: qsTr("Opens setup window to add a new account connection") Accessible.onPressAction: { Systray.pauseResumeSync() } } MenuSeparator { contentItem: Rectangle { implicitHeight: 1 color: Style.menuBorder } } MenuItem { id: syncPauseButton font.pixelSize: Style.topLinePixelSize hoverEnabled: true onClicked: Systray.pauseResumeSync() background: Item { height: parent.height width: parent.menu.width Rectangle { anchors.fill: parent anchors.margins: 1 color: parent.parent.hovered ? Style.lightHover : "transparent" } } Accessible.role: Accessible.MenuItem Accessible.name: Systray.syncIsPaused() ? qsTr("Resume sync for all") : qsTr("Pause sync for all") Accessible.description: qsTr("Button that pauses or resumes sync activity") Accessible.onPressAction: { Systray.pauseResumeSync() } } MenuItem { text: qsTr("Settings") font.pixelSize: Style.topLinePixelSize hoverEnabled: true onClicked: Systray.openSettings() background: Item { height: parent.height width: parent.menu.width Rectangle { anchors.fill: parent anchors.margins: 1 color: parent.parent.hovered ? Style.lightHover : "transparent" } } Accessible.role: Accessible.MenuItem Accessible.name: text Accessible.description: qsTr("Button that opens the client settings window") Accessible.onPressAction: { Systray.openSettings() } } MenuItem { text: qsTr("Exit"); font.pixelSize: Style.topLinePixelSize hoverEnabled: true onClicked: Systray.shutdown() background: Item { height: parent.height width: parent.menu.width Rectangle { anchors.fill: parent anchors.margins: 1 color: parent.parent.hovered ? Style.lightHover : "transparent" } } Accessible.role: Accessible.MenuItem Accessible.name: text Accessible.description: qsTr("Quits the nextcloud client application") Accessible.onPressAction: { Systray.shutdown() } } } } background: Rectangle { color: accountBtnMouseArea.containsMouse ? "white" : "transparent" opacity: 0.2 } RowLayout { id: accountControlRowLayout height: Style.trayWindowHeaderHeight width: Style.currentAccountButtonWidth spacing: 0 Image { id: currentAccountAvatar Layout.leftMargin: 8 verticalAlignment: Qt.AlignCenter cache: false source: "image://avatars/currentUser" Layout.preferredHeight: Style.accountAvatarSize Layout.preferredWidth: Style.accountAvatarSize Accessible.role: Accessible.Graphic Accessible.name: qsTr("Current user avatar") Rectangle { id: currentAccountStateIndicatorBackground width: Style.accountAvatarStateIndicatorSize + 2 height: width anchors.bottom: currentAccountAvatar.bottom anchors.right: currentAccountAvatar.right color: Style.ncBlue radius: width*0.5 } Image { id: currentAccountStateIndicator source: UserModel.isUserConnected(UserModel.currentUserId()) ? "qrc:///client/theme/colored/state-ok.svg" : "qrc:///client/theme/colored/state-offline.svg" cache: false x: currentAccountStateIndicatorBackground.x + 1 y: currentAccountStateIndicatorBackground.y + 1 sourceSize.width: Style.accountAvatarStateIndicatorSize sourceSize.height: Style.accountAvatarStateIndicatorSize Accessible.role: Accessible.Indicator Accessible.name: UserModel.isUserConnected(UserModel.currentUserId()) ? qsTr("Connected") : qsTr("Disconnected") Accessible.description: qsTr("Icon that indicates connection state for the currently selected account") } } Column { id: accountLabels spacing: 4 Layout.alignment: Qt.AlignLeft Layout.leftMargin: 6 Label { id: currentAccountUser width: Style.currentAccountLabelWidth text: UserModel.currentUser.name elide: Text.ElideRight color: "white" font.pixelSize: Style.topLinePixelSize font.bold: true } Label { id: currentAccountServer width: Style.currentAccountLabelWidth text: UserModel.currentUser.server elide: Text.ElideRight color: "white" font.pixelSize: Style.subLinePixelSize } } Image { Layout.alignment: Qt.AlignRight verticalAlignment: Qt.AlignCenter Layout.margins: Style.accountDropDownCaretMargin source: "qrc:///client/theme/white/caret-down.svg" sourceSize.width: Style.accountDropDownCaretSize sourceSize.height: Style.accountDropDownCaretSize } } } // Filler between account dropdown and header app buttons Item { id: trayWindowHeaderSpacer Layout.fillWidth: true } HeaderButton { id: openLocalFolderButton visible: UserModel.currentUser.hasLocalFolder icon.source: "qrc:///client/theme/white/folder.svg" onClicked: UserModel.openCurrentAccountLocalFolder() Accessible.role: Accessible.Button Accessible.name: qsTr("Local folder button") Accessible.description: qsTr("Opens the local folder corresponding to the currently selected account") Accessible.onPressAction: { UserModel.openCurrentAccountLocalFolder() } } HeaderButton { id: trayWindowTalkButton visible: UserModel.currentUser.serverHasTalk icon.source: "qrc:///client/theme/white/talk-app.svg" onClicked: UserModel.openCurrentAccountTalk() Accessible.role: Accessible.Button Accessible.name: qsTr("Open Nextcloud Talk") Accessible.description: qsTr("Opens Nextcloud Talk corresponding the currently sdelected users server in a new browser window") Accessible.onPressAction: { UserModel.openCurrentAccountTalk() } } HeaderButton { id: trayWindowAppsButton icon.source: "qrc:///client/theme/white/more-apps.svg" onClicked: { if(appsMenu.count <= 0) { UserModel.openCurrentAccountServer() } else if (appsMenu.visible) { appsMenu.close() } else { appsMenu.open() } } Accessible.role: Accessible.ButtonMenu Accessible.name: qsTr("Show other apps") Accessible.description: qsTr("Opens a popup menu with all other supported Nextcloud apps from the currently selected users server") Accessible.onPressAction: { appsMenu.open(); UserModel.openCurrentAccountServer(); } Menu { id: appsMenu y: (trayWindowAppsButton.y + trayWindowAppsButton.height + 2) readonly property Item listContentItem: contentItem.contentItem width: Math.min(listContentItem.childrenRect.width + 4, Style.trayWindowWidth / 2) height: Math.min(implicitHeight, maxMenuHeight) closePolicy: Menu.CloseOnPressOutsideParent | Menu.CloseOnEscape background: Rectangle { border.color: Style.menuBorder radius: 2 } Accessible.role: Accessible.PopupMenu Accessible.name: qsTr("Apps menu") Instantiator { id: appsMenuInstantiator model: UserAppsModel onObjectAdded: appsMenu.insertItem(index, object) onObjectRemoved: appsMenu.removeItem(object) delegate: MenuItem { id: appEntry text: appName font.pixelSize: Style.topLinePixelSize icon.source: appIconUrl width: contentItem.implicitWidth + leftPadding + rightPadding onTriggered: UserAppsModel.openAppUrl(appUrl) hoverEnabled: true background: Item { width: appsMenu.width height: parent.height Rectangle { anchors.fill: parent anchors.margins: 1 color: appEntry.hovered ? Style.lightHover : "transparent" } } Accessible.role: Accessible.MenuItem Accessible.name: appName Accessible.description: qsTr("Opens the corresponding Nextcloud app in a new browser window") Accessible.onPressAction: { UserAppsModel.openAppUrl(appUrl) } } } } } } } // Rectangle trayWindowHeaderBackground ListView { id: activityListView anchors.top: trayWindowHeaderBackground.bottom anchors.horizontalCenter: trayWindowBackground.horizontalCenter width: Style.trayWindowWidth - Style.trayWindowBorderWidth height: Style.trayWindowHeight - Style.trayWindowHeaderHeight clip: true ScrollBar.vertical: ScrollBar { id: listViewScrollbar } keyNavigationEnabled: true Accessible.role: Accessible.List Accessible.name: qsTr("Activity list") Accessible.description: qsTr("A list of activities for the currently selected user account") model: activityModel delegate: RowLayout { id: activityItem width: parent.width height: Style.trayWindowHeaderHeight spacing: 0 Accessible.role: Accessible.ListItem Accessible.name: activityTextColumn.text Accessible.description: qsTr("Activity item - if applicable opens local folder containing the corresponding sync object when clicked") Accessible.onPressAction: { if (path !== "") { Qt.openUrlExternally(path) } else { Qt.openUrlExternally(link) } } MouseArea { id: activityMouseArea enabled: (path !== "" || link !== "") anchors.left: activityItem.left anchors.right: ((shareButton.visible) ? shareButton.left : activityItem.right) height: parent.height anchors.margins: 2 hoverEnabled: true onClicked: { if (path !== "") { Qt.openUrlExternally(path) } else { Qt.openUrlExternally(link) } } Rectangle { anchors.fill: parent color: (parent.containsMouse ? Style.lightHover : "transparent") } } Image { id: activityIcon anchors.left: activityItem.left anchors.leftMargin: 8 anchors.rightMargin: 8 Layout.preferredWidth: shareButton.icon.width Layout.preferredHeight: shareButton.icon.height verticalAlignment: Qt.AlignCenter cache: true source: icon sourceSize.height: 64 sourceSize.width: 64 } Column { id: activityTextColumn anchors.left: activityIcon.right anchors.leftMargin: 8 spacing: 4 Layout.alignment: Qt.AlignLeft Text { id: activityTextTitle text: (type === "Activity" || type === "Notification") ? subject : message width: Style.activityLabelBaseWidth + ((path === "") ? activityItem.height : 0) + ((link === "") ? activityItem.height : 0) - 8 elide: Text.ElideRight font.pixelSize: Style.topLinePixelSize color: activityTextTitleColor } Text { id: activityTextInfo text: (type === "Sync") ? displayPath : (type === "File") ? subject : (type === "Notification") ? message : "" height: (text === "") ? 0 : activityTextTitle.height width: Style.activityLabelBaseWidth + ((path === "") ? activityItem.height : 0) + ((link === "") ? activityItem.height : 0) - 8 elide: Text.ElideRight font.pixelSize: Style.subLinePixelSize } Text { id: activityTextDateTime text: dateTime height: (text === "") ? 0 : activityTextTitle.height width: Style.activityLabelBaseWidth + ((path === "") ? activityItem.height : 0) + ((link === "") ? activityItem.height : 0) - 8 elide: Text.ElideRight font.pixelSize: Style.subLinePixelSize color: "#808080" } ToolTip { id: toolTip visible: activityMouseArea.containsMouse text: activityTextTitle.text + ((activityTextInfo.text !== "") ? "\n\n" + activityTextInfo.text : "") delay: 250 timeout: 10000 // Can be dropped on more recent Qt, but on 5.12 it doesn't wrap... contentItem: Text { text: toolTip.text font: toolTip.font wrapMode: Text.Wrap color: toolTip.palette.toolTipText } } } Button { id: shareButton anchors.right: activityItem.right Layout.preferredWidth: (path === "") ? 0 : parent.height Layout.preferredHeight: parent.height Layout.alignment: Qt.AlignRight flat: true hoverEnabled: true visible: (path === "") ? false : true display: AbstractButton.IconOnly icon.source: "qrc:///client/theme/share.svg" icon.color: "transparent" background: Rectangle { color: parent.hovered ? Style.lightHover : "transparent" } ToolTip.visible: hovered ToolTip.delay: 1000 ToolTip.text: qsTr("Open share dialog") onClicked: Systray.openShareDialog(displayPath,absolutePath) Accessible.role: Accessible.Button Accessible.name: qsTr("Share button") Accessible.description: qsTr("Opens share window for the corresponding sync object") Accessible.onPressAction: { Systray.openShareDialog(displayPath,absolutePath) } } } /*add: Transition { NumberAnimation { properties: "y"; from: -60; duration: 100; easing.type: Easing.Linear } } remove: Transition { NumberAnimation { property: "opacity"; from: 1.0; to: 0; duration: 100 } } removeDisplaced: Transition { SequentialAnimation { PauseAnimation { duration: 100} NumberAnimation { properties: "y"; duration: 100; easing.type: Easing.Linear } } } displaced: Transition { NumberAnimation { properties: "y"; duration: 100; easing.type: Easing.Linear } }*/ } } // Rectangle trayWindowBackground }