Display 'Search globally' as the last sharees list element

Signed-off-by: alex-z <blackslayer4@gmail.com>
This commit is contained in:
alex-z 2023-02-24 14:06:29 +01:00
parent 355dbd1faa
commit 31de652d9b
9 changed files with 289 additions and 27 deletions

View File

@ -152,6 +152,7 @@ ColumnLayout {
}
ShareeSearchField {
id: shareeSearchField
Layout.fillWidth: true
Layout.leftMargin: root.horizontalPadding
Layout.rightMargin: root.horizontalPadding

View File

@ -20,8 +20,54 @@ import QtQuick.Controls 2.15
import com.nextcloud.desktopclient 1.0
import Style 1.0
import "../tray"
ItemDelegate {
id: root
text: model.display
contentItem: RowLayout {
height: visible ? implicitHeight : 0
Loader {
id: shareeIconLoader
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
active: model.icon !== ""
sourceComponent: Image {
id: shareeIcon
horizontalAlignment: Qt.AlignLeft
verticalAlignment: Qt.AlignVCenter
width: height
height: shareeLabel.height
smooth: true
antialiasing: true
mipmap: true
fillMode: Image.PreserveAspectFit
source: model.icon
sourceSize: Qt.size(shareeIcon.height * 1.0, shareeIcon.height * 1.0)
}
}
EnforcedPlainTextLabel {
id: shareeLabel
Layout.preferredHeight: unifiedSearchResultSkeletonItemDetails.iconWidth
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
Layout.fillWidth: true
horizontalAlignment: Text.AlignLeft
verticalAlignment: Text.AlignVCenter
text: model.display
color: Style.ncTextColor
}
}
}

View File

@ -41,7 +41,7 @@ TextField {
readonly property double iconsScaleFactor: 0.6
function triggerSuggestionsVisibility() {
shareeListView.count > 0 && text !== "" ? suggestionsPopup.open() : suggestionsPopup.close();
shareeListView.count > 0 ? suggestionsPopup.open() : suggestionsPopup.close();
}
placeholderText: qsTr("Search for users or groups…")
@ -73,7 +73,7 @@ TextField {
case Qt.Key_Enter:
case Qt.Key_Return:
if(shareeListView.currentIndex > -1) {
shareeListView.itemAtIndex(shareeListView.currentIndex).selectSharee();
shareeListView.itemAtIndex(shareeListView.currentIndex).selectItem();
event.accepted = true;
break;
}
@ -219,6 +219,9 @@ TextField {
anchors.left: parent.left
anchors.right: parent.right
enabled: model.type !== Sharee.LookupServerSearchResults
hoverEnabled: model.type !== Sharee.LookupServerSearchResults
function selectSharee() {
root.shareeSelected(model.sharee);
suggestionsPopup.close();
@ -226,6 +229,15 @@ TextField {
root.clear();
}
function selectItem() {
if (model.type === Sharee.LookupServerSearch) {
shareeListView.currentIndex = -1
root.shareeModel.searchGlobally()
} else {
selectSharee()
}
}
onHoveredChanged: if (hovered) {
// When we set the currentIndex the list view will scroll...
// unless we tamper with the preferred highlight points to stop this.
@ -241,7 +253,7 @@ TextField {
shareeListView.preferredHighlightBegin = savedPreferredHighlightBegin;
shareeListView.preferredHighlightEnd = savedPreferredHighlightEnd;
}
onClicked: selectSharee()
onClicked: selectItem()
}
}
}

View File

@ -19,6 +19,7 @@
#include <QJsonArray>
#include "ocsshareejob.h"
#include "theme.h"
namespace OCC {
@ -29,7 +30,10 @@ ShareeModel::ShareeModel(QObject *parent)
{
_searchRateLimitingTimer.setSingleShot(true);
_searchRateLimitingTimer.setInterval(500);
_searchGloballyPlaceholder.reset(new Sharee({}, tr("Search globally"), Sharee::LookupServerSearch, QStringLiteral("magnifying-glass.svg")));
_searchGloballyPlaceholder->setIsIconColourful(true);
connect(&_searchRateLimitingTimer, &QTimer::timeout, this, &ShareeModel::fetch);
connect(Theme::instance(), &Theme::darkModeChanged, this, &ShareeModel::slotDarkModeChanged);
}
// ---------------------- QAbstractListModel methods ---------------------- //
@ -48,6 +52,8 @@ QHash<int, QByteArray> ShareeModel::roleNames() const
auto roles = QAbstractListModel::roleNames();
roles[ShareeRole] = "sharee";
roles[AutoCompleterStringMatchRole] = "autoCompleterStringMatch";
roles[TypeRole] = "type";
roles[IconRole] = "icon";
return roles;
}
@ -68,6 +74,10 @@ QVariant ShareeModel::data(const QModelIndex &index, const int role) const
case AutoCompleterStringMatchRole:
// Don't show this to the user
return QString(sharee->displayName() + " (" + sharee->shareWith() + ")");
case IconRole:
return sharee->iconUrlColoured();
case TypeRole:
return sharee->type();
case ShareeRole:
return QVariant::fromValue(sharee);
}
@ -119,6 +129,12 @@ void ShareeModel::setSearchString(const QString &searchString)
return;
}
beginResetModel();
_sharees.clear();
endResetModel();
Q_EMIT shareesReady();
_searchString = searchString;
Q_EMIT searchStringChanged();
@ -165,16 +181,28 @@ void ShareeModel::setShareeBlocklist(const QVariantList shareeBlocklist)
filterSharees();
}
void ShareeModel::searchGlobally()
{
setLookupMode(ShareeModel::LookupMode::GlobalSearch);
beginResetModel();
_sharees.clear();
endResetModel();
Q_EMIT shareesReady();
fetch();
}
// ------------------------- Internal data methods ------------------------- //
void ShareeModel::fetch()
{
if(!_accountState || !_accountState->account() || _searchString.isEmpty()) {
if (!_accountState || !_accountState->account() || _searchString.isEmpty()) {
qCInfo(lcShareeModel) << "Not fetching sharees for searchString: " << _searchString;
return;
}
_fetchOngoing = true;
Q_EMIT fetchOngoingChanged();
const auto shareItemTypeString = _shareItemIsFolder ? QStringLiteral("folder") : QStringLiteral("file");
@ -233,9 +261,35 @@ void ShareeModel::shareesFetched(const QJsonDocument &reply)
beginResetModel();
_sharees = newSharees;
insertSearchGloballyItem(newSharees);
endResetModel();
Q_EMIT shareesReady();
setLookupMode(LookupMode::LocalSearch);
}
void ShareeModel::insertSearchGloballyItem(const QVector<ShareePtr> &newShareesFetched)
{
const auto foundIt = std::find_if(std::begin(_sharees), std::end(_sharees), [](const ShareePtr &sharee) {
return sharee->type() == Sharee::LookupServerSearch || sharee->type() == Sharee::LookupServerSearchResults;
});
// remove it if it somehow appeared not at the end, to avoid writing complex proxy models for sorting
if (foundIt != std::end(_sharees) && (foundIt + 1) != std::end(_sharees)) {
_sharees.erase(foundIt);
}
_sharees.push_back(_searchGloballyPlaceholder);
if (lookupMode() == LookupMode::GlobalSearch) {
const auto displayName = newShareesFetched.isEmpty() ? tr("No results found") : tr("Global search results");
_searchGloballyPlaceholder->setDisplayName(displayName);
_searchGloballyPlaceholder->setType(Sharee::LookupServerSearchResults);
} else {
_searchGloballyPlaceholder->setDisplayName(tr("Search globally"));
_searchGloballyPlaceholder->setType(Sharee::LookupServerSearch);
}
}
ShareePtr ShareeModel::parseSharee(const QJsonObject &data) const
@ -275,4 +329,13 @@ void ShareeModel::filterSharees()
Q_EMIT shareesReady();
}
void ShareeModel::slotDarkModeChanged()
{
for (int i = 0; i < _sharees.size(); ++i) {
if (_sharees[i]->updateIconUrl()) {
Q_EMIT dataChanged(index(i), index(i), {IconRole});
}
}
}
}

