From 3ae76ce7ea13d67018c7dc44accc6c61ed8190e0 Mon Sep 17 00:00:00 2001 From: Rello Date: Wed, 8 Jul 2026 21:38:31 +0200 Subject: [PATCH 1/4] feat: add search dialog to new tray menu Signed-off-by: Rello --- resources.qrc | 1 + src/gui/SearchWindow.qml | 202 ++++++++++++++++++ .../trayaccountpopup/ncaccountactionspopup.h | 4 +- .../trayaccountpopup/ncaccountactionspopup.mm | 9 + src/gui/macOS/trayaccountpopup/nctraypopup.h | 2 + src/gui/macOS/trayaccountpopup/nctraypopup.mm | 7 + src/gui/systray.cpp | 77 +++++++ src/gui/systray.h | 2 + src/gui/trayaccountpopup_qt.cpp | 14 ++ theme/Style/Style.qml | 2 + 10 files changed, 318 insertions(+), 2 deletions(-) create mode 100644 src/gui/SearchWindow.qml diff --git a/resources.qrc b/resources.qrc index 122a9487a4d21..cd40475f1a520 100644 --- a/resources.qrc +++ b/resources.qrc @@ -6,6 +6,7 @@ src/gui/WindowAccountHeader.qml src/gui/ActivitiesWindow.qml src/gui/AssistantWindow.qml + src/gui/SearchWindow.qml src/gui/UserStatusWindowStatusRow.qml src/gui/UserStatusWindowPredefinedStatusRow.qml src/gui/UserStatusSelectorPage.qml diff --git a/src/gui/SearchWindow.qml b/src/gui/SearchWindow.qml new file mode 100644 index 0000000000000..1ac322185f7cc --- /dev/null +++ b/src/gui/SearchWindow.qml @@ -0,0 +1,202 @@ +/* + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Controls.Basic +import QtQuick.Layouts + +import Style +import "./tray" + +WizardStyledWindow { + id: root + + property int userIndex: -1 + property var currentUser: null + property var searchModel: null + readonly property string headline: qsTr("Search") + readonly property bool isFetchMoreInProgress: searchModel !== null + && searchModel.currentFetchMoreInProgressProviderId.length > 0 + readonly property bool isSearchInProgress: searchModel !== null + && searchModel.isSearchInProgress + readonly property bool waitingForSearchTermEditEnd: searchModel !== null + && searchModel.waitingForSearchTermEditEnd + readonly property bool hasSearchTerm: searchModel !== null + && searchModel.searchTerm.length > 0 + readonly property bool hasSearchError: searchModel !== null + && searchModel.errorString.length > 0 + readonly property bool canEditSearch: currentUser !== null + && currentUser.isConnected + && searchModel !== null + && !isFetchMoreInProgress + readonly property bool showResults: searchModel !== null + && searchResultsListView.count > 0 + readonly property bool showNothingFound: hasSearchTerm + && !isSearchInProgress + && !waitingForSearchTermEditEnd + && !hasSearchError + && searchResultsListView.count === 0 + readonly property bool showPlaceholder: !hasSearchTerm + && !hasSearchError + readonly property bool showSearchError: hasSearchError + && !showResults + && !isSearchInProgress + && !isFetchMoreInProgress + readonly property bool showSkeleton: hasSearchTerm + && !showNothingFound + && !showResults + && !hasSearchError + + title: "" + width: Style.searchWindowWidth + height: Style.searchWindowHeight + minimumWidth: Style.wizardStandaloneWindowMinimumWidth + minimumHeight: Style.wizardStandaloneWindowMinimumHeight + + function focusSearchInput() { + if (visible && searchInput.enabled) { + searchInput.forceActiveFocus() + } + } + + Shortcut { + sequences: [StandardKey.Cancel] + onActivated: root.close() + } + + Component.onCompleted: Qt.callLater(focusSearchInput) + onVisibleChanged: { + if (visible) { + Qt.callLater(focusSearchInput) + } + } + + ColumnLayout { + anchors.fill: parent + anchors.leftMargin: Style.wizardWindowMargin + anchors.rightMargin: Style.wizardWindowMargin + anchors.topMargin: Style.wizardWindowTopMargin + anchors.bottomMargin: Style.wizardWindowMargin + spacing: Style.wizardSectionSpacing + + WindowAccountHeader { + Layout.fillWidth: true + title: root.headline + user: root.currentUser + } + + UnifiedSearchInputContainer { + id: searchInput + + Layout.fillWidth: true + Layout.preferredHeight: Style.unifiedSearchInputContainerHeight + enabled: root.searchModel !== null + readOnly: !root.canEditSearch + text: root.searchModel ? root.searchModel.searchTerm : "" + placeholderText: root.currentUser !== null && !root.currentUser.isConnected + ? qsTr("Search is available when this account is connected") + : qsTr("Search files, messages, events …") + isSearchInProgress: root.isSearchInProgress + onTextEdited: { + if (root.searchModel) { + root.searchModel.searchTerm = searchInput.text + } + } + onClearText: { + if (root.searchModel) { + root.searchModel.searchTerm = "" + } + } + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: Style.normalBorderWidth + color: Style.wizardRowBorder + } + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + + ErrorBox { + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + visible: root.showSearchError + text: root.searchModel ? root.searchModel.errorString : "" + } + + UnifiedSearchPlaceholderView { + anchors.fill: parent + visible: root.showPlaceholder + } + + UnifiedSearchResultNothingFound { + anchors.fill: parent + visible: root.showNothingFound + text: root.searchModel ? root.searchModel.searchTerm : "" + } + + Loader { + anchors.fill: parent + anchors.margins: Style.smallSpacing + active: root.showSkeleton + asynchronous: true + + sourceComponent: UnifiedSearchResultItemSkeletonContainer { + anchors.fill: parent + spacing: searchResultsListView.spacing + animationRectangleWidth: root.width + } + } + + ScrollView { + id: searchResultsScrollView + + anchors.fill: parent + contentWidth: availableWidth + visible: root.showResults + + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + + ListView { + id: searchResultsListView + + spacing: Style.smallSpacing + clip: true + keyNavigationEnabled: true + reuseItems: true + model: root.searchModel + + Accessible.role: Accessible.List + Accessible.name: qsTr("Search results list") + + delegate: UnifiedSearchResultListItem { + width: searchResultsListView.width + isSearchInProgress: root.isSearchInProgress + currentFetchMoreInProgressProviderId: root.searchModel + ? root.searchModel.currentFetchMoreInProgressProviderId + : "" + fetchMoreTriggerClicked: root.searchModel + ? root.searchModel.fetchMoreTriggerClicked + : function() {} + resultClicked: root.searchModel + ? root.searchModel.resultClicked + : function() {} + ListView.onPooled: isPooled = true + ListView.onReused: isPooled = false + } + + section.property: "providerName" + section.criteria: ViewSection.FullString + section.delegate: UnifiedSearchResultSectionItem { + width: searchResultsListView.width + } + } + } + } + } +} diff --git a/src/gui/macOS/trayaccountpopup/ncaccountactionspopup.h b/src/gui/macOS/trayaccountpopup/ncaccountactionspopup.h index 3b0a858482500..9a517374662cd 100644 --- a/src/gui/macOS/trayaccountpopup/ncaccountactionspopup.h +++ b/src/gui/macOS/trayaccountpopup/ncaccountactionspopup.h @@ -14,8 +14,8 @@ /** * @brief The submenu shown for a single account. * - * Lists the user status, "Reveal in Finder", the Assistant and Apps shortcuts, - * pending notifications and recent activity. Owns the apps and + * Lists the user status, "Reveal in Finder", the Assistant, Search and Apps + * shortcuts, pending notifications and recent activity. Owns the apps and * notification-actions sub-popups. */ @interface NCAccountActionsPopup : NSPanel diff --git a/src/gui/macOS/trayaccountpopup/ncaccountactionspopup.mm b/src/gui/macOS/trayaccountpopup/ncaccountactionspopup.mm index eaca7e5906302..1c3be0e5b6279 100644 --- a/src/gui/macOS/trayaccountpopup/ncaccountactionspopup.mm +++ b/src/gui/macOS/trayaccountpopup/ncaccountactionspopup.mm @@ -239,6 +239,7 @@ - (void)populateForUserIndex:(int)userIndex owner:(NCTrayPopup *)owner refreshAc appsModel->setUserId(userIndex); const auto appsEnabled = appsModel->rowCount() > 0; const auto assistantEnabled = model->data(userModelIndex, OCC::UserModel::AssistantEnabledRole).toBool(); + const auto searchEnabled = model->data(userModelIndex, OCC::UserModel::IsConnectedRole).toBool(); addOwnedArrangedSubview(_stack, [[NCSpacerView alloc] initWithHeight:kActionVerticalPadding width:kAccountActionsPopupWidth]); if (serverHasUserStatus) { const auto status = model->data(userModelIndex, OCC::UserModel::StatusRole).value(); @@ -275,6 +276,14 @@ - (void)populateForUserIndex:(int)userIndex owner:(NCTrayPopup *)owner refreshAc [weakSelf hideAppsPopup]; }]); } + addOwnedArrangedSubview(_stack, [[NCActionRow alloc] initWithTitle:QCoreApplication::translate("TrayAccountPopup", "Search").toNSString() + width:kAccountActionsPopupWidth + enabled:searchEnabled + action:^{ + [weakOwner openSearchForIndex:userIndex]; + } hoverAction:^(NSView *) { + [weakSelf hideAppsPopup]; + }]); addOwnedArrangedSubview(_stack, [[NCActionRow alloc] initWithTitle:QCoreApplication::translate("TrayWindowHeader", "Apps").toNSString() icon:nil width:kAccountActionsPopupWidth diff --git a/src/gui/macOS/trayaccountpopup/nctraypopup.h b/src/gui/macOS/trayaccountpopup/nctraypopup.h index ec85f79dde008..cb538e66e387d 100644 --- a/src/gui/macOS/trayaccountpopup/nctraypopup.h +++ b/src/gui/macOS/trayaccountpopup/nctraypopup.h @@ -31,6 +31,8 @@ - (void)openLocalFolderForIndex:(int)index; /** @brief Closes the popups and opens the Assistant window for the given account. */ - (void)openAssistantForIndex:(int)index; +/** @brief Closes the popups and opens the Search window for the given account. */ +- (void)openSearchForIndex:(int)index; /** @brief Closes the popups and opens the user-status window for the given account. */ - (void)openOnlineStatusForIndex:(int)index; @end diff --git a/src/gui/macOS/trayaccountpopup/nctraypopup.mm b/src/gui/macOS/trayaccountpopup/nctraypopup.mm index 5594f4dccfcfd..10d1da9d41cd0 100644 --- a/src/gui/macOS/trayaccountpopup/nctraypopup.mm +++ b/src/gui/macOS/trayaccountpopup/nctraypopup.mm @@ -303,6 +303,13 @@ - (void)openAssistantForIndex:(int)index OCC::Systray::instance()->showAssistantWindow(index); } +- (void)openSearchForIndex:(int)index +{ + [_accountActionsPopup orderOut:nil]; + [self orderOut:nil]; + OCC::Systray::instance()->showSearchWindow(index); +} + - (void)openOnlineStatusForIndex:(int)index { [_accountActionsPopup orderOut:nil]; diff --git a/src/gui/systray.cpp b/src/gui/systray.cpp index 78d2596e1ab66..2a4b885448988 100644 --- a/src/gui/systray.cpp +++ b/src/gui/systray.cpp @@ -10,6 +10,7 @@ #include "config.h" #include "common/utility.h" #include "tray/svgimageprovider.h" +#include "tray/unifiedsearchresultslistmodel.h" #include "tray/usermodel.h" #include "wheelhandler.h" #include "tray/trayimageprovider.h" @@ -389,6 +390,82 @@ void Systray::showAssistantWindow(int userIndex) window->requestActivate(); } +void Systray::showSearchWindow(int userIndex) +{ + const auto userModel = UserModel::instance(); + if (!userModel) { + return; + } + + const auto targetUserId = userIndex >= 0 ? userIndex : userModel->currentUserId(); + const auto user = userModel->user(targetUserId); + if (!user) { + qCWarning(lcSystray) << "Invalid user index for search window:" << targetUserId; + return; + } + + hideWindow(); + + if (!_trayEngine) { + qCWarning(lcSystray) << "Could not open search window as no tray engine was available"; + return; + } + + const auto windowKey = user->account()->id(); + + if (const auto existingWindow = _searchWindows.value(windowKey)) { + positionWindowAtScreenCenter(existingWindow.data()); + existingWindow->show(); + existingWindow->raise(); + existingWindow->requestActivate(); + return; + } + + const QVariantMap initialProperties{ + {"userIndex", targetUserId}, + {"currentUser", QVariant::fromValue(user)}, + {"searchModel", QVariant::fromValue(user->getUnifiedSearchResultsListModel())}, + }; + QQmlComponent searchWindowComponent(trayEngine(), QStringLiteral("qrc:/qml/src/gui/SearchWindow.qml")); + + if (searchWindowComponent.isError()) { + qCWarning(lcSystray) << searchWindowComponent.errorString(); + qCWarning(lcSystray) << searchWindowComponent.errors(); + return; + } + + const auto createdObject = searchWindowComponent.createWithInitialProperties(initialProperties); + const auto window = qobject_cast(createdObject); + if (!window) { + qCWarning(lcSystray) << "Search window resulted in creation of object that was not a window!"; + if (createdObject) { + createdObject->deleteLater(); + } + return; + } + + _searchWindows.insert(windowKey, window); + window->setIcon(Theme::instance()->applicationIcon()); + +#ifdef Q_OS_MACOS + auto *fgbg = new ForegroundBackground(this); + window->installEventFilter(fgbg); +#endif + +#if defined(Q_OS_MACOS) + configureMacOSExpandedQuickWindow(window); +#endif + + connect(window, &QObject::destroyed, this, [this, windowKey] { + _searchWindows.remove(windowKey); + }); + + positionWindowAtScreenCenter(window); + window->show(); + window->raise(); + window->requestActivate(); +} + void Systray::showUserStatusWindow(int userIndex) { const auto userModel = UserModel::instance(); diff --git a/src/gui/systray.h b/src/gui/systray.h index bf3e4e93ebaf9..0af43bc8b17df 100644 --- a/src/gui/systray.h +++ b/src/gui/systray.h @@ -154,6 +154,7 @@ public slots: void showQMLWindow(); void showActivitiesWindow(int userIndex = -1); void showAssistantWindow(int userIndex = -1); + void showSearchWindow(int userIndex = -1); void showUserStatusWindow(int userIndex); void setSyncIsPaused(const bool syncIsPaused); @@ -222,6 +223,7 @@ private slots: QPointer _contextMenu; QHash> _activitiesWindows; QHash> _assistantWindows; + QHash> _searchWindows; QPointer _userStatusWindow; AccessManagerFactory _accessManagerFactory; diff --git a/src/gui/trayaccountpopup_qt.cpp b/src/gui/trayaccountpopup_qt.cpp index 6f078d0e7dc64..8b80b8dda14f1 100644 --- a/src/gui/trayaccountpopup_qt.cpp +++ b/src/gui/trayaccountpopup_qt.cpp @@ -521,6 +521,12 @@ void openAssistantForUser(const int userId) Systray::instance()->showAssistantWindow(userId); } +void openSearchForUser(const int userId) +{ + closeTrayPopup(); + Systray::instance()->showSearchWindow(userId); +} + void openSettingsAfterTrayPopupCloses() { closeTrayPopup(); @@ -746,6 +752,14 @@ void populateAccountMenu(QMenu *menu, const int userId, const bool fetchActivity }); } + const auto searchAction = addMenuAction(menu, + templateBlackThemeIcon(QStringLiteral("search.svg"), menuIconSize, menuIconPalette), + QCoreApplication::translate("TrayAccountPopup", "Search")); + searchAction->setEnabled(userModel->data(userModelIndex, UserModel::IsConnectedRole).toBool()); + QObject::connect(searchAction, &QAction::triggered, searchAction, [userId] { + openSearchForUser(userId); + }); + const auto appsMenu = addSubMenu(menu, templateBlackThemeIcon(QStringLiteral("more-apps.svg"), menuIconSize, menuIconPalette), QCoreApplication::translate("TrayWindowHeader", "Apps")); diff --git a/theme/Style/Style.qml b/theme/Style/Style.qml index 8ce44934defd5..b73d60e9a7cfb 100644 --- a/theme/Style/Style.qml +++ b/theme/Style/Style.qml @@ -146,6 +146,8 @@ QtObject { readonly property int activitiesWindowHeight: 700 readonly property int assistantWindowWidth: 640 readonly property int assistantWindowHeight: 620 + readonly property int searchWindowWidth: 640 + readonly property int searchWindowHeight: 620 readonly property int userStatusWindowWidth: 560 readonly property int userStatusWindowHeight: 700 readonly property int userStatusWindowMinimumHeight: 560 From 6f562f6322855e712407f3984eaf557942ea2061 Mon Sep 17 00:00:00 2001 From: Camila Ayres Date: Thu, 9 Jul 2026 11:02:23 +0000 Subject: [PATCH 2/4] refactor(search): move search window state derivation into the model Expose isFetchMoreInProgress, hasSearchTerm, hasSearchError, canEditSearch and a computed SearchState enum as model properties, replacing the derived booleans in SearchWindow.qml. Assisted-by: Claude:claude-opus-4-8 Signed-off-by: Camila Ayres --- src/gui/SearchWindow.qml | 47 ++++------------ .../tray/unifiedsearchresultslistmodel.cpp | 54 +++++++++++++++++++ src/gui/tray/unifiedsearchresultslistmodel.h | 23 ++++++++ 3 files changed, 88 insertions(+), 36 deletions(-) diff --git a/src/gui/SearchWindow.qml b/src/gui/SearchWindow.qml index 1ac322185f7cc..4ebe9665ae61c 100644 --- a/src/gui/SearchWindow.qml +++ b/src/gui/SearchWindow.qml @@ -8,6 +8,7 @@ import QtQuick.Controls.Basic import QtQuick.Layouts import Style +import com.nextcloud.desktopclient import "./tray" WizardStyledWindow { @@ -17,37 +18,11 @@ WizardStyledWindow { property var currentUser: null property var searchModel: null readonly property string headline: qsTr("Search") - readonly property bool isFetchMoreInProgress: searchModel !== null - && searchModel.currentFetchMoreInProgressProviderId.length > 0 - readonly property bool isSearchInProgress: searchModel !== null - && searchModel.isSearchInProgress - readonly property bool waitingForSearchTermEditEnd: searchModel !== null - && searchModel.waitingForSearchTermEditEnd - readonly property bool hasSearchTerm: searchModel !== null - && searchModel.searchTerm.length > 0 - readonly property bool hasSearchError: searchModel !== null - && searchModel.errorString.length > 0 - readonly property bool canEditSearch: currentUser !== null - && currentUser.isConnected - && searchModel !== null - && !isFetchMoreInProgress - readonly property bool showResults: searchModel !== null - && searchResultsListView.count > 0 - readonly property bool showNothingFound: hasSearchTerm - && !isSearchInProgress - && !waitingForSearchTermEditEnd - && !hasSearchError - && searchResultsListView.count === 0 - readonly property bool showPlaceholder: !hasSearchTerm - && !hasSearchError - readonly property bool showSearchError: hasSearchError - && !showResults - && !isSearchInProgress - && !isFetchMoreInProgress - readonly property bool showSkeleton: hasSearchTerm - && !showNothingFound - && !showResults - && !hasSearchError + readonly property int searchState: searchModel + ? searchModel.searchState + : UnifiedSearchResultsListModel.Placeholder + readonly property bool isSearchInProgress: searchModel !== null && searchModel.isSearchInProgress + readonly property bool canEditSearch: searchModel !== null && searchModel.canEditSearch title: "" width: Style.searchWindowWidth @@ -125,25 +100,25 @@ WizardStyledWindow { anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top - visible: root.showSearchError + visible: root.searchState === UnifiedSearchResultsListModel.SearchError text: root.searchModel ? root.searchModel.errorString : "" } UnifiedSearchPlaceholderView { anchors.fill: parent - visible: root.showPlaceholder + visible: root.searchState === UnifiedSearchResultsListModel.Placeholder } UnifiedSearchResultNothingFound { anchors.fill: parent - visible: root.showNothingFound + visible: root.searchState === UnifiedSearchResultsListModel.NothingFound text: root.searchModel ? root.searchModel.searchTerm : "" } Loader { anchors.fill: parent anchors.margins: Style.smallSpacing - active: root.showSkeleton + active: root.searchState === UnifiedSearchResultsListModel.Skeleton asynchronous: true sourceComponent: UnifiedSearchResultItemSkeletonContainer { @@ -158,7 +133,7 @@ WizardStyledWindow { anchors.fill: parent contentWidth: availableWidth - visible: root.showResults + visible: root.searchState === UnifiedSearchResultsListModel.Results ScrollBar.horizontal.policy: ScrollBar.AlwaysOff diff --git a/src/gui/tray/unifiedsearchresultslistmodel.cpp b/src/gui/tray/unifiedsearchresultslistmodel.cpp index 424bc4c191a97..0be8dbd946544 100644 --- a/src/gui/tray/unifiedsearchresultslistmodel.cpp +++ b/src/gui/tray/unifiedsearchresultslistmodel.cpp @@ -189,6 +189,19 @@ UnifiedSearchResultsListModel::UnifiedSearchResultsListModel(AccountState *accou : QAbstractListModel(parent) , _accountState(accountState) { + connect(this, &UnifiedSearchResultsListModel::isSearchInProgressChanged, this, &UnifiedSearchResultsListModel::searchStateChanged); + connect(this, &UnifiedSearchResultsListModel::currentFetchMoreInProgressProviderIdChanged, this, &UnifiedSearchResultsListModel::searchStateChanged); + connect(this, &UnifiedSearchResultsListModel::searchTermChanged, this, &UnifiedSearchResultsListModel::searchStateChanged); + connect(this, &UnifiedSearchResultsListModel::errorStringChanged, this, &UnifiedSearchResultsListModel::searchStateChanged); + connect(this, &UnifiedSearchResultsListModel::waitingForSearchTermEditEndChanged, this, &UnifiedSearchResultsListModel::searchStateChanged); + connect(this, &QAbstractListModel::rowsInserted, this, &UnifiedSearchResultsListModel::searchStateChanged); + connect(this, &QAbstractListModel::rowsRemoved, this, &UnifiedSearchResultsListModel::searchStateChanged); + connect(this, &QAbstractListModel::modelReset, this, &UnifiedSearchResultsListModel::searchStateChanged); + + connect(this, &UnifiedSearchResultsListModel::currentFetchMoreInProgressProviderIdChanged, this, &UnifiedSearchResultsListModel::canEditSearchChanged); + if (_accountState) { + connect(_accountState, &AccountState::isConnectedChanged, this, &UnifiedSearchResultsListModel::canEditSearchChanged); + } } QVariant UnifiedSearchResultsListModel::data(const QModelIndex &index, int role) const @@ -326,6 +339,47 @@ bool UnifiedSearchResultsListModel::isSearchInProgress() const return !_searchJobConnections.isEmpty(); } +bool UnifiedSearchResultsListModel::isFetchMoreInProgress() const +{ + return !_currentFetchMoreInProgressProviderId.isEmpty(); +} + +bool UnifiedSearchResultsListModel::hasSearchTerm() const +{ + return !_searchTerm.isEmpty(); +} + +bool UnifiedSearchResultsListModel::hasSearchError() const +{ + return !_errorString.isEmpty(); +} + +bool UnifiedSearchResultsListModel::canEditSearch() const +{ + return _accountState && _accountState->isConnected() && !isFetchMoreInProgress(); +} + +UnifiedSearchResultsListModel::SearchState UnifiedSearchResultsListModel::searchState() const +{ + if (rowCount() > 0) { + return SearchState::Results; + } + + if (hasSearchError()) { + return (!isSearchInProgress() && !isFetchMoreInProgress()) ? SearchState::SearchError : SearchState::None; + } + + if (!hasSearchTerm()) { + return SearchState::Placeholder; + } + + if (!isSearchInProgress() && !waitingForSearchTermEditEnd()) { + return SearchState::NothingFound; + } + + return SearchState::Skeleton; +} + void UnifiedSearchResultsListModel::resultClicked(const QString &providerId, const QUrl &resourceUrl) const { const QUrlQuery urlQuery{resourceUrl}; diff --git a/src/gui/tray/unifiedsearchresultslistmodel.h b/src/gui/tray/unifiedsearchresultslistmodel.h index a2c07eb651fab..2dee666442fb5 100644 --- a/src/gui/tray/unifiedsearchresultslistmodel.h +++ b/src/gui/tray/unifiedsearchresultslistmodel.h @@ -30,6 +30,11 @@ class UnifiedSearchResultsListModel : public QAbstractListModel Q_PROPERTY(QString errorString READ errorString NOTIFY errorStringChanged) Q_PROPERTY(QString searchTerm READ searchTerm WRITE setSearchTerm NOTIFY searchTermChanged) Q_PROPERTY(bool waitingForSearchTermEditEnd READ waitingForSearchTermEditEnd NOTIFY waitingForSearchTermEditEndChanged) + Q_PROPERTY(bool isFetchMoreInProgress READ isFetchMoreInProgress NOTIFY currentFetchMoreInProgressProviderIdChanged) + Q_PROPERTY(bool hasSearchTerm READ hasSearchTerm NOTIFY searchTermChanged) + Q_PROPERTY(bool hasSearchError READ hasSearchError NOTIFY errorStringChanged) + Q_PROPERTY(bool canEditSearch READ canEditSearch NOTIFY canEditSearchChanged) + Q_PROPERTY(SearchState searchState READ searchState NOTIFY searchStateChanged) struct UnifiedSearchProvider { @@ -42,6 +47,16 @@ class UnifiedSearchResultsListModel : public QAbstractListModel }; public: + enum class SearchState { + None, + Placeholder, + Skeleton, + NothingFound, + Results, + SearchError, + }; + Q_ENUM(SearchState) + enum DataRole { ProviderNameRole = Qt::UserRole + 1, ProviderIdRole, @@ -71,6 +86,12 @@ class UnifiedSearchResultsListModel : public QAbstractListModel [[nodiscard]] QString errorString() const; [[nodiscard]] bool waitingForSearchTermEditEnd() const; + [[nodiscard]] bool isFetchMoreInProgress() const; + [[nodiscard]] bool hasSearchTerm() const; + [[nodiscard]] bool hasSearchError() const; + [[nodiscard]] bool canEditSearch() const; + [[nodiscard]] SearchState searchState() const; + Q_INVOKABLE void resultClicked(const QString &providerId, const QUrl &resourceUrl) const; Q_INVOKABLE void fetchMoreTriggerClicked(const QString &providerId); @@ -100,6 +121,8 @@ class UnifiedSearchResultsListModel : public QAbstractListModel void errorStringChanged(); void searchTermChanged(); void waitingForSearchTermEditEndChanged(); + void canEditSearchChanged(); + void searchStateChanged(); public slots: void setSearchTerm(const QString &term); From fcb36e9a43c3b9d1578b377b85ecb3aae21bf314 Mon Sep 17 00:00:00 2001 From: Camila Ayres Date: Thu, 9 Jul 2026 11:08:24 +0000 Subject: [PATCH 3/4] test(search): cover SearchState transitions in the search model Add cases for placeholder, skeleton, results, nothing found and search error states, plus the derived boolean properties. Assisted-by: Claude:claude-opus-4-8 Signed-off-by: Camila Ayres --- test/testunifiedsearchlistmodel.cpp | 111 ++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) diff --git a/test/testunifiedsearchlistmodel.cpp b/test/testunifiedsearchlistmodel.cpp index ed948d2a3993d..92b62379c0754 100644 --- a/test/testunifiedsearchlistmodel.cpp +++ b/test/testunifiedsearchlistmodel.cpp @@ -626,6 +626,117 @@ private slots: QVERIFY(!model->errorString().isEmpty()); } + void testSearchStatePlaceholderWhenNoSearchTerm() + { + model->setSearchTerm(QStringLiteral("")); + QVERIFY(model->rowCount() == 0); + + QVERIFY(!model->hasSearchTerm()); + QVERIFY(!model->hasSearchError()); + QCOMPARE(model->searchState(), OCC::UnifiedSearchResultsListModel::SearchState::Placeholder); + } + + void testSearchStateSkeletonWhileSearching() + { + model->setSearchTerm(QStringLiteral("")); + QVERIFY(model->rowCount() == 0); + + QSignalSpy searchStateChanged(model.data(), &OCC::UnifiedSearchResultsListModel::searchStateChanged); + + model->setSearchTerm(QStringLiteral("dis")); + + // waiting for the edit timer to fire, nothing fetched yet + QVERIFY(model->waitingForSearchTermEditEnd()); + QVERIFY(!model->isSearchInProgress()); + QVERIFY(model->hasSearchTerm()); + QCOMPARE(model->searchState(), OCC::UnifiedSearchResultsListModel::SearchState::Skeleton); + QVERIFY(searchStateChanged.count() > 0); + + QSignalSpy searchInProgressChanged( + model.data(), &OCC::UnifiedSearchResultsListModel::isSearchInProgressChanged); + + // search started but results have not arrived yet + QVERIFY(searchInProgressChanged.wait()); + QVERIFY(model->isSearchInProgress()); + QVERIFY(model->rowCount() == 0); + QCOMPARE(model->searchState(), OCC::UnifiedSearchResultsListModel::SearchState::Skeleton); + + model->setSearchTerm(QStringLiteral("")); + } + + void testSearchStateResultsWhenResultsFound() + { + model->setSearchTerm(QStringLiteral("")); + QVERIFY(model->rowCount() == 0); + + QSignalSpy searchStateChanged(model.data(), &OCC::UnifiedSearchResultsListModel::searchStateChanged); + QSignalSpy searchInProgressChanged( + model.data(), &OCC::UnifiedSearchResultsListModel::isSearchInProgressChanged); + + model->setSearchTerm(QStringLiteral("discuss")); + + QVERIFY(searchInProgressChanged.wait()); + QVERIFY(model->isSearchInProgress()); + + QVERIFY(searchInProgressChanged.wait()); + QVERIFY(!model->isSearchInProgress()); + + QVERIFY(model->rowCount() > 0); + QVERIFY(!model->hasSearchError()); + QVERIFY(!model->isFetchMoreInProgress()); + QCOMPARE(model->searchState(), OCC::UnifiedSearchResultsListModel::SearchState::Results); + QVERIFY(searchStateChanged.count() > 0); + + model->setSearchTerm(QStringLiteral("")); + } + + void testSearchStateNothingFoundWhenNoResults() + { + model->setSearchTerm(QStringLiteral("")); + QVERIFY(model->rowCount() == 0); + + QSignalSpy searchInProgressChanged( + model.data(), &OCC::UnifiedSearchResultsListModel::isSearchInProgressChanged); + + model->setSearchTerm(QStringLiteral("[empty]")); + + QVERIFY(searchInProgressChanged.wait()); + QVERIFY(model->isSearchInProgress()); + + QVERIFY(searchInProgressChanged.wait()); + QVERIFY(!model->isSearchInProgress()); + + QVERIFY(model->rowCount() == 0); + QVERIFY(model->hasSearchTerm()); + QVERIFY(!model->hasSearchError()); + QCOMPARE(model->searchState(), OCC::UnifiedSearchResultsListModel::SearchState::NothingFound); + + model->setSearchTerm(QStringLiteral("")); + } + + void testSearchStateSearchErrorWhenSearchFails() + { + model->setSearchTerm(QStringLiteral("")); + QVERIFY(model->rowCount() == 0); + + QSignalSpy searchInProgressChanged( + model.data(), &OCC::UnifiedSearchResultsListModel::isSearchInProgressChanged); + + model->setSearchTerm(QStringLiteral("[HTTP500]")); + + QVERIFY(searchInProgressChanged.wait()); + QVERIFY(model->isSearchInProgress()); + + QVERIFY(searchInProgressChanged.wait()); + QVERIFY(!model->isSearchInProgress()); + + QVERIFY(model->rowCount() == 0); + QVERIFY(model->hasSearchError()); + QCOMPARE(model->searchState(), OCC::UnifiedSearchResultsListModel::SearchState::SearchError); + + model->setSearchTerm(QStringLiteral("")); + } + void cleanupTestCase() { FakeSearchResultsStorage::destroy(); From 69f48939a39e37bd859a5b91ce8bc92aea13fbc1 Mon Sep 17 00:00:00 2001 From: Rello Date: Fri, 10 Jul 2026 11:56:53 +0200 Subject: [PATCH 4/4] fix: make the search modular by separating UI components Signed-off-by: Rello --- resources.qrc | 22 +-- src/gui/CMakeLists.txt | 8 +- src/gui/owncloudgui.cpp | 2 +- src/gui/{ => search/qml}/SearchWindow.qml | 10 +- .../qml}/UnifiedSearchInputContainer.qml | 3 +- .../qml}/UnifiedSearchPlaceholderView.qml | 1 + .../UnifiedSearchResultFetchMoreTrigger.qml | 1 + .../qml}/UnifiedSearchResultItem.qml | 1 + .../qml}/UnifiedSearchResultItemSkeleton.qml | 0 ...ifiedSearchResultItemSkeletonContainer.qml | 0 ...rchResultItemSkeletonGradientRectangle.qml | 0 .../qml}/UnifiedSearchResultListItem.qml | 0 .../qml}/UnifiedSearchResultNothingFound.qml | 1 + .../qml}/UnifiedSearchResultSectionItem.qml | 1 + .../{tray => search}/unifiedsearchresult.cpp | 0 .../{tray => search}/unifiedsearchresult.h | 0 .../unifiedsearchresultslistmodel.cpp | 15 +- .../unifiedsearchresultslistmodel.h | 3 + src/gui/settingsdialog.cpp | 8 +- src/gui/systray.cpp | 39 ++++- src/gui/tray/MainWindow.qml | 159 +----------------- src/gui/tray/usermodel.cpp | 7 - src/gui/tray/usermodel.h | 4 - test/testunifiedsearchlistmodel.cpp | 2 +- 24 files changed, 82 insertions(+), 205 deletions(-) rename src/gui/{ => search/qml}/SearchWindow.qml (95%) rename src/gui/{tray => search/qml}/UnifiedSearchInputContainer.qml (97%) rename src/gui/{tray => search/qml}/UnifiedSearchPlaceholderView.qml (98%) rename src/gui/{tray => search/qml}/UnifiedSearchResultFetchMoreTrigger.qml (98%) rename src/gui/{tray => search/qml}/UnifiedSearchResultItem.qml (99%) rename src/gui/{tray => search/qml}/UnifiedSearchResultItemSkeleton.qml (100%) rename src/gui/{tray => search/qml}/UnifiedSearchResultItemSkeletonContainer.qml (100%) rename src/gui/{tray => search/qml}/UnifiedSearchResultItemSkeletonGradientRectangle.qml (100%) rename src/gui/{tray => search/qml}/UnifiedSearchResultListItem.qml (100%) rename src/gui/{tray => search/qml}/UnifiedSearchResultNothingFound.qml (98%) rename src/gui/{tray => search/qml}/UnifiedSearchResultSectionItem.qml (97%) rename src/gui/{tray => search}/unifiedsearchresult.cpp (100%) rename src/gui/{tray => search}/unifiedsearchresult.h (100%) rename src/gui/{tray => search}/unifiedsearchresultslistmodel.cpp (99%) rename src/gui/{tray => search}/unifiedsearchresultslistmodel.h (96%) diff --git a/resources.qrc b/resources.qrc index cd40475f1a520..6683ed833ef1c 100644 --- a/resources.qrc +++ b/resources.qrc @@ -6,7 +6,7 @@ src/gui/WindowAccountHeader.qml src/gui/ActivitiesWindow.qml src/gui/AssistantWindow.qml - src/gui/SearchWindow.qml + src/gui/search/qml/SearchWindow.qml src/gui/UserStatusWindowStatusRow.qml src/gui/UserStatusWindowPredefinedStatusRow.qml src/gui/UserStatusSelectorPage.qml @@ -38,16 +38,16 @@ src/gui/tray/ActivityList.qml src/gui/tray/CurrentAccountHeaderButton.qml src/gui/tray/TrayWindowHeader.qml - src/gui/tray/UnifiedSearchInputContainer.qml - src/gui/tray/UnifiedSearchResultFetchMoreTrigger.qml - src/gui/tray/UnifiedSearchResultItem.qml - src/gui/tray/UnifiedSearchResultItemSkeleton.qml - src/gui/tray/UnifiedSearchResultItemSkeletonContainer.qml - src/gui/tray/UnifiedSearchResultItemSkeletonGradientRectangle.qml - src/gui/tray/UnifiedSearchResultListItem.qml - src/gui/tray/UnifiedSearchResultNothingFound.qml - src/gui/tray/UnifiedSearchPlaceholderView.qml - src/gui/tray/UnifiedSearchResultSectionItem.qml + src/gui/search/qml/UnifiedSearchInputContainer.qml + src/gui/search/qml/UnifiedSearchResultFetchMoreTrigger.qml + src/gui/search/qml/UnifiedSearchResultItem.qml + src/gui/search/qml/UnifiedSearchResultItemSkeleton.qml + src/gui/search/qml/UnifiedSearchResultItemSkeletonContainer.qml + src/gui/search/qml/UnifiedSearchResultItemSkeletonGradientRectangle.qml + src/gui/search/qml/UnifiedSearchResultListItem.qml + src/gui/search/qml/UnifiedSearchResultNothingFound.qml + src/gui/search/qml/UnifiedSearchPlaceholderView.qml + src/gui/search/qml/UnifiedSearchResultSectionItem.qml src/gui/tray/ActivityItemContextMenu.qml src/gui/tray/ActivityItemActions.qml src/gui/tray/ActivityItemContent.qml diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 545bab39cdb86..0c0ad0145318c 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -216,14 +216,14 @@ set(client_SRCS tray/activitydata.cpp tray/activitylistmodel.h tray/activitylistmodel.cpp - tray/unifiedsearchresult.h + search/unifiedsearchresult.h tray/asyncimageresponse.cpp - tray/unifiedsearchresult.cpp - tray/unifiedsearchresultslistmodel.h + search/unifiedsearchresult.cpp + search/unifiedsearchresultslistmodel.h tray/trayimageprovider.cpp tray/trayaccountappsmodel.h tray/trayaccountappsmodel.cpp - tray/unifiedsearchresultslistmodel.cpp + search/unifiedsearchresultslistmodel.cpp tray/usermodel.h tray/usermodel.cpp tray/notificationhandler.h diff --git a/src/gui/owncloudgui.cpp b/src/gui/owncloudgui.cpp index 4f644e89f6c46..561a1a7b783d2 100644 --- a/src/gui/owncloudgui.cpp +++ b/src/gui/owncloudgui.cpp @@ -35,7 +35,7 @@ #include "tray/sortedactivitylistmodel.h" #include "tray/syncstatussummary.h" #include "tray/trayaccountappsmodel.h" -#include "tray/unifiedsearchresultslistmodel.h" +#include "search/unifiedsearchresultslistmodel.h" #include "integration/fileactionsmodel.h" #include "filesystem.h" diff --git a/src/gui/SearchWindow.qml b/src/gui/search/qml/SearchWindow.qml similarity index 95% rename from src/gui/SearchWindow.qml rename to src/gui/search/qml/SearchWindow.qml index 4ebe9665ae61c..d051c76cca4a4 100644 --- a/src/gui/SearchWindow.qml +++ b/src/gui/search/qml/SearchWindow.qml @@ -9,13 +9,12 @@ import QtQuick.Layouts import Style import com.nextcloud.desktopclient -import "./tray" +import "../.." WizardStyledWindow { id: root - property int userIndex: -1 - property var currentUser: null + property var account: null property var searchModel: null readonly property string headline: qsTr("Search") readonly property int searchState: searchModel @@ -23,6 +22,7 @@ WizardStyledWindow { : UnifiedSearchResultsListModel.Placeholder readonly property bool isSearchInProgress: searchModel !== null && searchModel.isSearchInProgress readonly property bool canEditSearch: searchModel !== null && searchModel.canEditSearch + readonly property bool isAccountConnected: searchModel !== null && searchModel.isAccountConnected title: "" width: Style.searchWindowWidth @@ -59,7 +59,7 @@ WizardStyledWindow { WindowAccountHeader { Layout.fillWidth: true title: root.headline - user: root.currentUser + user: root.account } UnifiedSearchInputContainer { @@ -70,7 +70,7 @@ WizardStyledWindow { enabled: root.searchModel !== null readOnly: !root.canEditSearch text: root.searchModel ? root.searchModel.searchTerm : "" - placeholderText: root.currentUser !== null && !root.currentUser.isConnected + placeholderText: root.account !== null && !root.isAccountConnected ? qsTr("Search is available when this account is connected") : qsTr("Search files, messages, events …") isSearchInProgress: root.isSearchInProgress diff --git a/src/gui/tray/UnifiedSearchInputContainer.qml b/src/gui/search/qml/UnifiedSearchInputContainer.qml similarity index 97% rename from src/gui/tray/UnifiedSearchInputContainer.qml rename to src/gui/search/qml/UnifiedSearchInputContainer.qml index 4fd6ac4f1d927..37a77c6925698 100644 --- a/src/gui/tray/UnifiedSearchInputContainer.qml +++ b/src/gui/search/qml/UnifiedSearchInputContainer.qml @@ -10,6 +10,7 @@ import Qt5Compat.GraphicalEffects import Style import com.nextcloud.desktopclient +import "../../tray" TextField { id: root @@ -44,7 +45,7 @@ TextField { top: root.top topMargin: Style.extraSmallSpacing bottom: root.bottom - bottomMargin: Style.extraSmallSpacing + bottomMargin: Style.extraSmallSpacing } fillMode: Image.PreserveAspectFit diff --git a/src/gui/tray/UnifiedSearchPlaceholderView.qml b/src/gui/search/qml/UnifiedSearchPlaceholderView.qml similarity index 98% rename from src/gui/tray/UnifiedSearchPlaceholderView.qml rename to src/gui/search/qml/UnifiedSearchPlaceholderView.qml index e092363464d95..eaf6803e54105 100644 --- a/src/gui/tray/UnifiedSearchPlaceholderView.qml +++ b/src/gui/search/qml/UnifiedSearchPlaceholderView.qml @@ -8,6 +8,7 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts import Style +import "../../tray" ColumnLayout { id: root diff --git a/src/gui/tray/UnifiedSearchResultFetchMoreTrigger.qml b/src/gui/search/qml/UnifiedSearchResultFetchMoreTrigger.qml similarity index 98% rename from src/gui/tray/UnifiedSearchResultFetchMoreTrigger.qml rename to src/gui/search/qml/UnifiedSearchResultFetchMoreTrigger.qml index 565db1dd8ad4d..46de97d8ecf19 100644 --- a/src/gui/tray/UnifiedSearchResultFetchMoreTrigger.qml +++ b/src/gui/search/qml/UnifiedSearchResultFetchMoreTrigger.qml @@ -8,6 +8,7 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts import Style +import "../../tray" ColumnLayout { id: unifiedSearchResultItemFetchMore diff --git a/src/gui/tray/UnifiedSearchResultItem.qml b/src/gui/search/qml/UnifiedSearchResultItem.qml similarity index 99% rename from src/gui/tray/UnifiedSearchResultItem.qml rename to src/gui/search/qml/UnifiedSearchResultItem.qml index 2411034eb47e1..477f290632e49 100644 --- a/src/gui/tray/UnifiedSearchResultItem.qml +++ b/src/gui/search/qml/UnifiedSearchResultItem.qml @@ -10,6 +10,7 @@ import QtQuick.Layouts import Qt5Compat.GraphicalEffects import Style +import "../../tray" RowLayout { id: unifiedSearchResultItemDetails diff --git a/src/gui/tray/UnifiedSearchResultItemSkeleton.qml b/src/gui/search/qml/UnifiedSearchResultItemSkeleton.qml similarity index 100% rename from src/gui/tray/UnifiedSearchResultItemSkeleton.qml rename to src/gui/search/qml/UnifiedSearchResultItemSkeleton.qml diff --git a/src/gui/tray/UnifiedSearchResultItemSkeletonContainer.qml b/src/gui/search/qml/UnifiedSearchResultItemSkeletonContainer.qml similarity index 100% rename from src/gui/tray/UnifiedSearchResultItemSkeletonContainer.qml rename to src/gui/search/qml/UnifiedSearchResultItemSkeletonContainer.qml diff --git a/src/gui/tray/UnifiedSearchResultItemSkeletonGradientRectangle.qml b/src/gui/search/qml/UnifiedSearchResultItemSkeletonGradientRectangle.qml similarity index 100% rename from src/gui/tray/UnifiedSearchResultItemSkeletonGradientRectangle.qml rename to src/gui/search/qml/UnifiedSearchResultItemSkeletonGradientRectangle.qml diff --git a/src/gui/tray/UnifiedSearchResultListItem.qml b/src/gui/search/qml/UnifiedSearchResultListItem.qml similarity index 100% rename from src/gui/tray/UnifiedSearchResultListItem.qml rename to src/gui/search/qml/UnifiedSearchResultListItem.qml diff --git a/src/gui/tray/UnifiedSearchResultNothingFound.qml b/src/gui/search/qml/UnifiedSearchResultNothingFound.qml similarity index 98% rename from src/gui/tray/UnifiedSearchResultNothingFound.qml rename to src/gui/search/qml/UnifiedSearchResultNothingFound.qml index 93e9fad01d47a..baf1a26695380 100644 --- a/src/gui/tray/UnifiedSearchResultNothingFound.qml +++ b/src/gui/search/qml/UnifiedSearchResultNothingFound.qml @@ -8,6 +8,7 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts import Style +import "../../tray" ColumnLayout { id: unifiedSearchResultNothingFoundContainer diff --git a/src/gui/tray/UnifiedSearchResultSectionItem.qml b/src/gui/search/qml/UnifiedSearchResultSectionItem.qml similarity index 97% rename from src/gui/tray/UnifiedSearchResultSectionItem.qml rename to src/gui/search/qml/UnifiedSearchResultSectionItem.qml index 8318a10e75af1..547d909bf7fd3 100644 --- a/src/gui/tray/UnifiedSearchResultSectionItem.qml +++ b/src/gui/search/qml/UnifiedSearchResultSectionItem.qml @@ -9,6 +9,7 @@ import QtQuick.Controls import QtQuick.Layouts import Style import com.nextcloud.desktopclient +import "../../tray" EnforcedPlainTextLabel { required property string section diff --git a/src/gui/tray/unifiedsearchresult.cpp b/src/gui/search/unifiedsearchresult.cpp similarity index 100% rename from src/gui/tray/unifiedsearchresult.cpp rename to src/gui/search/unifiedsearchresult.cpp diff --git a/src/gui/tray/unifiedsearchresult.h b/src/gui/search/unifiedsearchresult.h similarity index 100% rename from src/gui/tray/unifiedsearchresult.h rename to src/gui/search/unifiedsearchresult.h diff --git a/src/gui/tray/unifiedsearchresultslistmodel.cpp b/src/gui/search/unifiedsearchresultslistmodel.cpp similarity index 99% rename from src/gui/tray/unifiedsearchresultslistmodel.cpp rename to src/gui/search/unifiedsearchresultslistmodel.cpp index 0be8dbd946544..21f1d1fb8aed4 100644 --- a/src/gui/tray/unifiedsearchresultslistmodel.cpp +++ b/src/gui/search/unifiedsearchresultslistmodel.cpp @@ -68,7 +68,7 @@ QString iconUrlForDefaultIconName(const QString &defaultIconName, const bool dar if (urlForIcon.isValid() && !urlForIcon.scheme().isEmpty()) { return defaultIconName; } - + const auto colorIconPath = darkMode ? QStringLiteral(":/client/theme/white/") : QStringLiteral(":/client/theme/black/"); if (defaultIconName.startsWith(QStringLiteral("icon-"))) { @@ -211,7 +211,7 @@ QVariant UnifiedSearchResultsListModel::data(const QModelIndex &index, int role) switch (role) { case ProviderNameRole: return _results.at(index.row())._providerName; - case ProviderIdRole: + case ProviderIdRole: return _results.at(index.row())._providerId; case DarkImagePlaceholderRole: return imagePlaceholderUrlForProviderId(_results.at(index.row())._providerId, true); @@ -356,7 +356,12 @@ bool UnifiedSearchResultsListModel::hasSearchError() const bool UnifiedSearchResultsListModel::canEditSearch() const { - return _accountState && _accountState->isConnected() && !isFetchMoreInProgress(); + return isAccountConnected() && !isFetchMoreInProgress(); +} + +bool UnifiedSearchResultsListModel::isAccountConnected() const +{ + return _accountState && _accountState->isConnected(); } UnifiedSearchResultsListModel::SearchState UnifiedSearchResultsListModel::searchState() const @@ -453,7 +458,7 @@ void UnifiedSearchResultsListModel::slotFetchProvidersFinished(const QJsonDocume emit errorStringChanged(); return; } - + if (statusCode != 200) { qCCritical(lcUnifiedSearch) << QStringLiteral("%1: Failed to fetch search providers for '%2'. Error: %3") .arg(statusCode) @@ -500,7 +505,7 @@ void UnifiedSearchResultsListModel::slotSearchForProviderFinished(const QJsonDoc } const auto providerId = job->property("providerId").toString(); - + if (providerId.isEmpty()) { return; } diff --git a/src/gui/tray/unifiedsearchresultslistmodel.h b/src/gui/search/unifiedsearchresultslistmodel.h similarity index 96% rename from src/gui/tray/unifiedsearchresultslistmodel.h rename to src/gui/search/unifiedsearchresultslistmodel.h index 2dee666442fb5..48e5085b4de8f 100644 --- a/src/gui/tray/unifiedsearchresultslistmodel.h +++ b/src/gui/search/unifiedsearchresultslistmodel.h @@ -34,6 +34,7 @@ class UnifiedSearchResultsListModel : public QAbstractListModel Q_PROPERTY(bool hasSearchTerm READ hasSearchTerm NOTIFY searchTermChanged) Q_PROPERTY(bool hasSearchError READ hasSearchError NOTIFY errorStringChanged) Q_PROPERTY(bool canEditSearch READ canEditSearch NOTIFY canEditSearchChanged) + Q_PROPERTY(bool isAccountConnected READ isAccountConnected NOTIFY canEditSearchChanged) Q_PROPERTY(SearchState searchState READ searchState NOTIFY searchStateChanged) struct UnifiedSearchProvider @@ -90,6 +91,8 @@ class UnifiedSearchResultsListModel : public QAbstractListModel [[nodiscard]] bool hasSearchTerm() const; [[nodiscard]] bool hasSearchError() const; [[nodiscard]] bool canEditSearch() const; + /** @brief Returns whether the account is connected. */ + [[nodiscard]] bool isAccountConnected() const; [[nodiscard]] SearchState searchState() const; Q_INVOKABLE void resultClicked(const QString &providerId, const QUrl &resourceUrl) const; diff --git a/src/gui/settingsdialog.cpp b/src/gui/settingsdialog.cpp index e272f5ee2ce57..ce835cccf115d 100644 --- a/src/gui/settingsdialog.cpp +++ b/src/gui/settingsdialog.cpp @@ -248,7 +248,7 @@ void SettingsDialog::showEvent(QShowEvent *event) // constructor via winId()) doesn't stick, because Qt recreates the NSWindow when the dialog is // shown, resetting the title-bar separator to its default. if (auto *const handle = windowHandle()) { - styleNativeTitleBar(handle, /*hideTitleText=*/false, palette().color(QPalette::Window)); + styleNativeTitleBar(handle, /*hideTitleText=*/false); } #endif } @@ -266,7 +266,7 @@ void SettingsDialog::changeEvent(QEvent *e) #ifdef Q_OS_MACOS // macOS resets title-bar styling across appearance changes; re-apply it. if (auto *const handle = windowHandle()) { - styleNativeTitleBar(handle, /*hideTitleText=*/false, palette().color(QPalette::Window)); + styleNativeTitleBar(handle, /*hideTitleText=*/false); } #endif break; @@ -498,14 +498,14 @@ void SettingsDialog::customizeStyle() .arg(separatorColor.alpha()); setStyleSheet(QStringLiteral( - "#Settings { background: palette(window); border-radius: 0; }" + "#Settings { border-radius: 0; }" /* Navigation */ "#settings_navigation_scroll { background: palette(" BACKGROUND_PALETTE "); border-radius: 12px; padding: 4px; }" "#settings_navigation { background: transparent; border: none; padding: 0px; }" /* Content area */ - "#settings_content, #settings_content_scroll { background: palette(window); border-radius: 12px; }" + "#settings_content, #settings_content_scroll { background: transparent; border-radius: 12px; }" /* Panels */ "#generalGroupBox, #notificationsGroupBox, #advancedGroupBox, #syncBehaviorGroupBox," diff --git a/src/gui/systray.cpp b/src/gui/systray.cpp index 2a4b885448988..6eeb6e3984fbe 100644 --- a/src/gui/systray.cpp +++ b/src/gui/systray.cpp @@ -5,12 +5,13 @@ */ #include "accountmanager.h" +#include "accountstate.h" #include "systray.h" #include "theme.h" #include "config.h" #include "common/utility.h" #include "tray/svgimageprovider.h" -#include "tray/unifiedsearchresultslistmodel.h" +#include "search/unifiedsearchresultslistmodel.h" #include "tray/usermodel.h" #include "wheelhandler.h" #include "tray/trayimageprovider.h" @@ -411,6 +412,12 @@ void Systray::showSearchWindow(int userIndex) return; } + const auto accountState = user->accountState(); + if (!accountState) { + qCWarning(lcSystray) << "Could not open search window without an account state"; + return; + } + const auto windowKey = user->account()->id(); if (const auto existingWindow = _searchWindows.value(windowKey)) { @@ -421,12 +428,7 @@ void Systray::showSearchWindow(int userIndex) return; } - const QVariantMap initialProperties{ - {"userIndex", targetUserId}, - {"currentUser", QVariant::fromValue(user)}, - {"searchModel", QVariant::fromValue(user->getUnifiedSearchResultsListModel())}, - }; - QQmlComponent searchWindowComponent(trayEngine(), QStringLiteral("qrc:/qml/src/gui/SearchWindow.qml")); + QQmlComponent searchWindowComponent(trayEngine(), QStringLiteral("qrc:/qml/src/gui/search/qml/SearchWindow.qml")); if (searchWindowComponent.isError()) { qCWarning(lcSystray) << searchWindowComponent.errorString(); @@ -434,6 +436,15 @@ void Systray::showSearchWindow(int userIndex) return; } + auto *const searchModel = new UnifiedSearchResultsListModel(accountState.data(), accountState.data()); + const QVariantMap initialProperties{ + {"account", QVariantMap{ + {"avatar", user->avatarUrl()}, + {"name", user->name()}, + {"server", user->server()}, + }}, + {"searchModel", QVariant::fromValue(searchModel)}, + }; const auto createdObject = searchWindowComponent.createWithInitialProperties(initialProperties); const auto window = qobject_cast(createdObject); if (!window) { @@ -441,6 +452,7 @@ void Systray::showSearchWindow(int userIndex) if (createdObject) { createdObject->deleteLater(); } + searchModel->deleteLater(); return; } @@ -459,6 +471,19 @@ void Systray::showSearchWindow(int userIndex) connect(window, &QObject::destroyed, this, [this, windowKey] { _searchWindows.remove(windowKey); }); + connect(window, &QObject::destroyed, searchModel, &QObject::deleteLater); + const auto searchModelGuard = QPointer(searchModel); + connect(window, &QWindow::visibleChanged, window, [window, searchModelGuard](const bool visible) { + if (!visible) { + if (searchModelGuard) { + searchModelGuard->deleteLater(); + } + window->deleteLater(); + } + }); + connect(accountState.data(), &QObject::destroyed, window, [window] { + window->close(); + }); positionWindowAtScreenCenter(window); window->show(); diff --git a/src/gui/tray/MainWindow.qml b/src/gui/tray/MainWindow.qml index 5dc87cc3d8487..f5dfe06c2c2aa 100644 --- a/src/gui/tray/MainWindow.qml +++ b/src/gui/tray/MainWindow.qml @@ -229,11 +229,6 @@ ApplicationWindow { Rectangle { id: trayWindowMainItem - property bool isUnifiedSearchActive: unifiedSearchResultsListViewSkeletonLoader.active - || unifiedSearchResultNothingFound.visible - || unifiedSearchResultsErrorLabel.visible - || unifiedSearchResultsListView.visible - || trayWindowUnifiedSearchInputContainer.activateSearchFocus property bool showAssistantPanel: false property bool isAssistantActive: assistantPromptLoader.active @@ -339,30 +334,6 @@ ApplicationWindow { } } - UnifiedSearchInputContainer { - id: trayWindowUnifiedSearchInputContainer - visible: !trayWindowMainItem.showAssistantPanel - - property bool activateSearchFocus: activeFocus - - anchors.top: trayWindowSyncWarning.visible - ? trayWindowSyncWarning.bottom - : trayWindowHeader.bottom - anchors.left: trayWindowMainItem.left - anchors.right: trayWindowMainItem.right - anchors.topMargin: Style.trayHorizontalMargin - anchors.leftMargin: Style.trayHorizontalMargin - anchors.rightMargin: Style.trayHorizontalMargin - - text: UserModel.currentUser.unifiedSearchResultsListModel.searchTerm - readOnly: !UserModel.currentUser.isConnected || UserModel.currentUser.unifiedSearchResultsListModel.currentFetchMoreInProgressProviderId - isSearchInProgress: UserModel.currentUser.unifiedSearchResultsListModel.isSearchInProgress - onTextEdited: { UserModel.currentUser.unifiedSearchResultsListModel.searchTerm = trayWindowUnifiedSearchInputContainer.text } - onClearText: { UserModel.currentUser.unifiedSearchResultsListModel.searchTerm = "" } - onActiveFocusChanged: activateSearchFocus = activeFocus && focusReason !== Qt.TabFocusReason && focusReason !== Qt.BacktabFocusReason - Keys.onEscapePressed: activateSearchFocus = false - } - Dialog { id: assistantResetConfirmationDialogWrapper modal: true @@ -671,137 +642,15 @@ ApplicationWindow { } } - Rectangle { - id: bottomUnifiedSearchInputSeparator - - anchors.top: trayWindowMainItem.showAssistantPanel ? assistantInputContainer.bottom : trayWindowUnifiedSearchInputContainer.bottom - anchors.left: parent.left - anchors.right: parent.right - anchors.topMargin: Style.trayHorizontalMargin - - height: 1 - color: palette.dark - visible: trayWindowMainItem.isUnifiedSearchActive || trayWindowMainItem.showAssistantPanel - } - - ErrorBox { - id: unifiedSearchResultsErrorLabel - visible: UserModel.currentUser.unifiedSearchResultsListModel.errorString && !unifiedSearchResultsListView.visible && ! UserModel.currentUser.unifiedSearchResultsListModel.isSearchInProgress && ! UserModel.currentUser.unifiedSearchResultsListModel.currentFetchMoreInProgressProviderId - text: UserModel.currentUser.unifiedSearchResultsListModel.errorString - anchors.top: bottomUnifiedSearchInputSeparator.bottom - anchors.left: trayWindowMainItem.left - anchors.right: trayWindowMainItem.right - anchors.margins: Style.trayHorizontalMargin - } - - UnifiedSearchPlaceholderView { - id: unifiedSearchPlaceholderView - - anchors.top: bottomUnifiedSearchInputSeparator.bottom - anchors.left: trayWindowMainItem.left - anchors.right: trayWindowMainItem.right - anchors.bottom: trayWindowMainItem.bottom - anchors.topMargin: Style.trayHorizontalMargin - - visible: trayWindowUnifiedSearchInputContainer.activateSearchFocus && !UserModel.currentUser.unifiedSearchResultsListModel.searchTerm - } - - UnifiedSearchResultNothingFound { - id: unifiedSearchResultNothingFound - - anchors.top: bottomUnifiedSearchInputSeparator.bottom - anchors.left: trayWindowMainItem.left - anchors.right: trayWindowMainItem.right - anchors.topMargin: Style.trayHorizontalMargin - - text: UserModel.currentUser.unifiedSearchResultsListModel.searchTerm - - property bool isSearchRunning: UserModel.currentUser.unifiedSearchResultsListModel.isSearchInProgress - property bool waitingForSearchTermEditEnd: UserModel.currentUser.unifiedSearchResultsListModel.waitingForSearchTermEditEnd - property bool isSearchResultsEmpty: unifiedSearchResultsListView.count === 0 - property bool nothingFound: text && isSearchResultsEmpty && !UserModel.currentUser.unifiedSearchResultsListModel.errorString - - visible: !isSearchRunning && !waitingForSearchTermEditEnd && nothingFound - } - - Loader { - id: unifiedSearchResultsListViewSkeletonLoader - - anchors.top: bottomUnifiedSearchInputSeparator.bottom - anchors.left: trayWindowMainItem.left - anchors.right: trayWindowMainItem.right - anchors.bottom: trayWindowMainItem.bottom - anchors.margins: controlRoot.padding - - active: !unifiedSearchResultNothingFound.visible && - !unifiedSearchResultsListView.visible && - !UserModel.currentUser.unifiedSearchResultsListModel.errorString && - UserModel.currentUser.unifiedSearchResultsListModel.searchTerm - - sourceComponent: UnifiedSearchResultItemSkeletonContainer { - anchors.fill: parent - spacing: unifiedSearchResultsListView.spacing - animationRectangleWidth: trayWindow.width - } - } - - ScrollView { - id: controlRoot - contentWidth: availableWidth - - ScrollBar.horizontal.policy: ScrollBar.AlwaysOff - - data: WheelHandler { - target: controlRoot.contentItem - } - visible: unifiedSearchResultsListView.count > 0 - - anchors.top: bottomUnifiedSearchInputSeparator.bottom - anchors.left: trayWindowMainItem.left - anchors.right: trayWindowMainItem.right - anchors.bottom: trayWindowMainItem.bottom - - ListView { - id: unifiedSearchResultsListView - spacing: 4 - clip: true - - keyNavigationEnabled: true - - reuseItems: true - - Accessible.role: Accessible.List - Accessible.name: qsTr("Unified search results list") - - model: UserModel.currentUser.unifiedSearchResultsListModel - - delegate: UnifiedSearchResultListItem { - width: unifiedSearchResultsListView.width - isSearchInProgress: unifiedSearchResultsListView.model.isSearchInProgress - currentFetchMoreInProgressProviderId: unifiedSearchResultsListView.model.currentFetchMoreInProgressProviderId - fetchMoreTriggerClicked: unifiedSearchResultsListView.model.fetchMoreTriggerClicked - resultClicked: unifiedSearchResultsListView.model.resultClicked - ListView.onPooled: isPooled = true - ListView.onReused: isPooled = false - } - - section.property: "providerName" - section.criteria: ViewSection.FullString - section.delegate: UnifiedSearchResultSectionItem { - width: unifiedSearchResultsListView.width - } - } - } - SyncStatus { id: syncStatus accentColor: Style.accentColor user: UserModel.currentUser activityListModel: activityModel - visible: !trayWindowMainItem.isUnifiedSearchActive && !trayWindowMainItem.showAssistantPanel + visible: !trayWindowMainItem.showAssistantPanel - anchors.top: trayWindowMainItem.showAssistantPanel ? assistantInputContainer.bottom : trayWindowUnifiedSearchInputContainer.bottom + anchors.top: trayWindowMainItem.showAssistantPanel ? assistantInputContainer.bottom : trayWindowHeader.bottom anchors.left: trayWindowMainItem.left anchors.right: trayWindowMainItem.right } @@ -813,7 +662,7 @@ ApplicationWindow { anchors.bottom: syncStatus.bottom height: 1 color: palette.dark - visible: !trayWindowMainItem.isUnifiedSearchActive && !trayWindowMainItem.showAssistantPanel + visible: !trayWindowMainItem.showAssistantPanel } Loader { @@ -871,7 +720,7 @@ ApplicationWindow { ActivityList { id: activityList - visible: !trayWindowMainItem.isUnifiedSearchActive && !trayWindowMainItem.isAssistantActive + visible: !trayWindowMainItem.isAssistantActive anchors.top: syncStatus.bottom anchors.left: trayWindowMainItem.left anchors.right: trayWindowMainItem.right diff --git a/src/gui/tray/usermodel.cpp b/src/gui/tray/usermodel.cpp index 44ade499a77d9..e7509fe416651 100644 --- a/src/gui/tray/usermodel.cpp +++ b/src/gui/tray/usermodel.cpp @@ -22,7 +22,6 @@ #include "syncfileitem.h" #include "systray.h" #include "tray/activitylistmodel.h" -#include "tray/unifiedsearchresultslistmodel.h" #include "tray/talkreply.h" #include "userstatusconnector.h" #include "common/utility.h" @@ -521,7 +520,6 @@ User::User(AccountStatePtr &account, const bool &isCurrent, QObject *parent) , _account(account) , _isCurrentUser(isCurrent) , _activityModel(new ActivityListModel(_account.data(), this)) - , _unifiedSearchResultsModel(new UnifiedSearchResultsListModel(_account.data(), this)) { connect(ProgressDispatcher::instance(), &ProgressDispatcher::progressInfo, this, &User::slotProgressInfo); @@ -1735,11 +1733,6 @@ void User::refreshAccountAlert() emit accountAlertChanged(); } -UnifiedSearchResultsListModel *User::getUnifiedSearchResultsListModel() const -{ - return _unifiedSearchResultsModel; -} - void User::openLocalFolder() const { if (const auto folder = getFolder()) { diff --git a/src/gui/tray/usermodel.h b/src/gui/tray/usermodel.h index dfbec20efbe4f..4e52612d11a9c 100644 --- a/src/gui/tray/usermodel.h +++ b/src/gui/tray/usermodel.h @@ -29,7 +29,6 @@ #include namespace OCC { -class UnifiedSearchResultsListModel; class OcsAssistantConnector; @@ -83,7 +82,6 @@ class User : public QObject Q_PROPERTY(bool syncStatusOk READ syncStatusOk NOTIFY syncStatusChanged) Q_PROPERTY(bool isConnected READ isConnected NOTIFY accountStateChanged) Q_PROPERTY(bool needsToSignTermsOfService READ needsToSignTermsOfService NOTIFY accountStateChanged) - Q_PROPERTY(UnifiedSearchResultsListModel* unifiedSearchResultsListModel READ getUnifiedSearchResultsListModel CONSTANT) Q_PROPERTY(QVariantList groupFolders READ groupFolders NOTIFY groupFoldersChanged) Q_PROPERTY(bool canLogout READ canLogout CONSTANT) Q_PROPERTY(bool isAssistantEnabled READ isNcAssistantEnabled NOTIFY assistantStateChanged) @@ -105,7 +103,6 @@ class User : public QObject void setCurrentUser(const bool &isCurrent); [[nodiscard]] Folder *getFolder() const; ActivityListModel *getActivityModel(); - [[nodiscard]] UnifiedSearchResultsListModel *getUnifiedSearchResultsListModel() const; void openLocalFolder() const; #ifdef BUILD_FILE_PROVIDER_MODULE void openFileProviderDomain() const; @@ -275,7 +272,6 @@ private slots: AccountStatePtr _account; bool _isCurrentUser; ActivityListModel *_activityModel; - UnifiedSearchResultsListModel *_unifiedSearchResultsModel; QVariantMap _accountAlert; QVariantList _trayFolderInfos; diff --git a/test/testunifiedsearchlistmodel.cpp b/test/testunifiedsearchlistmodel.cpp index 92b62379c0754..281efdadd4d7e 100644 --- a/test/testunifiedsearchlistmodel.cpp +++ b/test/testunifiedsearchlistmodel.cpp @@ -3,7 +3,7 @@ * SPDX-License-Identifier: GPL-2.0-or-later */ -#include "gui/tray/unifiedsearchresultslistmodel.h" +#include "gui/search/unifiedsearchresultslistmodel.h" #include "account.h" #include "accountstate.h"