diff --git a/resources.qrc b/resources.qrc
index 122a9487a4d21..6683ed833ef1c 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/search/qml/SearchWindow.qml
src/gui/UserStatusWindowStatusRow.qml
src/gui/UserStatusWindowPredefinedStatusRow.qml
src/gui/UserStatusSelectorPage.qml
@@ -37,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/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/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/search/qml/SearchWindow.qml b/src/gui/search/qml/SearchWindow.qml
new file mode 100644
index 0000000000000..d051c76cca4a4
--- /dev/null
+++ b/src/gui/search/qml/SearchWindow.qml
@@ -0,0 +1,177 @@
+/*
+ * 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 com.nextcloud.desktopclient
+import "../.."
+
+WizardStyledWindow {
+ id: root
+
+ property var account: null
+ property var searchModel: null
+ readonly property string headline: qsTr("Search")
+ 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
+ readonly property bool isAccountConnected: searchModel !== null && searchModel.isAccountConnected
+
+ 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.account
+ }
+
+ 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.account !== null && !root.isAccountConnected
+ ? 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.searchState === UnifiedSearchResultsListModel.SearchError
+ text: root.searchModel ? root.searchModel.errorString : ""
+ }
+
+ UnifiedSearchPlaceholderView {
+ anchors.fill: parent
+ visible: root.searchState === UnifiedSearchResultsListModel.Placeholder
+ }
+
+ UnifiedSearchResultNothingFound {
+ anchors.fill: parent
+ visible: root.searchState === UnifiedSearchResultsListModel.NothingFound
+ text: root.searchModel ? root.searchModel.searchTerm : ""
+ }
+
+ Loader {
+ anchors.fill: parent
+ anchors.margins: Style.smallSpacing
+ active: root.searchState === UnifiedSearchResultsListModel.Skeleton
+ asynchronous: true
+
+ sourceComponent: UnifiedSearchResultItemSkeletonContainer {
+ anchors.fill: parent
+ spacing: searchResultsListView.spacing
+ animationRectangleWidth: root.width
+ }
+ }
+
+ ScrollView {
+ id: searchResultsScrollView
+
+ anchors.fill: parent
+ contentWidth: availableWidth
+ visible: root.searchState === UnifiedSearchResultsListModel.Results
+
+ 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/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 91%
rename from src/gui/tray/unifiedsearchresultslistmodel.cpp
rename to src/gui/search/unifiedsearchresultslistmodel.cpp
index 424bc4c191a97..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-"))) {
@@ -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
@@ -198,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);
@@ -326,6 +339,52 @@ 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 isAccountConnected() && !isFetchMoreInProgress();
+}
+
+bool UnifiedSearchResultsListModel::isAccountConnected() const
+{
+ return _accountState && _accountState->isConnected();
+}
+
+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};
@@ -399,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)
@@ -446,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 79%
rename from src/gui/tray/unifiedsearchresultslistmodel.h
rename to src/gui/search/unifiedsearchresultslistmodel.h
index a2c07eb651fab..48e5085b4de8f 100644
--- a/src/gui/tray/unifiedsearchresultslistmodel.h
+++ b/src/gui/search/unifiedsearchresultslistmodel.h
@@ -30,6 +30,12 @@ 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(bool isAccountConnected READ isAccountConnected NOTIFY canEditSearchChanged)
+ Q_PROPERTY(SearchState searchState READ searchState NOTIFY searchStateChanged)
struct UnifiedSearchProvider
{
@@ -42,6 +48,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 +87,14 @@ 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;
+ /** @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;
Q_INVOKABLE void fetchMoreTriggerClicked(const QString &providerId);
@@ -100,6 +124,8 @@ class UnifiedSearchResultsListModel : public QAbstractListModel
void errorStringChanged();
void searchTermChanged();
void waitingForSearchTermEditEndChanged();
+ void canEditSearchChanged();
+ void searchStateChanged();
public slots:
void setSearchTerm(const QString &term);
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 78d2596e1ab66..6eeb6e3984fbe 100644
--- a/src/gui/systray.cpp
+++ b/src/gui/systray.cpp
@@ -5,11 +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 "search/unifiedsearchresultslistmodel.h"
#include "tray/usermodel.h"
#include "wheelhandler.h"
#include "tray/trayimageprovider.h"
@@ -389,6 +391,106 @@ 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 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)) {
+ positionWindowAtScreenCenter(existingWindow.data());
+ existingWindow->show();
+ existingWindow->raise();
+ existingWindow->requestActivate();
+ return;
+ }
+
+ QQmlComponent searchWindowComponent(trayEngine(), QStringLiteral("qrc:/qml/src/gui/search/qml/SearchWindow.qml"));
+
+ if (searchWindowComponent.isError()) {
+ qCWarning(lcSystray) << searchWindowComponent.errorString();
+ qCWarning(lcSystray) << searchWindowComponent.errors();
+ 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) {
+ qCWarning(lcSystray) << "Search window resulted in creation of object that was not a window!";
+ if (createdObject) {
+ createdObject->deleteLater();
+ }
+ searchModel->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);
+ });
+ 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();
+ 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/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/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/test/testunifiedsearchlistmodel.cpp b/test/testunifiedsearchlistmodel.cpp
index ed948d2a3993d..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"
@@ -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();
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