View File

@ -45,6 +45,8 @@ public:
enum Roles {
ShareeRole = Qt::UserRole + 1,
AutoCompleterStringMatchRole,
TypeRole,
IconRole,
};
Q_ENUM(Roles);
@ -80,12 +82,15 @@ public slots:
void setSearchString(const QString &searchString);
void setLookupMode(const OCC::ShareeModel::LookupMode lookupMode);
void setShareeBlocklist(const QVariantList shareeBlocklist);
void searchGlobally();
void fetch();
private slots:
void shareesFetched(const QJsonDocument &reply);
void insertSearchGloballyItem(const QVector<ShareePtr> &newShareesFetched);
void filterSharees();
void slotDarkModeChanged();
private:
[[nodiscard]] ShareePtr parseSharee(const QJsonObject &data) const;
@ -100,6 +105,8 @@ private:
QVector<ShareePtr> _sharees;
QVector<ShareePtr> _shareeBlocklist;
ShareePtr _searchGloballyPlaceholder;
};
}

View File

@ -128,6 +128,7 @@ ownCloudGui::ownCloudGui(Application *parent)
qmlRegisterUncreatableType<UnifiedSearchResultsListModel>("com.nextcloud.desktopclient", 1, 0, "UnifiedSearchResultsListModel", "UnifiedSearchResultsListModel");
qmlRegisterUncreatableType<UserStatus>("com.nextcloud.desktopclient", 1, 0, "UserStatus", "Access to Status enum");
qmlRegisterUncreatableType<Sharee>("com.nextcloud.desktopclient", 1, 0, "Sharee", "Access to Type enum");
qRegisterMetaTypeStreamOperators<Emoji>();
@ -136,6 +137,7 @@ ownCloudGui::ownCloudGui(Application *parent)
qRegisterMetaType<UserStatus>("UserStatus");
qRegisterMetaType<SharePtr>("SharePtr");
qRegisterMetaType<ShareePtr>("ShareePtr");
qRegisterMetaType<Sharee>("Sharee");
qmlRegisterSingletonInstance("com.nextcloud.desktopclient", 1, 0, "UserModel", UserModel::instance());
qmlRegisterSingletonInstance("com.nextcloud.desktopclient", 1, 0, "UserAppsModel", UserAppsModel::instance());

