Align, resize, and layout everything uniformly in the unified search view

Signed-off-by: Claudio Cambra <claudio.cambra@gmail.com>
This commit is contained in:
Claudio Cambra 2022-08-08 23:40:19 +02:00 committed by Matthieu Gallien
parent b248696062
commit 316f3981ab
12 changed files with 218 additions and 114 deletions

View File

@ -23,6 +23,7 @@
<file>src/gui/tray/UnifiedSearchResultItem.qml</file>
<file>src/gui/tray/UnifiedSearchResultItemSkeleton.qml</file>
<file>src/gui/tray/UnifiedSearchResultItemSkeletonContainer.qml</file>
<file>src/gui/tray/UnifiedSearchResultItemSkeletonGradientRectangle.qml</file>
<file>src/gui/tray/UnifiedSearchResultListItem.qml</file>
<file>src/gui/tray/UnifiedSearchResultNothingFound.qml</file>
<file>src/gui/tray/UnifiedSearchResultSectionItem.qml</file>

View File

@ -8,7 +8,6 @@ ColumnLayout {
id: unifiedSearchResultItemFetchMore
property bool isFetchMoreInProgress: false
property bool isWithinViewPort: false
property int fontSize: Style.unifiedSearchResultTitleFontSize
@ -21,22 +20,29 @@ ColumnLayout {
Label {
id: unifiedSearchResultItemFetchMoreText
text: qsTr("Load more results")
visible: !unifiedSearchResultItemFetchMore.isFetchMoreInProgress
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
Layout.fillWidth: true
Layout.fillHeight: true
Layout.leftMargin: Style.trayHorizontalMargin
Layout.rightMargin: Style.trayHorizontalMargin
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
text: qsTr("Load more results")
wrapMode: Text.Wrap
font.pixelSize: unifiedSearchResultItemFetchMore.fontSize
color: unifiedSearchResultItemFetchMore.textColor
visible: !unifiedSearchResultItemFetchMore.isFetchMoreInProgress
}
BusyIndicator {
id: unifiedSearchResultItemFetchMoreIconInProgress
running: visible
visible: unifiedSearchResultItemFetchMore.isFetchMoreInProgress && unifiedSearchResultItemFetchMore.isWithinViewPort
Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
Layout.preferredWidth: parent.height * 0.70
Layout.preferredHeight: parent.height * 0.70
Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
running: visible
visible: unifiedSearchResultItemFetchMore.isFetchMoreInProgress && unifiedSearchResultItemFetchMore.isWithinViewPort
}
}

View File

@ -2,9 +2,10 @@ import QtQml 2.15
import QtQuick 2.9
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.2
import Style 1.0
import QtGraphicalEffects 1.0
import Style 1.0
RowLayout {
id: unifiedSearchResultItemDetails
@ -12,14 +13,11 @@ RowLayout {
property string subline: ""
property string icons: ""
property string iconPlaceholder: ""
property bool iconsIsThumbnail: false
property bool isRounded: false
property int textLeftMargin: Style.unifiedSearchResultTextLeftMargin
property int textRightMargin: Style.unifiedSearchResultTextRightMargin
property int iconWidth: Style.unifiedSearchResultIconWidth
property int iconLeftMargin: Style.unifiedSearchResultIconLeftMargin
property int iconWidth: iconsIsThumbnail && icons !== "" ? Style.unifiedSearchResultIconWidth : Style.unifiedSearchResultSmallIconWidth
property int titleFontSize: Style.unifiedSearchResultTitleFontSize
property int sublineFontSize: Style.unifiedSearchResultSublineFontSize
@ -30,76 +28,78 @@ RowLayout {
Accessible.name: resultTitle
Accessible.onPressAction: unifiedSearchResultMouseArea.clicked()
ColumnLayout {
spacing: Style.trayHorizontalMargin
Item {
id: unifiedSearchResultImageContainer
visible: true
Layout.preferredWidth: unifiedSearchResultItemDetails.iconWidth + 10
Layout.preferredHeight: unifiedSearchResultItemDetails.height
property int whiteSpace: (Style.trayListItemIconSize - unifiedSearchResultItemDetails.iconWidth)
Layout.preferredWidth: unifiedSearchResultItemDetails.iconWidth
Layout.preferredHeight: unifiedSearchResultItemDetails.iconWidth
Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
Layout.leftMargin: Style.trayHorizontalMargin + (whiteSpace * (0.5 - Style.thumbnailImageSizeReduction))
Layout.rightMargin: whiteSpace * (0.5 + Style.thumbnailImageSizeReduction)
Image {
id: unifiedSearchResultThumbnail
anchors.fill: parent
visible: false
asynchronous: true
source: "image://tray-image-provider/" + icons
source: "image://tray-image-provider/" + unifiedSearchResultItemDetails.icons
cache: true
sourceSize.width: imageData.width
sourceSize.height: imageData.height
width: imageData.width
height: imageData.height
verticalAlignment: Qt.AlignVCenter
horizontalAlignment: Qt.AlignHCenter
sourceSize.width: width
sourceSize.height: height
}
Rectangle {
id: mask
anchors.fill: unifiedSearchResultThumbnail
visible: false
radius: isRounded ? width / 2 : 0
width: imageData.width
height: imageData.height
radius: unifiedSearchResultItemDetails.isRounded ? width / 2 : 3
}
OpacityMask {
id: imageData
visible: !unifiedSearchResultThumbnailPlaceholder.visible && icons
Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
Layout.leftMargin: iconLeftMargin
Layout.preferredWidth: unifiedSearchResultItemDetails.iconWidth
Layout.preferredHeight: unifiedSearchResultItemDetails.iconWidth
anchors.fill: unifiedSearchResultThumbnail
visible: unifiedSearchResultItemDetails.icons !== ""
source: unifiedSearchResultThumbnail
maskSource: mask
}
Image {
id: unifiedSearchResultThumbnailPlaceholder
Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
Layout.leftMargin: iconLeftMargin
verticalAlignment: Qt.AlignCenter
anchors.fill: parent
verticalAlignment: Qt.AlignVCenter
horizontalAlignment: Qt.AlignHCenter
cache: true
source: "image://tray-image-provider/" + iconPlaceholder
visible: false
source: "image://tray-image-provider/" + unifiedSearchResultItemDetails.iconPlaceholder
visible: unifiedSearchResultItemDetails.iconPlaceholder !== "" && unifiedSearchResultItemDetails.icons === ""
sourceSize.height: unifiedSearchResultItemDetails.iconWidth
sourceSize.width: unifiedSearchResultItemDetails.iconWidth
Layout.preferredWidth: unifiedSearchResultItemDetails.iconWidth
Layout.preferredHeight: unifiedSearchResultItemDetails.iconWidth
}
}
ColumnLayout {
id: unifiedSearchResultTextContainer
Layout.fillWidth: true
Layout.rightMargin: Style.trayHorizontalMargin
spacing: Style.standardSpacing
Label {
id: unifiedSearchResultTitleText
text: title.replace(/[\r\n]+/g, " ")
Layout.leftMargin: textLeftMargin
Layout.rightMargin: textRightMargin
Layout.fillWidth: true
text: unifiedSearchResultItemDetails.title.replace(/[\r\n]+/g, " ")
elide: Text.ElideRight
font.pixelSize: unifiedSearchResultItemDetails.titleFontSize
color: unifiedSearchResultItemDetails.titleColor
}
Label {
id: unifiedSearchResultTextSubline
text: subline.replace(/[\r\n]+/g, " ")
Layout.fillWidth: true
text: unifiedSearchResultItemDetails.subline.replace(/[\r\n]+/g, " ")
elide: Text.ElideRight
font.pixelSize: unifiedSearchResultItemDetails.sublineFontSize
Layout.leftMargin: textLeftMargin
Layout.rightMargin: textRightMargin
Layout.fillWidth: true
color: unifiedSearchResultItemDetails.sublineColor
}
}

View File

@ -7,11 +7,7 @@ import Style 1.0
RowLayout {
id: unifiedSearchResultSkeletonItemDetails
property int textLeftMargin: Style.unifiedSearchResultTextLeftMargin
property int textRightMargin: Style.unifiedSearchResultTextRightMargin
property int iconWidth: Style.unifiedSearchResultIconWidth
property int iconLeftMargin: Style.unifiedSearchResultIconLeftMargin
property int titleFontSize: Style.unifiedSearchResultTitleFontSize
property int sublineFontSize: Style.unifiedSearchResultSublineFontSize
@ -19,6 +15,7 @@ RowLayout {
Accessible.name: qsTr("Search result skeleton.").arg(model.index)
height: Style.trayWindowHeaderHeight
spacing: Style.trayHorizontalMargin
/*
* An overview of what goes on here:
@ -41,52 +38,16 @@ RowLayout {
*/
property color baseGradientColor: Style.lightHover
property color progressGradientColor: Style.darkMode ? Qt.lighter(baseGradientColor, 1.2) : Qt.darker(baseGradientColor, 1.1)
property int animationRectangleWidth: Style.trayWindowWidth
property int animationStartX: -animationRectangleWidth
property int animationEndX: animationRectangleWidth
Component {
id: gradientAnimationRectangle
Rectangle {
width: unifiedSearchResultSkeletonItemDetails.animationRectangleWidth
height: parent.height
gradient: Gradient {
orientation: Gradient.Horizontal
GradientStop {
position: 0
color: "transparent"
}
GradientStop {
position: 0.4
color: unifiedSearchResultSkeletonItemDetails.progressGradientColor
}
GradientStop {
position: 0.6
color: unifiedSearchResultSkeletonItemDetails.progressGradientColor
}
GradientStop {
position: 1.0
color: "transparent"
}
}
NumberAnimation on x {
from: unifiedSearchResultSkeletonItemDetails.animationStartX
to: unifiedSearchResultSkeletonItemDetails.animationEndX
duration: 1000
loops: Animation.Infinite
running: true
}
}
}
Item {
property int whiteSpace: (Style.trayListItemIconSize - unifiedSearchResultSkeletonItemDetails.iconWidth)
Layout.preferredWidth: unifiedSearchResultSkeletonItemDetails.iconWidth
Layout.preferredHeight: unifiedSearchResultSkeletonItemDetails.iconWidth
Layout.leftMargin: unifiedSearchResultSkeletonItemDetails.iconLeftMargin
Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
Layout.leftMargin: Style.trayHorizontalMargin + (whiteSpace * (0.5 - Style.thumbnailImageSizeReduction))
Layout.rightMargin: whiteSpace * (0.5 + Style.thumbnailImageSizeReduction)
Rectangle {
id: unifiedSearchResultSkeletonThumbnail
@ -98,7 +59,10 @@ RowLayout {
Loader {
x: mapFromItem(unifiedSearchResultSkeletonItemDetails, 0, 0).x
height: parent.height
sourceComponent: gradientAnimationRectangle
sourceComponent: UnifiedSearchResultItemSkeletonGradientRectangle {
width: unifiedSearchResultSkeletonItemDetails.animationRectangleWidth
height: parent.height
}
}
}
@ -121,9 +85,7 @@ RowLayout {
id: unifiedSearchResultSkeletonTextContainer
Layout.fillWidth: true
Layout.leftMargin: unifiedSearchResultSkeletonItemDetails.textLeftMargin
Layout.rightMargin: unifiedSearchResultSkeletonItemDetails.textRightMargin
Layout.rightMargin: Style.trayHorizontalMargin
spacing: Style.standardSpacing
Item {
@ -140,7 +102,10 @@ RowLayout {
Loader {
x: mapFromItem(unifiedSearchResultSkeletonItemDetails, 0, 0).x
height: parent.height
sourceComponent: gradientAnimationRectangle
sourceComponent: UnifiedSearchResultItemSkeletonGradientRectangle {
width: unifiedSearchResultSkeletonItemDetails.animationRectangleWidth
height: parent.height
}
}
}
@ -173,7 +138,10 @@ RowLayout {
Loader {
x: mapFromItem(unifiedSearchResultSkeletonItemDetails, 0, 0).x
height: parent.height
sourceComponent: gradientAnimationRectangle
sourceComponent: UnifiedSearchResultItemSkeletonGradientRectangle {
width: unifiedSearchResultSkeletonItemDetails.animationRectangleWidth
height: parent.height
}
}
}

View File

@ -1,12 +1,65 @@
import QtQml 2.15
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtGraphicalEffects 1.15
import Style 1.0
Column {
ColumnLayout {
id: unifiedSearchResultsListViewSkeletonColumn
property int animationRectangleWidth: Style.trayWindowWidth
Item {
id: placeholderSectionHeader
property rect textRect: fontMetrics.boundingRect("Dummy text")
Layout.topMargin: Style.unifiedSearchResultSectionItemVerticalPadding / 2
Layout.bottomMargin: Style.unifiedSearchResultSectionItemVerticalPadding / 2
Layout.leftMargin: Style.unifiedSearchResultSectionItemLeftPadding
width: textRect.width
height: textRect.height
FontMetrics {
id: fontMetrics
font.pixelSize: Style.unifiedSearchResultTitleFontSize
}
Rectangle {
id: placeholderSectionHeaderRectangle
anchors.fill: parent
radius: Style.veryRoundedButtonRadius
color: Style.lightHover
clip: true
visible: false
Loader {
x: mapFromItem(placeholderSectionHeader, 0, 0).x
height: parent.height
sourceComponent: UnifiedSearchResultItemSkeletonGradientRectangle {
width: unifiedSearchResultsListViewSkeletonColumn.animationRectangleWidth
height: parent.height
}
}
}
Rectangle {
id: placeholderSectionHeaderMask
anchors.fill: placeholderSectionHeaderRectangle
color: "white"
radius: Style.veryRoundedButtonRadius
visible: false
}
OpacityMask {
anchors.fill: placeholderSectionHeaderRectangle
source: placeholderSectionHeaderRectangle
maskSource: placeholderSectionHeaderMask
}
}
Repeater {
model: Math.ceil(unifiedSearchResultsListViewSkeletonColumn.height / Style.trayWindowHeaderHeight)
UnifiedSearchResultItemSkeleton {

View File

@ -0,0 +1,56 @@
/*
* Copyright (C) 2022 by Claudio Cambra <claudio.cambra@nextcloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
import QtQml 2.15
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtGraphicalEffects 1.15
import Style 1.0
Rectangle {
id: root
property color progressGradientColor: Style.darkMode ? Qt.lighter(Style.lightHover, 1.2) : Qt.darker(Style.lightHover, 1.1)
property int animationStartX: -width
property int animationEndX: width
gradient: Gradient {
orientation: Gradient.Horizontal
GradientStop {
position: 0
color: "transparent"
}
GradientStop {
position: 0.4
color: root.progressGradientColor
}
GradientStop {
position: 0.6
color: root.progressGradientColor
}
GradientStop {
position: 1.0
color: "transparent"
}
}
NumberAnimation on x {
from: root.animationStartX
to: root.animationEndX
duration: 1000
loops: Animation.Infinite
running: true
}
}

View File

@ -36,24 +36,25 @@ MouseArea {
}
Loader {
anchors.fill: parent
active: !isFetchMoreTrigger
sourceComponent: UnifiedSearchResultItem {
width: unifiedSearchResultMouseArea.width
height: unifiedSearchResultMouseArea.height
anchors.fill: parent
title: model.resultTitle
subline: model.subline
icons: Theme.darkMode ? model.darkIcons : model.lightIcons
iconsIsThumbnail: Theme.darkMode ? model.darkIconsIsThumbnail : model.lightIconsIsThumbnail
iconPlaceholder: Theme.darkMode ? model.darkImagePlaceholder : model.lightImagePlaceholder
isRounded: model.isRounded
isRounded: model.isRounded && iconsIsThumbnail
}
}
Loader {
anchors.fill: parent
active: isFetchMoreTrigger
sourceComponent: UnifiedSearchResultFetchMoreTrigger {
anchors.fill: parent
isFetchMoreInProgress: unifiedSearchResultMouseArea.isFetchMoreInProgress
width: unifiedSearchResultMouseArea.width
height: unifiedSearchResultMouseArea.height
isWithinViewPort: !unifiedSearchResultMouseArea.isPooled
}
}

View File

@ -741,10 +741,12 @@ ApplicationWindow {
Loader {
id: unifiedSearchResultsListViewSkeletonLoader
anchors.top: trayWindowUnifiedSearchInputContainer.bottom
anchors.left: trayWindowMainItem.left
anchors.right: trayWindowMainItem.right
anchors.bottom: trayWindowMainItem.bottom
anchors.margins: controlRoot.padding
active: !unifiedSearchResultNothingFound.visible &&
!unifiedSearchResultsListView.visible &&
@ -753,6 +755,7 @@ ApplicationWindow {
sourceComponent: UnifiedSearchResultItemSkeletonContainer {
anchors.fill: parent
spacing: unifiedSearchResultsListView.spacing
animationRectangleWidth: trayWindow.width
}
}

View File

@ -44,6 +44,8 @@ struct UnifiedSearchResult
QUrl _resourceUrl;
QString _darkIcons;
QString _lightIcons;
bool _darkIconsIsThumbnail;
bool _lightIconsIsThumbnail;
Type _type = Type::Default;
};
}

View File

@ -155,7 +155,8 @@ QString generateUrlForIcon(const QString &fallbackIcon, const QUrl &serverUrl, c
return fallbackIconCopy;
}
QString iconsFromThumbnailAndFallbackIcon(const QString &thumbnailUrl, const QString &fallbackIcon, const QUrl &serverUrl, const bool darkMode)
// Return image URL and whether it is a thumbnail or not
std::pair<QString, bool> iconsFromThumbnailAndFallbackIcon(const QString &thumbnailUrl, const QString &fallbackIcon, const QUrl &serverUrl, const bool darkMode)
{
if (thumbnailUrl.isEmpty() && fallbackIcon.isEmpty()) {
return {};
@ -163,7 +164,7 @@ QString iconsFromThumbnailAndFallbackIcon(const QString &thumbnailUrl, const QSt
if (serverUrl.isEmpty()) {
const QStringList listImages = {thumbnailUrl, fallbackIcon};
return listImages.join(QLatin1Char(';'));
return {listImages.join(QLatin1Char(';')), false};
}
const auto urlForThumbnail = generateUrlForThumbnail(thumbnailUrl, serverUrl);
@ -172,15 +173,15 @@ QString iconsFromThumbnailAndFallbackIcon(const QString &thumbnailUrl, const QSt
qDebug() << "SEARCH" << urlForThumbnail << urlForFallbackIcon;
if (urlForThumbnail.isEmpty() && !urlForFallbackIcon.isEmpty()) {
return urlForFallbackIcon;
return {urlForFallbackIcon, false};
}
if (!urlForThumbnail.isEmpty() && urlForFallbackIcon.isEmpty()) {
return urlForThumbnail;
return {urlForThumbnail, true};
}
const QStringList listImages{urlForThumbnail, urlForFallbackIcon};
return listImages.join(QLatin1Char(';'));
return {listImages.join(QLatin1Char(';')), true};
}
constexpr int searchTermEditingFinishedSearchStartDelay = 800;
@ -214,6 +215,10 @@ QVariant UnifiedSearchResultsListModel::data(const QModelIndex &index, int role)
return _results.at(index.row())._darkIcons;
case LightIconsRole:
return _results.at(index.row())._lightIcons;
case DarkIconsIsThumbnailRole:
return _results.at(index.row())._darkIconsIsThumbnail;
case LightIconsIsThumbnailRole:
return _results.at(index.row())._lightIconsIsThumbnail;
case TitleRole:
return _results.at(index.row())._title;
case SublineRole:
@ -247,6 +252,8 @@ QHash<int, QByteArray> UnifiedSearchResultsListModel::roleNames() const
roles[ProviderIdRole] = "providerId";
roles[DarkIconsRole] = "darkIcons";
roles[LightIconsRole] = "lightIcons";
roles[DarkIconsIsThumbnailRole] = "darkIconsIsThumbnail";
roles[LightIconsIsThumbnailRole] = "lightIconsIsThumbnail";
roles[DarkImagePlaceholderRole] = "darkImagePlaceholder";
roles[LightImagePlaceholderRole] = "lightImagePlaceholder";
roles[TitleRole] = "resultTitle";
@ -585,10 +592,14 @@ void UnifiedSearchResultsListModel::parseResultsForProvider(const QJsonObject &d
const auto accountUrl = (_accountState && _accountState->account()) ? _accountState->account()->url() : QUrl();
result._resourceUrl = makeResourceUrl(resourceUrl, accountUrl);
result._darkIcons = iconsFromThumbnailAndFallbackIcon(entryMap.value(QStringLiteral("thumbnailUrl")).toString(),
entryMap.value(QStringLiteral("icon")).toString(), accountUrl, true);
result._lightIcons = iconsFromThumbnailAndFallbackIcon(entryMap.value(QStringLiteral("thumbnailUrl")).toString(),
entryMap.value(QStringLiteral("icon")).toString(), accountUrl, false);
const auto darkIconsData = iconsFromThumbnailAndFallbackIcon(entryMap.value(QStringLiteral("thumbnailUrl")).toString(),
entryMap.value(QStringLiteral("icon")).toString(), accountUrl, true);
const auto lightIconsData = iconsFromThumbnailAndFallbackIcon(entryMap.value(QStringLiteral("thumbnailUrl")).toString(),
entryMap.value(QStringLiteral("icon")).toString(), accountUrl, false);
result._darkIcons = darkIconsData.first;
result._lightIcons = lightIconsData.first;
result._darkIconsIsThumbnail = darkIconsData.second;
result._lightIconsIsThumbnail = lightIconsData.second;
newEntries.push_back(result);
}

View File

@ -57,6 +57,8 @@ public:
LightImagePlaceholderRole,
DarkIconsRole,
LightIconsRole,
DarkIconsIsThumbnailRole,
LightIconsIsThumbnailRole,
TitleRole,
SublineRole,
ResourceUrlRole,

View File

@ -88,7 +88,7 @@ QtObject {
property int roundButtonBackgroundVerticalMargins: 10
property int roundedButtonBackgroundVerticalMargins: 5
property int userStatusEmojiSize: 8
property int userStatusSpacing: trayHorizontalMargin
property int userStatusAnchorsMargin: 2
@ -105,7 +105,8 @@ QtObject {
readonly property int unifiedSearchItemHeight: trayWindowHeaderHeight
readonly property int unifiedSearchResultTextLeftMargin: 18
readonly property int unifiedSearchResultTextRightMargin: 16
readonly property int unifiedSearchResultIconWidth: 24
readonly property int unifiedSearchResultIconWidth: trayListItemIconSize * (1 - thumbnailImageSizeReduction)
readonly property int unifiedSearchResultSmallIconWidth: trayListItemIconSize * (1 - thumbnailImageSizeReduction * 2)
readonly property int unifiedSearchResultIconLeftMargin: 12
readonly property int unifiedSearchResultTitleFontSize: topLinePixelSize
readonly property int unifiedSearchResultSublineFontSize: subLinePixelSize
@ -118,6 +119,6 @@ QtObject {
readonly property int activityContentSpace: 4
function variableSize(size) {
return size * (1 + Math.min(pixelSize / 100, 1));
return size * (1 + Math.min(pixelSize / 100, 1));
}
}