Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions resources.qrc
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<file>src/gui/UserStatusWindow.qml</file>
<file>src/gui/WindowAccountHeader.qml</file>
<file>src/gui/activity/qml/ActivitiesWindow.qml</file>
<file>src/gui/activity/qml/ActivitiesWindowMenu.qml</file>
<file>src/gui/AssistantWindow.qml</file>
<file>src/gui/search/SearchWindow.qml</file>
<file>src/gui/UserStatusWindowStatusRow.qml</file>
Expand Down
67 changes: 64 additions & 3 deletions src/gui/WindowAccountHeader.qml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/

import QtQuick
import QtQuick.Controls.Basic as BasicControls
import QtQuick.Layouts

import Style
Expand All @@ -14,14 +15,22 @@ Item {

property string title: ""
property var user: null
// Opt-in: turns the account area into a control that opens an application menu.
property bool accountMenuEnabled: false

signal accountMenuRequested()

readonly property string avatarSource: user && user.avatar !== ""
? user.avatar
: (Style.darkMode ? "image://avatars/fallbackWhite" : "image://avatars/fallbackBlack")
readonly property int accountMenuIndicatorWidth: accountMenuEnabled
? Style.smallIconSize + Style.wizardHeaderRowSpacing
: 0
readonly property int maximumAccountTextWidth: Math.max(0,
Math.round(width * 0.55)
- Style.wizardHeaderAvatarSize
- Style.wizardHeaderRowSpacing)
- Style.wizardHeaderRowSpacing
- accountMenuIndicatorWidth)

implicitHeight: Math.max(titleLabel.implicitHeight, accountRow.implicitHeight)

Expand All @@ -40,9 +49,42 @@ Item {
elide: Text.ElideRight
}

BasicControls.Button {
id: accountMenuButton

objectName: "windowAccountHeaderMenuButton"
anchors.fill: accountRow
anchors.margins: -Style.extraSmallSpacing
z: -1
visible: root.accountMenuEnabled && accountRow.visible
enabled: visible
focusPolicy: Qt.StrongFocus

Accessible.role: Accessible.ButtonMenu
Accessible.name: qsTr("Account switcher and settings menu")
Accessible.onPressAction: accountMenuButton.clicked()

onClicked: root.accountMenuRequested()

background: Rectangle {
radius: Style.mediumRoundedButtonRadius
color: {
if (accountMenuButton.down) {
return Style.wizardSecondaryButtonPressed
}
return accountMenuButton.hovered ? Style.listItemHoverBackground : "transparent"
}
}

HoverHandler {
cursorShape: Qt.PointingHandCursor
}
}