View File

@ -14,22 +14,29 @@
#include "sharee.h"
#include "ocsshareejob.h"
#include "theme.h"
#include <QJsonObject>
#include <QJsonDocument>
#include <QJsonArray>
namespace OCC {
namespace OCC
{
Q_LOGGING_CATEGORY(lcSharing, "nextcloud.gui.sharing", QtInfoMsg)
Sharee::Sharee(const QString shareWith,
const QString displayName,
const Type type)
Sharee::Sharee(const QString &shareWith, const QString &displayName, const Type type, const QString &iconUrl)
: _shareWith(shareWith)
, _displayName(displayName)
, _type(type)
, _iconUrl(iconUrl)
{
if (!_iconUrl.isEmpty()) {
// make sure no color path is contained in the url
_iconUrl.replace(QStringLiteral("/black"), "");
_iconUrl.replace(QStringLiteral("/white"), "");
_iconColor = Theme::instance()->darkMode() ? QStringLiteral("white") : QStringLiteral("black");
}
updateIconUrl();
}
QString Sharee::format() const
@ -61,9 +68,61 @@ QString Sharee::displayName() const
return _displayName;
}
void Sharee::setDisplayName(const QString &displayName)
{
if (displayName != _displayName) {
_displayName = displayName;
}
}
void Sharee::setIconUrl(const QString &iconUrl)
{
if (iconUrl != _iconUrl) {
_iconUrl = iconUrl;
}
}
void Sharee::setType(const Type &type)
{
if (type != _type) {
_type = type;
}
}
void Sharee::setIsIconColourful(const bool isColourful)
{
if (_isIconColourful != isColourful) {
_isIconColourful = isColourful;
updateIconUrl();
}
}
bool Sharee::updateIconUrl()
{
if (_iconUrl.isEmpty() || !_isIconColourful) {
return false;
}
const auto iconUrlColoured = _iconUrlColoured;
_iconColor = (!_isIconColourful || !Theme::instance()->darkMode()) ? QStringLiteral("black") : QStringLiteral("white");
_iconUrlColoured = QStringLiteral("image://svgimage-custom-color/") + _iconUrl + QStringLiteral("/") + _iconColor;
return iconUrlColoured != _iconUrlColoured;
}
Sharee::Type Sharee::type() const
{
return _type;
}
QString Sharee::iconUrl() const
{
return _iconUrl;
}
QString Sharee::iconUrlColoured() const
{
return _iconUrlColoured;
}
}

View File

@ -35,35 +35,47 @@ Q_DECLARE_LOGGING_CATEGORY(lcSharing)
class Sharee
{
Q_GADGET
Q_PROPERTY(QString format READ format)
Q_PROPERTY(QString shareWith MEMBER _shareWith)
Q_PROPERTY(QString displayName MEMBER _displayName)
Q_PROPERTY(QString iconUrlColoured MEMBER _iconUrlColoured)
Q_PROPERTY(Type type MEMBER _type)
public:
// Keep in sync with Share::ShareType
enum Type {
User = 0,
Group = 1,
Email = 4,
Federated = 6,
Circle = 7,
Room = 10
};
explicit Sharee(const QString shareWith,
const QString displayName,
const Type type);
enum Type { Invalid = -1, User = 0, Group = 1, Email = 4, Federated = 6, Circle = 7, Room = 10, LookupServerSearch = 999, LookupServerSearchResults = 1000 };
Q_ENUM(Type);
explicit Sharee() = default;
explicit Sharee(const QString &shareWith, const QString &displayName, const Type type, const QString &iconUrl = {});
[[nodiscard]] QString format() const;
[[nodiscard]] QString shareWith() const;
[[nodiscard]] QString displayName() const;
[[nodiscard]] QString iconUrl() const;
[[nodiscard]] QString iconUrlColoured() const;
[[nodiscard]] Type type() const;
bool updateIconUrl();
void setDisplayName(const QString &displayName);
void setType(const Type &type);
void setIsIconColourful(const bool isColourful);
void setIconUrl(const QString &iconUrl);
private:
QString _shareWith;
QString _displayName;
Type _type;
QString _iconUrlColoured;
QString _iconColor;
Type _type = Type::Invalid;
QString _iconUrl;
bool _isIconColourful = false;
};
using ShareePtr = QSharedPointer<OCC::Sharee>;
}
Q_DECLARE_METATYPE(OCC::ShareePtr)
Q_DECLARE_METATYPE(OCC::Sharee)
#endif //SHAREE_H

View File