RowLayout {
id: accountRow

objectName: "windowAccountHeaderAccountRow"
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
width: implicitWidth
Expand All @@ -64,16 +106,19 @@ Item {
}

ColumnLayout {
// Round up so that a fractional implicit width does not elide text that fits.
Layout.preferredWidth: Math.min(root.maximumAccountTextWidth,
Math.max(accountNameLabel.implicitWidth,
accountServerLabel.implicitWidth))
Math.ceil(Math.max(accountNameLabel.implicitWidth,
accountServerLabel.implicitWidth)))
Layout.maximumWidth: root.maximumAccountTextWidth
Layout.minimumWidth: 0
spacing: Style.wizardHeaderLabelSpacing

EnforcedPlainTextLabel {
id: accountNameLabel

objectName: "windowAccountHeaderNameLabel"

Layout.fillWidth: true
text: root.user ? root.user.name : ""
color: Style.wizardPrimaryText
Expand All @@ -86,6 +131,8 @@ Item {
EnforcedPlainTextLabel {
id: accountServerLabel

objectName: "windowAccountHeaderServerLabel"

Layout.fillWidth: true
text: root.user ? root.user.server : ""
color: Style.wizardSecondaryText
Expand All @@ -94,5 +141,19 @@ Item {
elide: Text.ElideRight
}
}

Image {
objectName: "windowAccountHeaderCaret"
Layout.preferredWidth: visible ? Style.smallIconSize : 0
Layout.preferredHeight: Style.smallIconSize
visible: root.accountMenuEnabled
source: visible
? "image://svgimage-custom-color/caret-down.svg/" + Style.wizardSecondaryText
: ""
sourceSize.width: Style.smallIconSize
sourceSize.height: Style.smallIconSize
fillMode: Image.PreserveAspectFit
Accessible.ignored: true
}
}
}
13 changes: 13 additions & 0 deletions src/gui/activity/qml/ActivitiesWindow.qml
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,18 @@ WizardStyledWindow {

Shortcut {
sequences: [StandardKey.Cancel]
// Let the application menu handle the key while it is open.
enabled: !applicationMenu.opened
onActivated: root.close()
}

ActivitiesWindowMenu {
id: applicationMenu

anchorItem: accountHeader
onCloseWindowRequested: root.close()
}

Component.onCompleted: resetActivityList()

onVisibleChanged: {
Expand All @@ -51,9 +60,13 @@ WizardStyledWindow {
spacing: Style.wizardSectionSpacing

WindowAccountHeader {
id: accountHeader

Layout.fillWidth: true
title: root.headline
user: root.account
accountMenuEnabled: true
onAccountMenuRequested: applicationMenu.toggleUnder(accountHeader)
}

Rectangle {
Expand Down
103 changes: 103 additions & 0 deletions src/gui/activity/qml/ActivitiesWindowMenu.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: GPL-2.0-or-later
*/

import QtQuick
import QtQuick.Controls.Basic as BasicControls

import Style
import com.nextcloud.desktopclient
import "qrc:/qml/src/gui/wizard/qml"

WizardMenu {
id: root

readonly property bool showAccounts: UserModel.count > 1

// Emitted when this window is replaced by another one, for example after switching account.
signal closeWindowRequested()

function switchToAccount(userIndex) {
if (userIndex === UserModel.currentUserId) {
return
}

UserModel.currentUserId = userIndex
Systray.showActivitiesWindow(userIndex)
root.closeWindowRequested()
}

function toggleUnder(item) {
if (opened) {
close()
return
}

// Keep the menu inside the window by aligning it with the edge the
// account area sits at instead of dropping it below its left side.
const menuX = item.LayoutMirroring.enabled ? 0 : item.width - width
popup(item, menuX, item.height + Style.smallSpacing)
}

objectName: "activitiesApplicationMenu"
width: Style.activitiesWindowMenuWidth

Instantiator {
model: root.showAccounts ? UserModel : null

delegate: WizardMenuItem {
objectName: "activitiesAccountMenuItem"
// UserModel does not report IsCurrentUserRole changes to its views,
// so the current account is derived from currentUserId instead.
text: (model.index === UserModel.currentUserId ? "✓ " : "")
+ "%1 (%2)".arg(model.name).arg(model.server)
onTriggered: root.switchToAccount(model.index)
}

onObjectAdded: (index, object) => root.insertItem(index, object)
onObjectRemoved: (index, object) => root.removeItem(object)
}

BasicControls.MenuSeparator {
objectName: "activitiesAccountsSeparator"
height: visible ? implicitHeight : 0
visible: root.showAccounts
}

WizardMenuItem {
objectName: "activitiesAddAccountMenuItem"
height: visible ? implicitHeight : 0
visible: Systray.enableAddAccount
text: qsTr("Add account")
onTriggered: UserModel.addAccount()
}

WizardMenuItem {
objectName: "activitiesPauseSyncMenuItem"
height: visible ? implicitHeight : 0
visible: Systray.canPauseSync
text: qsTr("Pause sync for all")
onTriggered: Systray.setSyncIsPaused(true)
}

WizardMenuItem {
objectName: "activitiesResumeSyncMenuItem"
height: visible ? implicitHeight : 0
visible: Systray.canResumeSync
text: qsTr("Resume sync for all")
onTriggered: Systray.setSyncIsPaused(false)
}

WizardMenuItem {
objectName: "activitiesSettingsMenuItem"
text: qsTr("Settings")
onTriggered: Systray.openSettings()
}

WizardMenuItem {
objectName: "activitiesQuitMenuItem"
text: qsTr("Quit")
onTriggered: Systray.shutdown()
}
}
15 changes: 15 additions & 0 deletions src/gui/systray.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1025,12 +1025,14 @@ void Systray::slotUpdateSyncPausedState()
if (!folder->syncPaused()) {
_syncIsPaused = false;
Q_EMIT syncIsPausedChanged();
Q_EMIT syncControlStateChanged();
return;
}
}

_syncIsPaused = true;
Q_EMIT syncIsPausedChanged();
Q_EMIT syncControlStateChanged();
}

void Systray::slotUnpauseAllFolders()
Expand All @@ -1048,6 +1050,7 @@ void Systray::slotSyncFoldersChanged(const OCC::Folder::Map &folderMap)
if (const auto currentAnySyncFolders = !folderMap.isEmpty(); currentAnySyncFolders != _anySyncFolders) {
_anySyncFolders = currentAnySyncFolders;
Q_EMIT anySyncFoldersChanged();
Q_EMIT syncControlStateChanged();
}
}

Expand Down Expand Up @@ -1215,6 +1218,18 @@ Systray::SyncControlState Systray::syncControlState() const
return anyPaused ? SyncControlState::Resume : SyncControlState::Pause;
}

bool Systray::canPauseSync() const
{
const auto state = syncControlState();
return state == SyncControlState::Pause || state == SyncControlState::PauseAndResume;
}

bool Systray::canResumeSync() const
{
const auto state = syncControlState();
return state == SyncControlState::Resume || state == SyncControlState::PauseAndResume;
}

/********************************************************************************************/
/* Helper functions for cross-platform tray icon position and taskbar orientation detection */
/********************************************************************************************/
Expand Down
7 changes: 7 additions & 0 deletions src/gui/systray.h
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ class Systray : public QSystemTrayIcon
Q_PROPERTY(bool useNormalWindow READ useNormalWindow CONSTANT)
Q_PROPERTY(bool syncIsPaused READ syncIsPaused WRITE setSyncIsPaused NOTIFY syncIsPausedChanged)
Q_PROPERTY(bool anySyncFolders READ anySyncFolders NOTIFY anySyncFoldersChanged)
Q_PROPERTY(bool canPauseSync READ canPauseSync NOTIFY syncControlStateChanged)
Q_PROPERTY(bool canResumeSync READ canResumeSync NOTIFY syncControlStateChanged)
Q_PROPERTY(bool isOpen READ isOpen WRITE setIsOpen NOTIFY isOpenChanged)
Q_PROPERTY(bool enableAddAccount READ enableAddAccount CONSTANT)

Expand Down Expand Up @@ -102,6 +104,10 @@ class Systray : public QSystemTrayIcon
[[nodiscard]] bool anySyncFolders() const;
/** @brief Returns the actions that the global tray synchronization control should offer. */
[[nodiscard]] SyncControlState syncControlState() const;
/** @brief Returns whether the global synchronization control offers pausing all folders. */
[[nodiscard]] bool canPauseSync() const;
/** @brief Returns whether the global synchronization control offers resuming all folders. */
[[nodiscard]] bool canResumeSync() const;
[[nodiscard]] bool isOpen() const;
[[nodiscard]] bool isActivitySurfaceVisible() const;
void setTrayContextMenuVisible(const bool visible);
Expand All @@ -127,6 +133,7 @@ class Systray : public QSystemTrayIcon

void syncIsPausedChanged();
void anySyncFoldersChanged();
void syncControlStateChanged();
void isOpenChanged();

void hideSettingsDialog();
Expand Down
15 changes: 15 additions & 0 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ nextcloud_add_test(SetUserStatusDialog)
nextcloud_add_test(TrayAccountMenuPolicy)
nextcloud_add_test(TrayActivationPolicy)
nextcloud_add_test(TrayAccountPopupPresentation)
nextcloud_add_test(SystraySyncControl)
if(NOT APPLE)
nextcloud_add_test(SystraySyncControlQt)
endif()
Expand All @@ -135,6 +136,20 @@ add_test(NAME SearchQmlTest
WORKING_DIRECTORY "${CMAKE_BINARY_DIR}/bin"
)
set_tests_properties(SearchQmlTest PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
add_executable(ActivitiesMenuQmlTest qml/activitiesmenu/activitiesmenuqmltestrunner.cpp)
target_link_libraries(ActivitiesMenuQmlTest PRIVATE nextcloudCore Qt::QuickTest)
target_compile_definitions(ActivitiesMenuQmlTest PRIVATE
ACTIVITIES_MENU_QML_TEST_IMPORT_PATH="${CMAKE_CURRENT_SOURCE_DIR}/qml/activitiesmenu/imports"
)
set_target_properties(ActivitiesMenuQmlTest PROPERTIES
RUNTIME_OUTPUT_DIRECTORY ${BIN_OUTPUT_DIRECTORY}
FOLDER Tests
)
add_test(NAME ActivitiesMenuQmlTest
COMMAND ActivitiesMenuQmlTest -input "${CMAKE_CURRENT_SOURCE_DIR}/qml/activitiesmenu/testactivitiesmenu.qml"
WORKING_DIRECTORY "${CMAKE_BINARY_DIR}/bin"
)
set_tests_properties(ActivitiesMenuQmlTest PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
nextcloud_add_test(TalkReply)
nextcloud_add_test(LockFile)
nextcloud_add_test(ShareModel)
Expand Down
Loading