@ -33,6 +33,8 @@ class TestShareeModel : public QObject
{
Q_OBJECT
int _numLookupSearchParamSet = 0;
public:
~TestShareeModel() override
{
@ -62,6 +64,9 @@ public:
QString category;
switch(definition.type) {
case Sharee::Invalid:
category = QStringLiteral("invalid");
break;
case Sharee::Circle:
category = QStringLiteral("circles");
break;
@ -80,6 +85,12 @@ public:
case Sharee::User:
category = QStringLiteral("users");
break;
case Sharee::LookupServerSearch:
category = QStringLiteral("placeholder_lookupserversearch");
break;
case Sharee::LookupServerSearchResults:
category = QStringLiteral("placeholder_lookupserversearchresults");
break;
}
auto shareesInCategory = _shareesMap.value(category).toJsonArray();
@ -244,6 +255,10 @@ private slots:
const auto lookupParam = urlQuery.queryItemValue(QStringLiteral("lookup"));
const auto formatParam = urlQuery.queryItemValue(QStringLiteral("format"));
if (!lookupParam.isEmpty() && lookupParam == QStringLiteral("true")) {
++_numLookupSearchParamSet;
}
if (formatParam != QStringLiteral("json")) {
reply = new FakeErrorReply(op, req, this, 400, fake400Response);
} else {
@ -324,12 +339,51 @@ private slots:
const auto searchString = QStringLiteral("i");
model.setSearchString(searchString);
QVERIFY(shareesReady.wait(3000));
QCOMPARE(model.rowCount(), shareesCount(searchString));
QCOMPARE(model.rowCount(), shareesCount(searchString) + 1);
QVERIFY(model.rowCount() > 0);
auto lastElementType = model.data(model.index(model.rowCount() - 1), ShareeModel::Roles::TypeRole).toInt();
QVERIFY(lastElementType == Sharee::Type::LookupServerSearch);
const auto emailSearchString = QStringLiteral("email");
model.setSearchString(emailSearchString);
QVERIFY(shareesReady.wait(3000));
QCOMPARE(model.rowCount(), shareesCount(emailSearchString));
QCOMPARE(model.rowCount(), shareesCount(emailSearchString) + 1);
QVERIFY(model.rowCount() > 0);
lastElementType = model.data(model.index(model.rowCount() - 1), ShareeModel::Roles::TypeRole).toInt();
QVERIFY(lastElementType == Sharee::Type::LookupServerSearch);
}
void testShareesFetchGlobally()
{
resetTestData();
standardReplyPopulate();
ShareeModel model;
QAbstractItemModelTester modelTester(&model);
QCOMPARE(model.rowCount(), 0);
model.setAccountState(_accountState.data());
QSignalSpy shareesReady(&model, &ShareeModel::shareesReady);
const auto emailSearchString = QStringLiteral("email");
model.setSearchString(emailSearchString);
QVERIFY(shareesReady.wait(3000));
QCOMPARE(model.rowCount(), shareesCount(emailSearchString) + 1);
QVERIFY(model.rowCount() > 0);
auto lastElementType = model.data(model.index(model.rowCount() - 1), ShareeModel::Roles::TypeRole).toInt();
QVERIFY(lastElementType == Sharee::Type::LookupServerSearch);
QCOMPARE(_numLookupSearchParamSet, 0);
QSignalSpy lookupModeChanged(&model, &ShareeModel::lookupModeChanged);
model.searchGlobally();
QVERIFY(shareesReady.wait(3000));
QCOMPARE(lookupModeChanged.count(), 2);
QVERIFY(model.lookupMode() == ShareeModel::LookupMode::LocalSearch);
QCOMPARE(model.rowCount(), shareesCount(emailSearchString) + 1);
QVERIFY(model.rowCount() > 0);
lastElementType = model.data(model.index(model.rowCount() - 1), ShareeModel::Roles::TypeRole).toInt();
QVERIFY(lastElementType == Sharee::Type::LookupServerSearchResults);
QCOMPARE(_numLookupSearchParamSet, 1);
}
void testFetchSignalling()
@ -367,7 +421,9 @@ private slots:
QSignalSpy shareesReady(&model, &ShareeModel::shareesReady);
QVERIFY(shareesReady.wait(3000));
QCOMPARE(model.rowCount(), shareesCount(searchString));
QCOMPARE(model.rowCount(), shareesCount(searchString) + 1);
auto lastElementType = model.data(model.index(model.rowCount() - 1), ShareeModel::Roles::TypeRole).toInt();
QVERIFY(lastElementType == Sharee::Type::LookupServerSearch);
const auto shareeIndex = model.index(0, 0, {});
@ -409,12 +465,16 @@ private slots:
const auto searchString = QStringLiteral("i");
model.setSearchString(searchString);
QVERIFY(shareesReady.wait(3000));
QCOMPARE(model.rowCount(), shareesCount(searchString) - 1);
QCOMPARE(model.rowCount(), shareesCount(searchString) - 1 + 1);
auto lastElementType = model.data(model.index(model.rowCount() - 1), ShareeModel::Roles::TypeRole).toInt();
QVERIFY(lastElementType == Sharee::Type::LookupServerSearch);
const ShareePtr shareeTwo(new Sharee(_michaelUserDefinition.shareWith, _michaelUserDefinition.label, _michaelUserDefinition.type));
const QVariantList largerShareeBlocklist {QVariant::fromValue(sharee), QVariant::fromValue(shareeTwo)};
model.setShareeBlocklist(largerShareeBlocklist);
QCOMPARE(model.rowCount(), shareesCount(searchString) - 2);
QCOMPARE(model.rowCount(), shareesCount(searchString) - 2 + 1);
lastElementType = model.data(model.index(model.rowCount() - 1), ShareeModel::Roles::TypeRole).toInt();
QVERIFY(lastElementType == Sharee::Type::LookupServerSearch);
}
void testServerError()