From 7b4245a3c3b65c8fc124f8d81523fdc3a7f1bfc0 Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Fri, 28 Apr 2023 12:43:41 +0200 Subject: [PATCH 01/28] fix #include header style Signed-off-by: Matthieu Gallien --- src/gui/tray/activitylistmodel.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gui/tray/activitylistmodel.cpp b/src/gui/tray/activitylistmodel.cpp index b820ed941c2b1..434074cf069d6 100644 --- a/src/gui/tray/activitylistmodel.cpp +++ b/src/gui/tray/activitylistmodel.cpp @@ -32,7 +32,7 @@ #include #include #include -#include +#include namespace OCC { From 1491c134c3fd9326e401c31fd68db9c2328182ba Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Fri, 28 Apr 2023 12:43:04 +0200 Subject: [PATCH 02/28] basic implementation of a dialog to resolve conflicts as a batch will allow solving all conflicts at once FIX #2786 Signed-off-by: Matthieu Gallien --- resources.qrc | 2 + src/gui/ConflictDelegate.qml | 151 +++++++++++++++++++++++ src/gui/ResolveConflictsDialog.qml | 189 +++++++++++++++++++++++++++++ src/gui/main.cpp | 2 +- src/gui/systray.cpp | 15 +++ src/gui/systray.h | 1 + src/gui/tray/activitylistmodel.cpp | 62 +++++----- src/gui/tray/activitylistmodel.h | 2 + 8 files changed, 396 insertions(+), 28 deletions(-) create mode 100644 src/gui/ConflictDelegate.qml create mode 100644 src/gui/ResolveConflictsDialog.qml diff --git a/resources.qrc b/resources.qrc index 2d7f227484ca2..12ed9ae1d9229 100644 --- a/resources.qrc +++ b/resources.qrc @@ -56,5 +56,7 @@ src/gui/tray/ListItemLineAndSubline.qml src/gui/tray/TrayFoldersMenuButton.qml src/gui/tray/TrayFolderListItem.qml + src/gui/ResolveConflictsDialog.qml + src/gui/ConflictDelegate.qml diff --git a/src/gui/ConflictDelegate.qml b/src/gui/ConflictDelegate.qml new file mode 100644 index 0000000000000..aa2a138f31192 --- /dev/null +++ b/src/gui/ConflictDelegate.qml @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2023 by Matthieu Gallien + * + * 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 QtQuick.Controls 2.15 +import Style 1.0 +import com.nextcloud.desktopclient 1.0 +import "./tray" + +Item { + id: root + + required property string existingFileName + required property string conflictFileName + required property string existingSize + required property string conflictSize + required property string existingDate + required property string conflictDate + required property bool existingSelected + required property bool conflictSelected + + EnforcedPlainTextLabel { + id: existingFileNameLabel + + anchors.top: parent.top + anchors.left: parent.left + + text: root.existingFileName + + font.weight: Font.Light + font.pixelSize: 15 + } + + RowLayout { + anchors.top: existingFileNameLabel.bottom + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + + Image { + id: existingPreview + + anchors.top: parent.top + anchors.left: parent.left + + source: 'https://nextcloud.local/index.php/apps/theming/img/core/filetypes/text.svg?v=b9feb2d6' + width: 64 + height: 64 + sourceSize.width: 64 + sourceSize.height: 64 + } + + ColumnLayout { + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.left: existingPreview.right + anchors.right: parent.right + + CheckBox { + id: selectExisting + + Layout.alignment: Layout.TopLeft + + checked: root.existingSelected + } + + EnforcedPlainTextLabel { + Layout.fillWidth: true + + text: root.existingDate + + font.pixelSize: 15 + } + + EnforcedPlainTextLabel { + Layout.fillWidth: true + + text: existingSize + + font.pixelSize: 15 + } + } + } + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + + Image { + id: conflictPreview + + anchors.top: parent.top + anchors.left: parent.left + + source: 'https://nextcloud.local/index.php/apps/theming/img/core/filetypes/text.svg?v=b9feb2d6' + width: 64 + height: 64 + sourceSize.width: 64 + sourceSize.height: 64 + } + + ColumnLayout { + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.left: conflictPreview.right + anchors.right: parent.right + + CheckBox { + id: selectConflict + + Layout.alignment: Layout.TopLeft + + checked: root.conflictSelected + } + + EnforcedPlainTextLabel { + Layout.fillWidth: true + + text: root.conflictDate + + font.pixelSize: 15 + } + + EnforcedPlainTextLabel { + Layout.fillWidth: true + + text: conflictSize + + font.pixelSize: 15 + } + } + } + } +} diff --git a/src/gui/ResolveConflictsDialog.qml b/src/gui/ResolveConflictsDialog.qml new file mode 100644 index 0000000000000..251d4d2ad620d --- /dev/null +++ b/src/gui/ResolveConflictsDialog.qml @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2023 by Matthieu Gallien + * + * 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.Window 2.15 +import QtQuick.Layouts 1.15 +import QtQuick.Controls 2.15 +import QtQml.Models 2.15 +import Style 1.0 +import com.nextcloud.desktopclient 1.0 +import "./tray" + +Window { + id: root + + flags: Qt.Dialog + visible: true + + width: 600 + height: 800 + minimumWidth: 600 + minimumHeight: 800 + + onClosing: function() { + Systray.destroyDialog(root); + } + + Component.onCompleted: { + Systray.forceWindowInit(root); + Systray.positionNotificationWindow(root); + + root.show(); + root.raise(); + root.requestActivate(); + } + + ColumnLayout { + anchors.fill: parent + anchors.leftMargin: 20 + anchors.rightMargin: 20 + anchors.bottomMargin: 20 + anchors.topMargin: 20 + spacing: 15 + z: 2 + + EnforcedPlainTextLabel { + text: qsTr("%1 files in conflict").arg(12) + font.bold: true + font.pixelSize: 20 + Layout.fillWidth: true + } + + EnforcedPlainTextLabel { + text: qsTr("Which files do you want to keep?") + font.pixelSize: 15 + Layout.fillWidth: true + } + + EnforcedPlainTextLabel { + text: qsTr("If you select both versions, the local file will have a number added to its name.") + font.pixelSize: 15 + Layout.fillWidth: true + Layout.topMargin: -15 + } + + RowLayout { + Layout.fillWidth: true + Layout.topMargin: 15 + + CheckBox { + id: selectExisting + + Layout.fillWidth: true + Layout.alignment: Layout.TopLeft + + text: qsTr('Local version') + + font.pixelSize: 15 + } + + CheckBox { + id: selectConflict + + Layout.fillWidth: true + Layout.alignment: Layout.TopLeft + + text: qsTr('Server version') + + font.pixelSize: 15 + } + } + + Rectangle { + Layout.fillWidth: true + Layout.leftMargin: 5 + Layout.rightMargin: 5 + color: Style.menuBorder + height: 1 + } + + ScrollView { + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + + ListView { + id: conflictListView + + model: DelegateModel { + model: ListModel { + ListElement { + existingFileName: 'Text File.txt' + conflictFileName: 'Text File.txt' + existingSize: '2 B' + conflictSize: '15 B' + existingDate: '28 avril 2023 09:53' + conflictDate: '28 avril 2023 09:53' + existingSelected: false + conflictSelected: false + } + + ListElement { + existingFileName: 'Text File.txt' + conflictFileName: 'Text File.txt' + existingSize: '2 B' + conflictSize: '15 B' + existingDate: '28 avril 2023 09:53' + conflictDate: '28 avril 2023 09:53' + existingSelected: false + conflictSelected: false + } + + ListElement { + existingFileName: 'Text File.txt' + conflictFileName: 'Text File.txt' + existingSize: '2 B' + conflictSize: '15 B' + existingDate: '28 avril 2023 09:53' + conflictDate: '28 avril 2023 09:53' + existingSelected: false + conflictSelected: false + } + } + + delegate: ConflictDelegate { + width: conflictListView.contentItem.width + height: 100 + } + } + } + } + + DialogButtonBox { + Layout.fillWidth: true + + standardButtons: DialogButtonBox.Ok | DialogButtonBox.Cancel + + onAccepted: function() { + console.log("Ok clicked") + Systray.destroyDialog(root) + } + + onRejected: function() { + console.log("Cancel clicked") + Systray.destroyDialog(root) + } + } + } + + Rectangle { + color: Theme.systemPalette.window + anchors.fill: parent + z: 1 + } +} diff --git a/src/gui/main.cpp b/src/gui/main.cpp index 40af80ac019c5..a53aa160b1d54 100644 --- a/src/gui/main.cpp +++ b/src/gui/main.cpp @@ -66,7 +66,7 @@ int main(int argc, char **argv) // the platformtheme plugin won't try to force qqc2-desktops-style // anymore. // Can be removed once the bug in qqc2-desktop-style is gone. - QQuickStyle::setStyle("Default"); + QQuickStyle::setStyle("Fusion"); // OpenSSL 1.1.0: No explicit initialisation or de-initialisation is necessary. diff --git a/src/gui/systray.cpp b/src/gui/systray.cpp index c45b870b09cb9..f2e1d12d07749 100644 --- a/src/gui/systray.cpp +++ b/src/gui/systray.cpp @@ -285,6 +285,21 @@ void Systray::destroyEditFileLocallyLoadingDialog() _editFileLocallyLoadingDialog = nullptr; } +void Systray::createResolveConflictsDialog() +{ + const auto callDialog = new QQmlComponent(_trayEngine, QStringLiteral("qrc:/qml/src/gui/ResolveConflictsDialog.qml")); + const QVariantMap initialProperties{}; + + if(callDialog->isError()) { + qCWarning(lcSystray) << callDialog->errorString(); + return; + } + + // This call dialog gets deallocated on close conditions + // by a call from the QML side to the destroyDialog slot + callDialog->createWithInitialProperties(initialProperties); +} + bool Systray::raiseDialogs() { return raiseFileDetailDialogs(); diff --git a/src/gui/systray.h b/src/gui/systray.h index fdc9861752b48..69c34a65f4adf 100644 --- a/src/gui/systray.h +++ b/src/gui/systray.h @@ -121,6 +121,7 @@ public slots: void createCallDialog(const OCC::Activity &callNotification, const OCC::AccountStatePtr accountState); void createEditFileLocallyLoadingDialog(const QString &fileName); void destroyEditFileLocallyLoadingDialog(); + void createResolveConflictsDialog(); void slotCurrentUserChanged(); diff --git a/src/gui/tray/activitylistmodel.cpp b/src/gui/tray/activitylistmodel.cpp index 434074cf069d6..f3cb3838f3ee8 100644 --- a/src/gui/tray/activitylistmodel.cpp +++ b/src/gui/tray/activitylistmodel.cpp @@ -640,34 +640,8 @@ void ActivityListModel::slotTriggerDefaultAction(const int activityIndex) const auto activity = _finalList.at(activityIndex); if (activity._syncFileItemStatus == SyncFileItem::Conflict) { - Q_ASSERT(!activity._file.isEmpty()); - Q_ASSERT(!activity._folder.isEmpty()); - Q_ASSERT(Utility::isConflictFile(activity._file)); + displaySingleConflictDialog(activity); - const auto folder = FolderMan::instance()->folder(activity._folder); - - const auto conflictedRelativePath = activity._file; - const auto baseRelativePath = folder->journalDb()->conflictFileBaseName(conflictedRelativePath.toUtf8()); - - const auto dir = QDir(folder->path()); - const auto conflictedPath = dir.filePath(conflictedRelativePath); - const auto basePath = dir.filePath(baseRelativePath); - - const auto baseName = QFileInfo(basePath).fileName(); - - if (!_currentConflictDialog.isNull()) { - _currentConflictDialog->close(); - } - _currentConflictDialog = new ConflictDialog; - _currentConflictDialog->setBaseFilename(baseName); - _currentConflictDialog->setLocalVersionFilename(conflictedPath); - _currentConflictDialog->setRemoteVersionFilename(basePath); - _currentConflictDialog->setAttribute(Qt::WA_DeleteOnClose); - connect(_currentConflictDialog, &ConflictDialog::accepted, folder, [folder]() { - folder->scheduleThisFolderSoon(); - }); - _currentConflictDialog->open(); - ownCloudGui::raiseDialog(_currentConflictDialog); return; } else if (activity._syncFileItemStatus == SyncFileItem::FileNameClash) { triggerCaseClashAction(activity); @@ -730,6 +704,40 @@ void ActivityListModel::triggerCaseClashAction(Activity activity) ownCloudGui::raiseDialog(_currentCaseClashFilenameDialog); } +void ActivityListModel::displaySingleConflictDialog(const Activity &activity) +{ + Q_ASSERT(!activity._file.isEmpty()); + Q_ASSERT(!activity._folder.isEmpty()); + Q_ASSERT(Utility::isConflictFile(activity._file)); + + const auto folder = FolderMan::instance()->folder(activity._folder); + + const auto conflictedRelativePath = activity._file; + const auto baseRelativePath = folder->journalDb()->conflictFileBaseName(conflictedRelativePath.toUtf8()); + + const auto dir = QDir(folder->path()); + const auto conflictedPath = dir.filePath(conflictedRelativePath); + const auto basePath = dir.filePath(baseRelativePath); + + const auto baseName = QFileInfo(basePath).fileName(); + + if (!_currentConflictDialog.isNull()) { + _currentConflictDialog->close(); + } + _currentConflictDialog = new ConflictDialog; + _currentConflictDialog->setBaseFilename(baseName); + _currentConflictDialog->setLocalVersionFilename(conflictedPath); + _currentConflictDialog->setRemoteVersionFilename(basePath); + _currentConflictDialog->setAttribute(Qt::WA_DeleteOnClose); + connect(_currentConflictDialog, &ConflictDialog::accepted, folder, [folder]() { + folder->scheduleThisFolderSoon(); + }); + _currentConflictDialog->open(); + ownCloudGui::raiseDialog(_currentConflictDialog); + + Systray::instance()->createResolveConflictsDialog(); +} + void ActivityListModel::slotTriggerAction(const int activityIndex, const int actionIndex) { if (activityIndex < 0 || activityIndex >= _finalList.size()) { diff --git a/src/gui/tray/activitylistmodel.h b/src/gui/tray/activitylistmodel.h index 3e21ebd1162f7..eae46ea78b587 100644 --- a/src/gui/tray/activitylistmodel.h +++ b/src/gui/tray/activitylistmodel.h @@ -162,6 +162,8 @@ private slots: void insertOrRemoveDummyFetchingActivity(); void triggerCaseClashAction(Activity activity); + void displaySingleConflictDialog(const Activity &activity); + Activity _notificationIgnoredFiles; Activity _dummyFetchingActivities; From 396accd208b4baff00f2eb34b8753c06b2ec0ba2 Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Fri, 28 Apr 2023 14:54:55 +0200 Subject: [PATCH 03/28] feedback from design review Signed-off-by: Matthieu Gallien --- src/gui/ConflictDelegate.qml | 77 +++++++++++++++++++++--------- src/gui/ResolveConflictsDialog.qml | 28 ++++++----- 2 files changed, 71 insertions(+), 34 deletions(-) diff --git a/src/gui/ConflictDelegate.qml b/src/gui/ConflictDelegate.qml index aa2a138f31192..6ebb8d20408f0 100644 --- a/src/gui/ConflictDelegate.qml +++ b/src/gui/ConflictDelegate.qml @@ -31,6 +31,8 @@ Item { required property string conflictDate required property bool existingSelected required property bool conflictSelected + required property url existingPreviewUrl + required property url conflictPreviewUrl EnforcedPlainTextLabel { id: existingFileNameLabel @@ -40,7 +42,7 @@ Item { text: root.existingFileName - font.weight: Font.Light + font.weight: Font.Bold font.pixelSize: 15 } @@ -54,17 +56,29 @@ Item { Layout.fillWidth: true Layout.fillHeight: true + CheckBox { + id: selectExisting + + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + + leftPadding: 0 + spacing: 0 + + checked: root.existingSelected + } + Image { id: existingPreview - anchors.top: parent.top - anchors.left: parent.left + anchors.left: selectExisting.right + anchors.verticalCenter: parent.verticalCenter - source: 'https://nextcloud.local/index.php/apps/theming/img/core/filetypes/text.svg?v=b9feb2d6' - width: 64 - height: 64 - sourceSize.width: 64 - sourceSize.height: 64 + source: root.existingPreviewUrl + width: 48 + height: 48 + sourceSize.width: 48 + sourceSize.height: 48 } ColumnLayout { @@ -72,13 +86,15 @@ Item { anchors.bottom: parent.bottom anchors.left: existingPreview.right anchors.right: parent.right + anchors.leftMargin: 10 + spacing: 0 - CheckBox { - id: selectExisting + EnforcedPlainTextLabel { + Layout.fillWidth: true - Layout.alignment: Layout.TopLeft + text: qsTr('Local version') - checked: root.existingSelected + font.pixelSize: 15 } EnforcedPlainTextLabel { @@ -103,17 +119,30 @@ Item { Layout.fillWidth: true Layout.fillHeight: true + CheckBox { + id: selectConflict + + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 0 + + leftPadding: 0 + spacing: 0 + + checked: root.conflictSelected + } + Image { id: conflictPreview - anchors.top: parent.top - anchors.left: parent.left + anchors.left: selectConflict.right + anchors.verticalCenter: parent.verticalCenter - source: 'https://nextcloud.local/index.php/apps/theming/img/core/filetypes/text.svg?v=b9feb2d6' - width: 64 - height: 64 - sourceSize.width: 64 - sourceSize.height: 64 + source: root.conflictPreviewUrl + width: 48 + height: 48 + sourceSize.width: 48 + sourceSize.height: 48 } ColumnLayout { @@ -121,13 +150,15 @@ Item { anchors.bottom: parent.bottom anchors.left: conflictPreview.right anchors.right: parent.right + anchors.leftMargin: 10 + spacing: 0 - CheckBox { - id: selectConflict + EnforcedPlainTextLabel { + Layout.fillWidth: true - Layout.alignment: Layout.TopLeft + text: qsTr('Server version') - checked: root.conflictSelected + font.pixelSize: 15 } EnforcedPlainTextLabel { diff --git a/src/gui/ResolveConflictsDialog.qml b/src/gui/ResolveConflictsDialog.qml index 251d4d2ad620d..a73efe068604d 100644 --- a/src/gui/ResolveConflictsDialog.qml +++ b/src/gui/ResolveConflictsDialog.qml @@ -32,6 +32,7 @@ Window { height: 800 minimumWidth: 600 minimumHeight: 800 + title: qsTr('Solve sync conflicts') onClosing: function() { Systray.destroyDialog(root); @@ -63,13 +64,8 @@ Window { } EnforcedPlainTextLabel { - text: qsTr("Which files do you want to keep?") - font.pixelSize: 15 - Layout.fillWidth: true - } - - EnforcedPlainTextLabel { - text: qsTr("If you select both versions, the local file will have a number added to its name.") + text: qsTr("Choose if you want to keep the local version, server version, or both? If you choose both, the local file will have a number added to its name.") + wrapMode: Text.WrapAtWordBoundaryOrAnywhere font.pixelSize: 15 Layout.fillWidth: true Layout.topMargin: -15 @@ -83,9 +79,11 @@ Window { id: selectExisting Layout.fillWidth: true - Layout.alignment: Layout.TopLeft - text: qsTr('Local version') + text: qsTr('All local versions') + + leftPadding: 0 + implicitWidth: 100 font.pixelSize: 15 } @@ -94,9 +92,11 @@ Window { id: selectConflict Layout.fillWidth: true - Layout.alignment: Layout.TopLeft - text: qsTr('Server version') + text: qsTr('All server versions') + + leftPadding: 0 + implicitWidth: 100 font.pixelSize: 15 } @@ -131,6 +131,8 @@ Window { conflictDate: '28 avril 2023 09:53' existingSelected: false conflictSelected: false + existingPreviewUrl: 'https://nextcloud.local/index.php/apps/theming/img/core/filetypes/text.svg?v=b9feb2d6' + conflictPreviewUrl: 'https://nextcloud.local/index.php/apps/theming/img/core/filetypes/text.svg?v=b9feb2d6' } ListElement { @@ -142,6 +144,8 @@ Window { conflictDate: '28 avril 2023 09:53' existingSelected: false conflictSelected: false + existingPreviewUrl: 'https://nextcloud.local/index.php/apps/theming/img/core/filetypes/text.svg?v=b9feb2d6' + conflictPreviewUrl: 'https://nextcloud.local/index.php/apps/theming/img/core/filetypes/text.svg?v=b9feb2d6' } ListElement { @@ -153,6 +157,8 @@ Window { conflictDate: '28 avril 2023 09:53' existingSelected: false conflictSelected: false + existingPreviewUrl: 'https://nextcloud.local/index.php/apps/theming/img/core/filetypes/text.svg?v=b9feb2d6' + conflictPreviewUrl: 'https://nextcloud.local/index.php/apps/theming/img/core/filetypes/text.svg?v=b9feb2d6' } } From 4bd93e39d638b47a5958c1761cb30b8ced179a16 Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Tue, 2 May 2023 14:53:53 +0200 Subject: [PATCH 04/28] more qml feedback Signed-off-by: Matthieu Gallien --- src/gui/ConflictDelegate.qml | 18 ++++++++++++++++++ src/gui/ResolveConflictsDialog.qml | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/gui/ConflictDelegate.qml b/src/gui/ConflictDelegate.qml index 6ebb8d20408f0..7d386404bac52 100644 --- a/src/gui/ConflictDelegate.qml +++ b/src/gui/ConflictDelegate.qml @@ -87,8 +87,13 @@ Item { anchors.left: existingPreview.right anchors.right: parent.right anchors.leftMargin: 10 + spacing: 0 + Item { + Layout.fillHeight: true + } + EnforcedPlainTextLabel { Layout.fillWidth: true @@ -112,6 +117,10 @@ Item { font.pixelSize: 15 } + + Item { + Layout.fillHeight: true + } } } @@ -151,8 +160,13 @@ Item { anchors.left: conflictPreview.right anchors.right: parent.right anchors.leftMargin: 10 + spacing: 0 + Item { + Layout.fillHeight: true + } + EnforcedPlainTextLabel { Layout.fillWidth: true @@ -176,6 +190,10 @@ Item { font.pixelSize: 15 } + + Item { + Layout.fillHeight: true + } } } } diff --git a/src/gui/ResolveConflictsDialog.qml b/src/gui/ResolveConflictsDialog.qml index a73efe068604d..e03404fd8a165 100644 --- a/src/gui/ResolveConflictsDialog.qml +++ b/src/gui/ResolveConflictsDialog.qml @@ -64,7 +64,7 @@ Window { } EnforcedPlainTextLabel { - text: qsTr("Choose if you want to keep the local version, server version, or both? If you choose both, the local file will have a number added to its name.") + text: qsTr("Choose if you want to keep the local version, server version, or both. If you choose both, the local file will have a number added to its name.") wrapMode: Text.WrapAtWordBoundaryOrAnywhere font.pixelSize: 15 Layout.fillWidth: true From 8f61d539da2dc9c0da44f5eb4408cb82e3227ea4 Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Wed, 3 May 2023 13:59:29 +0200 Subject: [PATCH 05/28] replace "OK" button by "Resolve conflicts" to help users Signed-off-by: Matthieu Gallien --- src/gui/ResolveConflictsDialog.qml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/gui/ResolveConflictsDialog.qml b/src/gui/ResolveConflictsDialog.qml index e03404fd8a165..643b92f953072 100644 --- a/src/gui/ResolveConflictsDialog.qml +++ b/src/gui/ResolveConflictsDialog.qml @@ -173,7 +173,14 @@ Window { DialogButtonBox { Layout.fillWidth: true - standardButtons: DialogButtonBox.Ok | DialogButtonBox.Cancel + Button { + text: qsTr("Resolve conflicts") + DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole + } + Button { + text: qsTr("Cancel") + DialogButtonBox.buttonRole: DialogButtonBox.RejectRole + } onAccepted: function() { console.log("Ok clicked") From 280094b220af55faf5f71010dcbce45155b70c3c Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Tue, 2 May 2023 13:55:17 +0200 Subject: [PATCH 06/28] model: first step Signed-off-by: Matthieu Gallien --- src/gui/CMakeLists.txt | 2 + src/gui/ResolveConflictsDialog.qml | 4 + src/gui/folderman.h | 2 + src/gui/owncloudgui.cpp | 2 + src/gui/syncconflictsmodel.cpp | 173 +++++++++++++++++++++++++++++ src/gui/syncconflictsmodel.h | 89 +++++++++++++++ test/CMakeLists.txt | 1 + test/testsyncconflictsmodel.cpp | 116 +++++++++++++++++++ 8 files changed, 389 insertions(+) create mode 100644 src/gui/syncconflictsmodel.cpp create mode 100644 src/gui/syncconflictsmodel.h create mode 100644 test/testsyncconflictsmodel.cpp diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 8e67738292d26..e4af0e33a6fef 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -186,6 +186,8 @@ set(client_SRCS userstatusselectormodel.cpp emojimodel.h emojimodel.cpp + syncconflictsmodel.h + syncconflictsmodel.cpp fileactivitylistmodel.h fileactivitylistmodel.cpp filedetails/filedetails.h diff --git a/src/gui/ResolveConflictsDialog.qml b/src/gui/ResolveConflictsDialog.qml index 643b92f953072..5234b8a8a79b0 100644 --- a/src/gui/ResolveConflictsDialog.qml +++ b/src/gui/ResolveConflictsDialog.qml @@ -110,6 +110,10 @@ Window { height: 1 } + SyncConflictsModel { + id: realModel + } + ScrollView { Layout.fillWidth: true Layout.fillHeight: true diff --git a/src/gui/folderman.h b/src/gui/folderman.h index 6b9186b72e2da..933954b173f56 100644 --- a/src/gui/folderman.h +++ b/src/gui/folderman.h @@ -30,6 +30,7 @@ class TestCfApiShellExtensionsIPC; class TestShareModel; class ShareTestHelper; class EndToEndTestHelper; +class TestSyncConflictsModel; namespace OCC { @@ -391,6 +392,7 @@ private slots: explicit FolderMan(QObject *parent = nullptr); friend class OCC::Application; friend class ::TestFolderMan; + friend class ::TestSyncConflictsModel; friend class ::TestCfApiShellExtensionsIPC; friend class ::ShareTestHelper; friend class ::EndToEndTestHelper; diff --git a/src/gui/owncloudgui.cpp b/src/gui/owncloudgui.cpp index a43363879ef23..20d15b9f42e18 100644 --- a/src/gui/owncloudgui.cpp +++ b/src/gui/owncloudgui.cpp @@ -31,6 +31,7 @@ #include "settingsdialog.h" #include "theme.h" #include "wheelhandler.h" +#include "syncconflictsmodel.h" #include "filedetails/filedetails.h" #include "filedetails/shareemodel.h" #include "filedetails/sharemodel.h" @@ -125,6 +126,7 @@ ownCloudGui::ownCloudGui(Application *parent) qmlRegisterType("com.nextcloud.desktopclient", 1, 0, "ShareModel"); qmlRegisterType("com.nextcloud.desktopclient", 1, 0, "ShareeModel"); qmlRegisterType("com.nextcloud.desktopclient", 1, 0, "SortedShareModel"); + qmlRegisterType("com.nextcloud.desktopclient", 1, 0, "SyncConflictsModel"); qmlRegisterUncreatableType("com.nextcloud.desktopclient", 1, 0, "UnifiedSearchResultsListModel", "UnifiedSearchResultsListModel"); qmlRegisterUncreatableType("com.nextcloud.desktopclient", 1, 0, "UserStatus", "Access to Status enum"); diff --git a/src/gui/syncconflictsmodel.cpp b/src/gui/syncconflictsmodel.cpp new file mode 100644 index 0000000000000..872619b7ab635 --- /dev/null +++ b/src/gui/syncconflictsmodel.cpp @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2023 by Matthieu Gallien + * + * 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. + */ + +#include "syncconflictsmodel.h" +#include "folderman.h" + +#include + +namespace OCC { + +Q_LOGGING_CATEGORY(lcSyncConflictsModel, "nextcloud.syncconflictsmodel", QtInfoMsg) + +SyncConflictsModel::SyncConflictsModel(QObject *parent) + : QAbstractListModel(parent) +{ +} + +int SyncConflictsModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) { + return 0; + } + + return mData.size(); +} + +QVariant SyncConflictsModel::data(const QModelIndex &index, int role) const +{ + auto result = QVariant{}; + + Q_ASSERT(checkIndex(index, CheckIndexOption::IndexIsValid | CheckIndexOption::ParentIsInvalid)); + + if (index.parent().isValid()) { + return result; + } + + if (role >= static_cast(SyncConflictRoles::ExistingFileName) && role <= static_cast(SyncConflictRoles::ConflictPreviewUrl)) { + auto convertedRole = static_cast(role); + + switch (convertedRole) { + case SyncConflictRoles::ExistingFileName: + result = mConflictData[index.row()].mExistingFileName; + break; + case SyncConflictRoles::ExistingSize: + result = mConflictData[index.row()].mExistingSize; + break; + case SyncConflictRoles::ConflictSize: + result = mConflictData[index.row()].mConflictSize; + break; + case SyncConflictRoles::ExistingDate: + result = mConflictData[index.row()].mExistingDate; + break; + case SyncConflictRoles::ConflictDate: + result = mConflictData[index.row()].mConflictDate; + break; + case SyncConflictRoles::ExistingSelected: + result = mConflictData[index.row()].mExistingSelected; + break; + case SyncConflictRoles::ConflictSelected: + result = mConflictData[index.row()].mConflictSelected; + break; + case SyncConflictRoles::ExistingPreviewUrl: + result = mConflictData[index.row()].mExistingPreviewUrl; + break; + case SyncConflictRoles::ConflictPreviewUrl: + result = mConflictData[index.row()].mConflictPreviewUrl; + break; + } + } + + return result; +} + +QHash SyncConflictsModel::roleNames() const +{ + auto result = QAbstractListModel::roleNames(); + + result[static_cast(SyncConflictRoles::ExistingFileName)] = "existingFileName"; + result[static_cast(SyncConflictRoles::ExistingSize)] = "existingSize"; + result[static_cast(SyncConflictRoles::ConflictSize)] = "conflictSize"; + result[static_cast(SyncConflictRoles::ExistingDate)] = "existingDate"; + result[static_cast(SyncConflictRoles::ConflictDate)] = "conflictDate"; + result[static_cast(SyncConflictRoles::ExistingSelected)] = "existingSelected"; + result[static_cast(SyncConflictRoles::ConflictSelected)] = "conflictSelected"; + result[static_cast(SyncConflictRoles::ExistingPreviewUrl)] = "existingPreviewUrl"; + result[static_cast(SyncConflictRoles::ConflictPreviewUrl)] = "conflictPreviewUrl"; + + return result; +} + +ActivityList SyncConflictsModel::conflictActivities() const +{ + return mData; +} + +void SyncConflictsModel::setConflictActivities(ActivityList conflicts) +{ + if (mData == conflicts) { + return; + } + + beginResetModel(); + + mData = conflicts; + emit conflictActivitiesChanged(); + + updateConflictsData(); + + endResetModel(); +} + +void SyncConflictsModel::updateConflictsData() +{ + mConflictData.clear(); + mConflictData.reserve(mData.size()); + + for (const auto &oneConflict : qAsConst(mData)) { + if (!FolderMan::instance()) { + qCWarning(lcSyncConflictsModel) << "no FolderMan instance"; + mConflictData.push_back({}); + continue; + } + const auto folder = FolderMan::instance()->folder(oneConflict._folder); + if (!folder) { + qCWarning(lcSyncConflictsModel) << "no Folder instance for" << oneConflict._folder; + mConflictData.push_back({}); + continue; + } + + const auto conflictedRelativePath = oneConflict._file; + const auto dbRecord = folder->journalDb(); + const auto baseRelativePath = dbRecord ? dbRecord->conflictFileBaseName(conflictedRelativePath.toUtf8()) : QString{}; + + const auto dir = QDir(folder->path()); + const auto conflictedPath = dir.filePath(conflictedRelativePath); + const auto basePath = dir.filePath(baseRelativePath); + + qCInfo(lcSyncConflictsModel()) << "conflictedPath" << conflictedPath << "basePath" << basePath; + + const auto existingFileInfo = QFileInfo(basePath); + const auto conflictFileInfo = QFileInfo(conflictedPath); + + const auto existingMimeType = mMimeDb.mimeTypeForFile(existingFileInfo.fileName()); + const auto conflictMimeType = mMimeDb.mimeTypeForFile(conflictFileInfo.fileName()); + + auto newConflictData = ConflictInfo{ + existingFileInfo.fileName(), + mLocale.formattedDataSize(existingFileInfo.size()), + mLocale.formattedDataSize(conflictFileInfo.size()), + existingFileInfo.lastModified().toString(), + conflictFileInfo.lastModified().toString(), + QIcon::hasThemeIcon(existingMimeType.iconName()) ? QUrl{} : QUrl{":/qt-project.org/styles/commonstyle/images/file-128.png"}, + QIcon::hasThemeIcon(conflictMimeType.iconName()) ? QUrl{} : QUrl{":/qt-project.org/styles/commonstyle/images/file-128.png"}, + false, + false, + }; + + mConflictData.push_back(std::move(newConflictData)); + } +} + +} diff --git a/src/gui/syncconflictsmodel.h b/src/gui/syncconflictsmodel.h new file mode 100644 index 0000000000000..d7ca61961d97d --- /dev/null +++ b/src/gui/syncconflictsmodel.h @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2023 by Matthieu Gallien + * + * 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. + */ + +#ifndef SYNCCONFLICTSMODEL_H +#define SYNCCONFLICTSMODEL_H + +#include "tray/activitydata.h" + +#include +#include +#include + +namespace OCC { + +class SyncConflictsModel : public QAbstractListModel +{ + Q_OBJECT + + Q_PROPERTY(OCC::ActivityList conflictActivities READ conflictActivities WRITE setConflictActivities NOTIFY conflictActivitiesChanged) + + struct ConflictInfo { + QString mExistingFileName; + QString mExistingSize; + QString mConflictSize; + QString mExistingDate; + QString mConflictDate; + QUrl mExistingPreviewUrl; + QUrl mConflictPreviewUrl; + bool mExistingSelected = false; + bool mConflictSelected = false; + }; + +public: + enum class SyncConflictRoles : int { + ExistingFileName = Qt::UserRole, + ExistingSize, + ConflictSize, + ExistingDate, + ConflictDate, + ExistingSelected, + ConflictSelected, + ExistingPreviewUrl, + ConflictPreviewUrl, + }; + + Q_ENUM(SyncConflictRoles) + + explicit SyncConflictsModel(QObject *parent = nullptr); + + [[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override; + + [[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + [[nodiscard]] QHash roleNames() const override; + + [[nodiscard]] OCC::ActivityList conflictActivities() const; + +public slots: + void setConflictActivities(OCC::ActivityList conflicts); + +signals: + void conflictActivitiesChanged(); + +private: + void updateConflictsData(); + + OCC::ActivityList mData; + + QVector mConflictData; + + QMimeDatabase mMimeDb; + + QLocale mLocale; +}; + +} + +#endif // SYNCCONFLICTSMODEL_H diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index d6c27a1304941..2dbb5a9b7f134 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -71,6 +71,7 @@ nextcloud_add_test(ShareeModel) nextcloud_add_test(SortedShareModel) nextcloud_add_test(SecureFileDrop) nextcloud_add_test(FileTagModel) +nextcloud_add_test(SyncConflictsModel) target_link_libraries(SecureFileDropTest PRIVATE Nextcloud::sync) configure_file(fake2eelocksucceeded.json "${PROJECT_BINARY_DIR}/bin/fake2eelocksucceeded.json" COPYONLY) diff --git a/test/testsyncconflictsmodel.cpp b/test/testsyncconflictsmodel.cpp new file mode 100644 index 0000000000000..a235562b46fe4 --- /dev/null +++ b/test/testsyncconflictsmodel.cpp @@ -0,0 +1,116 @@ +/* + * Copyright (C) by Claudio Cambra + * + * 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. + */ + +#include "gui/syncconflictsmodel.h" +#include "folderman.h" +#include "accountstate.h" +#include "configfile.h" +#include "syncfileitem.h" + +#include "syncenginetestutils.h" +#include "testhelper.h" + +#include +#include +#include + +namespace { + +QStringList findConflicts(const FileInfo &dir) +{ + QStringList conflicts; + for (const auto &item : dir.children) { + if (item.name.contains("(conflicted copy")) { + conflicts.append(item.path()); + } + } + return conflicts; +} + +} + +using namespace OCC; + +class TestSyncConflictsModel : public QObject +{ + Q_OBJECT + +private: + +private slots: + void initTestCase() + { + } + + void testSuccessfulFetchShares() + { + auto dir = QTemporaryDir {}; + ConfigFile::setConfDir(dir.path()); // we don't want to pollute the user's config file + + FolderMan fm; + + auto account = Account::create(); + auto url = QUrl{"http://example.de"}; + auto cred = new HttpCredentialsTest("testuser", "secret"); + account->setCredentials(cred); + account->setUrl(url); + url.setUserName(cred->user()); + + auto newAccountState{AccountStatePtr{ new AccountState{account}}}; + auto folderman = FolderMan::instance(); + QCOMPARE(folderman, &fm); + + auto fakeFolder = FakeFolder{FileInfo::A12_B12_C12_S12()}; + + QVERIFY(folderman->addFolder(newAccountState.data(), folderDefinition(fakeFolder.localPath()))); + + QVERIFY(fakeFolder.syncOnce()); + + fakeFolder.localModifier().appendByte("A/a2"); + fakeFolder.remoteModifier().appendByte("A/a2"); + fakeFolder.remoteModifier().appendByte("A/a2"); + + QVERIFY(fakeFolder.syncOnce()); + + OCC::ActivityList allConflicts; + + const auto conflicts = findConflicts(fakeFolder.currentLocalState().children["A"]); + for (const auto &conflict : conflicts) { + auto conflictActivity = OCC::Activity{}; + conflictActivity._file = fakeFolder.localPath() + conflict; + conflictActivity._folder = fakeFolder.localPath(); + allConflicts.push_back(std::move(conflictActivity)); + } + + SyncConflictsModel model; + QAbstractItemModelTester modelTester(&model); + + model.setConflictActivities(allConflicts); + + QCOMPARE(model.rowCount(), 1); + QCOMPARE(model.data(model.index(0), static_cast(SyncConflictsModel::SyncConflictRoles::ExistingFileName)), QString{"a2"}); + QCOMPARE(model.data(model.index(0), static_cast(SyncConflictsModel::SyncConflictRoles::ExistingSize)), QString{"6 bytes"}); + QCOMPARE(model.data(model.index(0), static_cast(SyncConflictsModel::SyncConflictRoles::ConflictSize)), QString{"5 bytes"}); + QVERIFY(!model.data(model.index(0), static_cast(SyncConflictsModel::SyncConflictRoles::ExistingDate)).toString().isEmpty()); + QVERIFY(!model.data(model.index(0), static_cast(SyncConflictsModel::SyncConflictRoles::ConflictDate)).toString().isEmpty()); + QCOMPARE(model.data(model.index(0), static_cast(SyncConflictsModel::SyncConflictRoles::ExistingPreviewUrl)), QUrl{":/qt-project.org/styles/commonstyle/images/file-128.png"}); + QCOMPARE(model.data(model.index(0), static_cast(SyncConflictsModel::SyncConflictRoles::ConflictPreviewUrl)), QUrl{":/qt-project.org/styles/commonstyle/images/file-128.png"}); + QCOMPARE(model.data(model.index(0), static_cast(SyncConflictsModel::SyncConflictRoles::ExistingSelected)), false); + QCOMPARE(model.data(model.index(0), static_cast(SyncConflictsModel::SyncConflictRoles::ConflictSelected)), false); + } + +}; + +QTEST_GUILESS_MAIN(TestSyncConflictsModel) +#include "testsyncconflictsmodel.moc" From 72b78c90112d8d40827fd88480a6953f2078e6d6 Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Wed, 3 May 2023 18:09:12 +0200 Subject: [PATCH 07/28] replace "Sync now" button by "Solve all conflicts" for many conflicts offer an easy way to open a dialog to solve all conflicts at once replacing the "Sync now" button Signed-off-by: Matthieu Gallien --- src/gui/ResolveConflictsDialog.qml | 45 ++++-------------------------- src/gui/systray.cpp | 6 ++-- src/gui/systray.h | 2 +- src/gui/tray/SyncStatus.qml | 26 ++++++++++++++++- src/gui/tray/activitylistmodel.cpp | 21 ++++++++++++++ src/gui/tray/activitylistmodel.h | 6 ++++ 6 files changed, 62 insertions(+), 44 deletions(-) diff --git a/src/gui/ResolveConflictsDialog.qml b/src/gui/ResolveConflictsDialog.qml index 5234b8a8a79b0..38adcea7138bd 100644 --- a/src/gui/ResolveConflictsDialog.qml +++ b/src/gui/ResolveConflictsDialog.qml @@ -25,6 +25,8 @@ import "./tray" Window { id: root + required property var allConflicts + flags: Qt.Dialog visible: true @@ -112,6 +114,8 @@ Window { SyncConflictsModel { id: realModel + + conflictActivities: root.allConflicts } ScrollView { @@ -125,46 +129,7 @@ Window { id: conflictListView model: DelegateModel { - model: ListModel { - ListElement { - existingFileName: 'Text File.txt' - conflictFileName: 'Text File.txt' - existingSize: '2 B' - conflictSize: '15 B' - existingDate: '28 avril 2023 09:53' - conflictDate: '28 avril 2023 09:53' - existingSelected: false - conflictSelected: false - existingPreviewUrl: 'https://nextcloud.local/index.php/apps/theming/img/core/filetypes/text.svg?v=b9feb2d6' - conflictPreviewUrl: 'https://nextcloud.local/index.php/apps/theming/img/core/filetypes/text.svg?v=b9feb2d6' - } - - ListElement { - existingFileName: 'Text File.txt' - conflictFileName: 'Text File.txt' - existingSize: '2 B' - conflictSize: '15 B' - existingDate: '28 avril 2023 09:53' - conflictDate: '28 avril 2023 09:53' - existingSelected: false - conflictSelected: false - existingPreviewUrl: 'https://nextcloud.local/index.php/apps/theming/img/core/filetypes/text.svg?v=b9feb2d6' - conflictPreviewUrl: 'https://nextcloud.local/index.php/apps/theming/img/core/filetypes/text.svg?v=b9feb2d6' - } - - ListElement { - existingFileName: 'Text File.txt' - conflictFileName: 'Text File.txt' - existingSize: '2 B' - conflictSize: '15 B' - existingDate: '28 avril 2023 09:53' - conflictDate: '28 avril 2023 09:53' - existingSelected: false - conflictSelected: false - existingPreviewUrl: 'https://nextcloud.local/index.php/apps/theming/img/core/filetypes/text.svg?v=b9feb2d6' - conflictPreviewUrl: 'https://nextcloud.local/index.php/apps/theming/img/core/filetypes/text.svg?v=b9feb2d6' - } - } + model: realModel delegate: ConflictDelegate { width: conflictListView.contentItem.width diff --git a/src/gui/systray.cpp b/src/gui/systray.cpp index f2e1d12d07749..25276b84f1074 100644 --- a/src/gui/systray.cpp +++ b/src/gui/systray.cpp @@ -285,10 +285,12 @@ void Systray::destroyEditFileLocallyLoadingDialog() _editFileLocallyLoadingDialog = nullptr; } -void Systray::createResolveConflictsDialog() +void Systray::createResolveConflictsDialog(const OCC::ActivityList &allConflicts) { const auto callDialog = new QQmlComponent(_trayEngine, QStringLiteral("qrc:/qml/src/gui/ResolveConflictsDialog.qml")); - const QVariantMap initialProperties{}; + const QVariantMap initialProperties{ + {"allConflicts", QVariant::fromValue(allConflicts)}, + }; if(callDialog->isError()) { qCWarning(lcSystray) << callDialog->errorString(); diff --git a/src/gui/systray.h b/src/gui/systray.h index 69c34a65f4adf..21607233e6ed3 100644 --- a/src/gui/systray.h +++ b/src/gui/systray.h @@ -121,7 +121,7 @@ public slots: void createCallDialog(const OCC::Activity &callNotification, const OCC::AccountStatePtr accountState); void createEditFileLocallyLoadingDialog(const QString &fileName); void destroyEditFileLocallyLoadingDialog(); - void createResolveConflictsDialog(); + void createResolveConflictsDialog(const OCC::ActivityList &allConflicts); void slotCurrentUserChanged(); diff --git a/src/gui/tray/SyncStatus.qml b/src/gui/tray/SyncStatus.qml index a1dc370c8555a..8fa91b454323f 100644 --- a/src/gui/tray/SyncStatus.qml +++ b/src/gui/tray/SyncStatus.qml @@ -121,7 +121,8 @@ RowLayout { contentsFont.bold: true bgColor: Style.currentUserHeaderColor - visible: !syncStatus.syncing && + visible: !activityModel.hasManySyncConflicts && + !syncStatus.syncing && NC.UserModel.currentUser.hasLocalFolder && NC.UserModel.currentUser.isConnected enabled: visible @@ -131,4 +132,27 @@ RowLayout { } } } + + CustomButton { + Layout.preferredWidth: syncNowFm.boundingRect(text).width + + leftPadding + + rightPadding + + Style.standardSpacing * 2 + Layout.rightMargin: Style.trayHorizontalMargin + + text: qsTr("Solve all conflicts") + textColor: Style.adjustedCurrentUserHeaderColor + textColorHovered: Style.currentUserHeaderTextColor + contentsFont.bold: true + bgColor: Style.currentUserHeaderColor + + visible: activityModel.hasManySyncConflicts && + !syncStatus.syncing && + NC.UserModel.currentUser.hasLocalFolder && + NC.UserModel.currentUser.isConnected + enabled: visible + onClicked: { + Systray.createResolveConflictsDialog(); + } + } } diff --git a/src/gui/tray/activitylistmodel.cpp b/src/gui/tray/activitylistmodel.cpp index f3cb3838f3ee8..6d7ceec74aa73 100644 --- a/src/gui/tray/activitylistmodel.cpp +++ b/src/gui/tray/activitylistmodel.cpp @@ -548,6 +548,21 @@ void ActivityListModel::addEntriesToActivityList(const ActivityList &activityLis _finalList.append(activity); } endInsertRows(); + + auto conflictsCount = 0; + for(const auto &activity : _finalList) { + if (activity._syncFileItemStatus == SyncFileItem::Conflict) { + ++conflictsCount; + } + } + + if (!_hasManySyncConflicts && conflictsCount > 2) { + _hasManySyncConflicts = true; + emit hasManySyncConflictsChanged(); + } else if (_hasManySyncConflicts && conflictsCount <= 2) { + _hasManySyncConflicts = false; + emit hasManySyncConflictsChanged(); + } } void ActivityListModel::addErrorToActivityList(const Activity &activity) @@ -893,4 +908,10 @@ QString ActivityListModel::replyMessageSent(const Activity &activity) const { return activity._talkNotificationData.messageSent; } + +bool ActivityListModel::hasManySyncConflicts() const +{ + return _hasManySyncConflicts; +} + } diff --git a/src/gui/tray/activitylistmodel.h b/src/gui/tray/activitylistmodel.h index eae46ea78b587..17e22eaeadbd9 100644 --- a/src/gui/tray/activitylistmodel.h +++ b/src/gui/tray/activitylistmodel.h @@ -42,6 +42,7 @@ class ActivityListModel : public QAbstractListModel Q_OBJECT Q_PROPERTY(quint32 maxActionButtons READ maxActionButtons CONSTANT) Q_PROPERTY(AccountState *accountState READ accountState WRITE setAccountState NOTIFY accountStateChanged) + Q_PROPERTY(bool hasManySyncConflicts READ hasManySyncConflicts NOTIFY hasManySyncConflictsChanged) public: enum DataRole { @@ -105,6 +106,8 @@ class ActivityListModel : public QAbstractListModel [[nodiscard]] QString replyMessageSent(const Activity &activity) const; + [[nodiscard]] bool hasManySyncConflicts() const; + public slots: void slotRefreshActivity(); void slotRefreshActivityInitial(); @@ -126,6 +129,7 @@ public slots: signals: void accountStateChanged(); + void hasManySyncConflictsChanged(); void activityJobStatusCode(int statusCode); void sendNotificationRequest(const QString &accountName, const QString &link, const QByteArray &verb, int row); @@ -192,6 +196,8 @@ private slots: bool _doneFetching = false; bool _hideOldActivities = true; + bool _hasManySyncConflicts = false; + static constexpr quint32 MaxActionButtons = 3; }; } From c118958d3d4363f18abe0f1fc7639022bbbe3f25 Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Wed, 3 May 2023 18:28:55 +0200 Subject: [PATCH 08/28] do not show many conflicts dialog for a single conflict Signed-off-by: Matthieu Gallien --- src/gui/tray/activitylistmodel.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/gui/tray/activitylistmodel.cpp b/src/gui/tray/activitylistmodel.cpp index 6d7ceec74aa73..3630bccad5814 100644 --- a/src/gui/tray/activitylistmodel.cpp +++ b/src/gui/tray/activitylistmodel.cpp @@ -749,8 +749,6 @@ void ActivityListModel::displaySingleConflictDialog(const Activity &activity) }); _currentConflictDialog->open(); ownCloudGui::raiseDialog(_currentConflictDialog); - - Systray::instance()->createResolveConflictsDialog(); } void ActivityListModel::slotTriggerAction(const int activityIndex, const int actionIndex) From f799d8c697964023c3643ac95eb44ce595981ba6 Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Wed, 3 May 2023 18:33:19 +0200 Subject: [PATCH 09/28] tune the pipes to really get a dialog to show for many conflicts Signed-off-by: Matthieu Gallien --- src/gui/owncloudgui.cpp | 1 + src/gui/tray/SyncStatus.qml | 2 +- src/gui/tray/activitydata.h | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/gui/owncloudgui.cpp b/src/gui/owncloudgui.cpp index 20d15b9f42e18..45215d53ee333 100644 --- a/src/gui/owncloudgui.cpp +++ b/src/gui/owncloudgui.cpp @@ -140,6 +140,7 @@ ownCloudGui::ownCloudGui(Application *parent) qRegisterMetaType("SharePtr"); qRegisterMetaType("ShareePtr"); qRegisterMetaType("Sharee"); + qRegisterMetaType("ActivityList"); qmlRegisterSingletonInstance("com.nextcloud.desktopclient", 1, 0, "UserModel", UserModel::instance()); qmlRegisterSingletonInstance("com.nextcloud.desktopclient", 1, 0, "UserAppsModel", UserAppsModel::instance()); diff --git a/src/gui/tray/SyncStatus.qml b/src/gui/tray/SyncStatus.qml index 8fa91b454323f..b973361c909d1 100644 --- a/src/gui/tray/SyncStatus.qml +++ b/src/gui/tray/SyncStatus.qml @@ -152,7 +152,7 @@ RowLayout { NC.UserModel.currentUser.isConnected enabled: visible onClicked: { - Systray.createResolveConflictsDialog(); + NC.Systray.createResolveConflictsDialog(); } } } diff --git a/src/gui/tray/activitydata.h b/src/gui/tray/activitydata.h index c4e8947240a70..36671e4739679 100644 --- a/src/gui/tray/activitydata.h +++ b/src/gui/tray/activitydata.h @@ -178,6 +178,7 @@ using ActivityList = QList; } Q_DECLARE_METATYPE(OCC::Activity) +Q_DECLARE_METATYPE(OCC::ActivityList) Q_DECLARE_METATYPE(OCC::Activity::Type) Q_DECLARE_METATYPE(OCC::ActivityLink) Q_DECLARE_METATYPE(OCC::PreviewData) From 789ca7230a522e6b7cf4cc38bf6846286c4a3cb6 Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Wed, 3 May 2023 18:40:15 +0200 Subject: [PATCH 10/28] plug models into the dialog Signed-off-by: Matthieu Gallien --- src/gui/ConflictDelegate.qml | 1 - src/gui/ResolveConflictsDialog.qml | 4 +++- src/gui/tray/SyncStatus.qml | 2 +- src/gui/tray/activitylistmodel.cpp | 15 +++++++++++++++ src/gui/tray/activitylistmodel.h | 4 ++++ 5 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/gui/ConflictDelegate.qml b/src/gui/ConflictDelegate.qml index 7d386404bac52..66adb571d4ede 100644 --- a/src/gui/ConflictDelegate.qml +++ b/src/gui/ConflictDelegate.qml @@ -24,7 +24,6 @@ Item { id: root required property string existingFileName - required property string conflictFileName required property string existingSize required property string conflictSize required property string existingDate diff --git a/src/gui/ResolveConflictsDialog.qml b/src/gui/ResolveConflictsDialog.qml index 38adcea7138bd..4228c75145d5d 100644 --- a/src/gui/ResolveConflictsDialog.qml +++ b/src/gui/ResolveConflictsDialog.qml @@ -59,7 +59,7 @@ Window { z: 2 EnforcedPlainTextLabel { - text: qsTr("%1 files in conflict").arg(12) + text: qsTr("%1 files in conflict").arg(delegateModel.count) font.bold: true font.pixelSize: 20 Layout.fillWidth: true @@ -129,6 +129,8 @@ Window { id: conflictListView model: DelegateModel { + id: delegateModel + model: realModel delegate: ConflictDelegate { diff --git a/src/gui/tray/SyncStatus.qml b/src/gui/tray/SyncStatus.qml index b973361c909d1..799d4d3e09109 100644 --- a/src/gui/tray/SyncStatus.qml +++ b/src/gui/tray/SyncStatus.qml @@ -152,7 +152,7 @@ RowLayout { NC.UserModel.currentUser.isConnected enabled: visible onClicked: { - NC.Systray.createResolveConflictsDialog(); + NC.Systray.createResolveConflictsDialog(activityModel.allConflicts); } } } diff --git a/src/gui/tray/activitylistmodel.cpp b/src/gui/tray/activitylistmodel.cpp index 3630bccad5814..507f4da42f275 100644 --- a/src/gui/tray/activitylistmodel.cpp +++ b/src/gui/tray/activitylistmodel.cpp @@ -428,6 +428,8 @@ void ActivityListModel::startFetchJob() void ActivityListModel::setFinalList(const ActivityList &finalList) { _finalList = finalList; + + emit allConflictsChanged(); } const ActivityList &ActivityListModel::finalList() const @@ -912,4 +914,17 @@ bool ActivityListModel::hasManySyncConflicts() const return _hasManySyncConflicts; } +ActivityList ActivityListModel::allConflicts() const +{ + auto result = ActivityList{}; + + for(const auto &activity : _finalList) { + if (activity._syncFileItemStatus == SyncFileItem::Conflict) { + result.push_back(activity); + } + } + + return result; +} + } diff --git a/src/gui/tray/activitylistmodel.h b/src/gui/tray/activitylistmodel.h index 17e22eaeadbd9..b6864d81c582b 100644 --- a/src/gui/tray/activitylistmodel.h +++ b/src/gui/tray/activitylistmodel.h @@ -43,6 +43,7 @@ class ActivityListModel : public QAbstractListModel Q_PROPERTY(quint32 maxActionButtons READ maxActionButtons CONSTANT) Q_PROPERTY(AccountState *accountState READ accountState WRITE setAccountState NOTIFY accountStateChanged) Q_PROPERTY(bool hasManySyncConflicts READ hasManySyncConflicts NOTIFY hasManySyncConflictsChanged) + Q_PROPERTY(OCC::ActivityList allConflicts READ allConflicts NOTIFY allConflictsChanged) public: enum DataRole { @@ -108,6 +109,8 @@ class ActivityListModel : public QAbstractListModel [[nodiscard]] bool hasManySyncConflicts() const; + [[nodiscard]] OCC::ActivityList allConflicts() const; + public slots: void slotRefreshActivity(); void slotRefreshActivityInitial(); @@ -130,6 +133,7 @@ public slots: signals: void accountStateChanged(); void hasManySyncConflictsChanged(); + void allConflictsChanged(); void activityJobStatusCode(int statusCode); void sendNotificationRequest(const QString &accountName, const QString &link, const QByteArray &verb, int row); From 779cc3a75ba37f24efe54297ac80b1e7458f14e9 Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Wed, 3 May 2023 19:17:11 +0200 Subject: [PATCH 11/28] display icons for files when solving many conflicts Signed-off-by: Matthieu Gallien --- src/gui/syncconflictsmodel.cpp | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/gui/syncconflictsmodel.cpp b/src/gui/syncconflictsmodel.cpp index 872619b7ab635..613a0bbbc3ba2 100644 --- a/src/gui/syncconflictsmodel.cpp +++ b/src/gui/syncconflictsmodel.cpp @@ -151,17 +151,14 @@ void SyncConflictsModel::updateConflictsData() const auto existingFileInfo = QFileInfo(basePath); const auto conflictFileInfo = QFileInfo(conflictedPath); - const auto existingMimeType = mMimeDb.mimeTypeForFile(existingFileInfo.fileName()); - const auto conflictMimeType = mMimeDb.mimeTypeForFile(conflictFileInfo.fileName()); - auto newConflictData = ConflictInfo{ existingFileInfo.fileName(), mLocale.formattedDataSize(existingFileInfo.size()), mLocale.formattedDataSize(conflictFileInfo.size()), existingFileInfo.lastModified().toString(), conflictFileInfo.lastModified().toString(), - QIcon::hasThemeIcon(existingMimeType.iconName()) ? QUrl{} : QUrl{":/qt-project.org/styles/commonstyle/images/file-128.png"}, - QIcon::hasThemeIcon(conflictMimeType.iconName()) ? QUrl{} : QUrl{":/qt-project.org/styles/commonstyle/images/file-128.png"}, + QUrl{QStringLiteral("image://tray-image-provider/:/fileicon") + existingFileInfo.fileName()}, + QUrl{QStringLiteral("image://tray-image-provider/:/fileicon") + conflictFileInfo.fileName()}, false, false, }; From 6efa9f81beaba1e7dc7d78402b25ff02af546a1d Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Thu, 4 May 2023 10:32:56 +0200 Subject: [PATCH 12/28] prevent close of conflicts dialog on focus lost Signed-off-by: Matthieu Gallien --- src/gui/ResolveConflictsDialog.qml | 22 +++++++--------------- src/gui/systray.cpp | 27 +++++++++++++++++++++++---- 2 files changed, 30 insertions(+), 19 deletions(-) diff --git a/src/gui/ResolveConflictsDialog.qml b/src/gui/ResolveConflictsDialog.qml index 4228c75145d5d..bf6422fe6a292 100644 --- a/src/gui/ResolveConflictsDialog.qml +++ b/src/gui/ResolveConflictsDialog.qml @@ -14,7 +14,7 @@ import QtQml 2.15 import QtQuick 2.15 -import QtQuick.Window 2.15 +import QtQuick.Window 2.15 as QtWindow import QtQuick.Layouts 1.15 import QtQuick.Controls 2.15 import QtQml.Models 2.15 @@ -22,12 +22,12 @@ import Style 1.0 import com.nextcloud.desktopclient 1.0 import "./tray" -Window { - id: root +QtWindow.Window { + id: conflictsDialog required property var allConflicts - flags: Qt.Dialog + flags: Qt.Window | Qt.Dialog visible: true width: 600 @@ -36,17 +36,9 @@ Window { minimumHeight: 800 title: qsTr('Solve sync conflicts') - onClosing: function() { + onClosing: function(close) { Systray.destroyDialog(root); - } - - Component.onCompleted: { - Systray.forceWindowInit(root); - Systray.positionNotificationWindow(root); - - root.show(); - root.raise(); - root.requestActivate(); + close.accepted = true } ColumnLayout { @@ -115,7 +107,7 @@ Window { SyncConflictsModel { id: realModel - conflictActivities: root.allConflicts + conflictActivities: conflictsDialog.allConflicts } ScrollView { diff --git a/src/gui/systray.cpp b/src/gui/systray.cpp index 25276b84f1074..4e14c2b5d2523 100644 --- a/src/gui/systray.cpp +++ b/src/gui/systray.cpp @@ -287,19 +287,38 @@ void Systray::destroyEditFileLocallyLoadingDialog() void Systray::createResolveConflictsDialog(const OCC::ActivityList &allConflicts) { - const auto callDialog = new QQmlComponent(_trayEngine, QStringLiteral("qrc:/qml/src/gui/ResolveConflictsDialog.qml")); + const auto conflictsDialog = new QQmlComponent(_trayEngine, QStringLiteral("qrc:/qml/src/gui/ResolveConflictsDialog.qml")); const QVariantMap initialProperties{ {"allConflicts", QVariant::fromValue(allConflicts)}, }; - if(callDialog->isError()) { - qCWarning(lcSystray) << callDialog->errorString(); + if(conflictsDialog->isError()) { + qCWarning(lcSystray) << conflictsDialog->errorString(); + delete conflictsDialog; return; } // This call dialog gets deallocated on close conditions // by a call from the QML side to the destroyDialog slot - callDialog->createWithInitialProperties(initialProperties); + auto dialog = conflictsDialog->createWithInitialProperties(initialProperties); + if (!dialog) { + delete dialog; + delete conflictsDialog; + return; + } + dialog->setParent(QGuiApplication::instance()); + + auto dialogWindow = qobject_cast(dialog); + if (!dialogWindow) { + delete dialog; + delete conflictsDialog; + return; + } + dialogWindow->show(); + dialogWindow->raise(); + dialogWindow->requestActivate(); + + delete conflictsDialog; } bool Systray::raiseDialogs() From 2e501f0c75a082b93e0c611a137eac2fd0359d28 Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Thu, 4 May 2023 10:47:45 +0200 Subject: [PATCH 13/28] add some margins at bottom of items in conflicts list view Signed-off-by: Matthieu Gallien --- src/gui/ConflictDelegate.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/src/gui/ConflictDelegate.qml b/src/gui/ConflictDelegate.qml index 66adb571d4ede..a8cbe9dd5cbe4 100644 --- a/src/gui/ConflictDelegate.qml +++ b/src/gui/ConflictDelegate.qml @@ -50,6 +50,7 @@ Item { anchors.bottom: parent.bottom anchors.left: parent.left anchors.right: parent.right + anchors.bottomMargin: 8 Item { Layout.fillWidth: true From 30bba4c90600425d2d14e7562889f97e9d713b2b Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Thu, 4 May 2023 12:29:41 +0200 Subject: [PATCH 14/28] fix automated tests Signed-off-by: Matthieu Gallien --- test/testsyncconflictsmodel.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/testsyncconflictsmodel.cpp b/test/testsyncconflictsmodel.cpp index a235562b46fe4..0eb9f6d075e6a 100644 --- a/test/testsyncconflictsmodel.cpp +++ b/test/testsyncconflictsmodel.cpp @@ -104,8 +104,8 @@ private slots: QCOMPARE(model.data(model.index(0), static_cast(SyncConflictsModel::SyncConflictRoles::ConflictSize)), QString{"5 bytes"}); QVERIFY(!model.data(model.index(0), static_cast(SyncConflictsModel::SyncConflictRoles::ExistingDate)).toString().isEmpty()); QVERIFY(!model.data(model.index(0), static_cast(SyncConflictsModel::SyncConflictRoles::ConflictDate)).toString().isEmpty()); - QCOMPARE(model.data(model.index(0), static_cast(SyncConflictsModel::SyncConflictRoles::ExistingPreviewUrl)), QUrl{":/qt-project.org/styles/commonstyle/images/file-128.png"}); - QCOMPARE(model.data(model.index(0), static_cast(SyncConflictsModel::SyncConflictRoles::ConflictPreviewUrl)), QUrl{":/qt-project.org/styles/commonstyle/images/file-128.png"}); + QCOMPARE(model.data(model.index(0), static_cast(SyncConflictsModel::SyncConflictRoles::ExistingPreviewUrl)), QVariant::fromValue(QUrl{QStringLiteral("image://tray-image-provider/:/fileicon%1A/a2").arg(fakeFolder.localPath())})); + QCOMPARE(model.data(model.index(0), static_cast(SyncConflictsModel::SyncConflictRoles::ConflictPreviewUrl)), QVariant::fromValue(QUrl{QStringLiteral("image://tray-image-provider/:/fileicon%1%2").arg(fakeFolder.localPath(), conflicts.constFirst())})); QCOMPARE(model.data(model.index(0), static_cast(SyncConflictsModel::SyncConflictRoles::ExistingSelected)), false); QCOMPARE(model.data(model.index(0), static_cast(SyncConflictsModel::SyncConflictRoles::ConflictSelected)), false); } From 382a58c6bdb49cdd9468e91b9dded27e202b5eaa Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Thu, 4 May 2023 12:29:54 +0200 Subject: [PATCH 15/28] allow the conflicts dialog to close Signed-off-by: Matthieu Gallien --- src/gui/ResolveConflictsDialog.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gui/ResolveConflictsDialog.qml b/src/gui/ResolveConflictsDialog.qml index bf6422fe6a292..6f8241c14cad8 100644 --- a/src/gui/ResolveConflictsDialog.qml +++ b/src/gui/ResolveConflictsDialog.qml @@ -37,7 +37,7 @@ QtWindow.Window { title: qsTr('Solve sync conflicts') onClosing: function(close) { - Systray.destroyDialog(root); + Systray.destroyDialog(self); close.accepted = true } From fa135a4228910e809f797d81ee5a4be154b32c43 Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Thu, 4 May 2023 12:30:13 +0200 Subject: [PATCH 16/28] ensure the URL to mimetype icons are correct Signed-off-by: Matthieu Gallien --- src/gui/ResolveConflictsDialog.qml | 4 ++-- src/gui/syncconflictsmodel.cpp | 6 ++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/gui/ResolveConflictsDialog.qml b/src/gui/ResolveConflictsDialog.qml index 6f8241c14cad8..5e83d951a1fbc 100644 --- a/src/gui/ResolveConflictsDialog.qml +++ b/src/gui/ResolveConflictsDialog.qml @@ -147,12 +147,12 @@ QtWindow.Window { onAccepted: function() { console.log("Ok clicked") - Systray.destroyDialog(root) + Systray.destroyDialog(conflictsDialog) } onRejected: function() { console.log("Cancel clicked") - Systray.destroyDialog(root) + Systray.destroyDialog(conflictsDialog) } } } diff --git a/src/gui/syncconflictsmodel.cpp b/src/gui/syncconflictsmodel.cpp index 613a0bbbc3ba2..b03d35371970a 100644 --- a/src/gui/syncconflictsmodel.cpp +++ b/src/gui/syncconflictsmodel.cpp @@ -146,8 +146,6 @@ void SyncConflictsModel::updateConflictsData() const auto conflictedPath = dir.filePath(conflictedRelativePath); const auto basePath = dir.filePath(baseRelativePath); - qCInfo(lcSyncConflictsModel()) << "conflictedPath" << conflictedPath << "basePath" << basePath; - const auto existingFileInfo = QFileInfo(basePath); const auto conflictFileInfo = QFileInfo(conflictedPath); @@ -157,8 +155,8 @@ void SyncConflictsModel::updateConflictsData() mLocale.formattedDataSize(conflictFileInfo.size()), existingFileInfo.lastModified().toString(), conflictFileInfo.lastModified().toString(), - QUrl{QStringLiteral("image://tray-image-provider/:/fileicon") + existingFileInfo.fileName()}, - QUrl{QStringLiteral("image://tray-image-provider/:/fileicon") + conflictFileInfo.fileName()}, + QUrl{QStringLiteral("image://tray-image-provider/:/fileicon") + existingFileInfo.filePath()}, + QUrl{QStringLiteral("image://tray-image-provider/:/fileicon") + conflictFileInfo.filePath()}, false, false, }; From 8d52d4e4cc0377b935720bfbdfb20da4b7e0641a Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Thu, 4 May 2023 16:23:50 +0200 Subject: [PATCH 17/28] implement more feedback Signed-off-by: Matthieu Gallien --- src/gui/tray/SyncStatus.qml | 6 +++--- src/gui/tray/activitylistmodel.cpp | 25 ++++++++++++++----------- src/gui/tray/activitylistmodel.h | 9 +++++---- 3 files changed, 22 insertions(+), 18 deletions(-) diff --git a/src/gui/tray/SyncStatus.qml b/src/gui/tray/SyncStatus.qml index 799d4d3e09109..40c6553c6b0f3 100644 --- a/src/gui/tray/SyncStatus.qml +++ b/src/gui/tray/SyncStatus.qml @@ -121,7 +121,7 @@ RowLayout { contentsFont.bold: true bgColor: Style.currentUserHeaderColor - visible: !activityModel.hasManySyncConflicts && + visible: !activityModel.hasSyncConflicts && !syncStatus.syncing && NC.UserModel.currentUser.hasLocalFolder && NC.UserModel.currentUser.isConnected @@ -140,13 +140,13 @@ RowLayout { Style.standardSpacing * 2 Layout.rightMargin: Style.trayHorizontalMargin - text: qsTr("Solve all conflicts") + text: qsTr("Resolve conflicts") textColor: Style.adjustedCurrentUserHeaderColor textColorHovered: Style.currentUserHeaderTextColor contentsFont.bold: true bgColor: Style.currentUserHeaderColor - visible: activityModel.hasManySyncConflicts && + visible: activityModel.hasSyncConflicts && !syncStatus.syncing && NC.UserModel.currentUser.hasLocalFolder && NC.UserModel.currentUser.isConnected diff --git a/src/gui/tray/activitylistmodel.cpp b/src/gui/tray/activitylistmodel.cpp index 507f4da42f275..8adae711f620b 100644 --- a/src/gui/tray/activitylistmodel.cpp +++ b/src/gui/tray/activitylistmodel.cpp @@ -551,20 +551,15 @@ void ActivityListModel::addEntriesToActivityList(const ActivityList &activityLis } endInsertRows(); - auto conflictsCount = 0; + auto conflictsFound = false; for(const auto &activity : _finalList) { if (activity._syncFileItemStatus == SyncFileItem::Conflict) { - ++conflictsCount; + conflictsFound = true; + break; } } - if (!_hasManySyncConflicts && conflictsCount > 2) { - _hasManySyncConflicts = true; - emit hasManySyncConflictsChanged(); - } else if (_hasManySyncConflicts && conflictsCount <= 2) { - _hasManySyncConflicts = false; - emit hasManySyncConflictsChanged(); - } + setHasSyncConflicts(conflictsFound); } void ActivityListModel::addErrorToActivityList(const Activity &activity) @@ -753,6 +748,14 @@ void ActivityListModel::displaySingleConflictDialog(const Activity &activity) ownCloudGui::raiseDialog(_currentConflictDialog); } +void ActivityListModel::setHasSyncConflicts(bool conflictsFound) +{ + if (_hasSyncConflicts != conflictsFound) { + _hasSyncConflicts = conflictsFound; + emit hasSyncConflictsChanged(); + } +} + void ActivityListModel::slotTriggerAction(const int activityIndex, const int actionIndex) { if (activityIndex < 0 || activityIndex >= _finalList.size()) { @@ -909,9 +912,9 @@ QString ActivityListModel::replyMessageSent(const Activity &activity) const return activity._talkNotificationData.messageSent; } -bool ActivityListModel::hasManySyncConflicts() const +bool ActivityListModel::hasSyncConflicts() const { - return _hasManySyncConflicts; + return _hasSyncConflicts; } ActivityList ActivityListModel::allConflicts() const diff --git a/src/gui/tray/activitylistmodel.h b/src/gui/tray/activitylistmodel.h index b6864d81c582b..05e76bacefe50 100644 --- a/src/gui/tray/activitylistmodel.h +++ b/src/gui/tray/activitylistmodel.h @@ -42,7 +42,7 @@ class ActivityListModel : public QAbstractListModel Q_OBJECT Q_PROPERTY(quint32 maxActionButtons READ maxActionButtons CONSTANT) Q_PROPERTY(AccountState *accountState READ accountState WRITE setAccountState NOTIFY accountStateChanged) - Q_PROPERTY(bool hasManySyncConflicts READ hasManySyncConflicts NOTIFY hasManySyncConflictsChanged) + Q_PROPERTY(bool hasSyncConflicts READ hasSyncConflicts NOTIFY hasSyncConflictsChanged) Q_PROPERTY(OCC::ActivityList allConflicts READ allConflicts NOTIFY allConflictsChanged) public: @@ -107,7 +107,7 @@ class ActivityListModel : public QAbstractListModel [[nodiscard]] QString replyMessageSent(const Activity &activity) const; - [[nodiscard]] bool hasManySyncConflicts() const; + [[nodiscard]] bool hasSyncConflicts() const; [[nodiscard]] OCC::ActivityList allConflicts() const; @@ -132,7 +132,7 @@ public slots: signals: void accountStateChanged(); - void hasManySyncConflictsChanged(); + void hasSyncConflictsChanged(); void allConflictsChanged(); void activityJobStatusCode(int statusCode); @@ -171,6 +171,7 @@ private slots: void triggerCaseClashAction(Activity activity); void displaySingleConflictDialog(const Activity &activity); + void setHasSyncConflicts(bool conflictsFound); Activity _notificationIgnoredFiles; Activity _dummyFetchingActivities; @@ -200,7 +201,7 @@ private slots: bool _doneFetching = false; bool _hideOldActivities = true; - bool _hasManySyncConflicts = false; + bool _hasSyncConflicts = false; static constexpr quint32 MaxActionButtons = 3; }; From 739c56c04a28c909a058fc30c561e28820f6c848 Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Fri, 12 May 2023 13:17:38 +0200 Subject: [PATCH 18/28] allow selecting conflict resolutions for many conflicts Signed-off-by: Matthieu Gallien --- src/gui/ConflictDelegate.qml | 9 ++ src/gui/ResolveConflictsDialog.qml | 10 ++ src/gui/syncconflictsmodel.cpp | 151 +++++++++++++++++++++++++++++ src/gui/syncconflictsmodel.h | 32 ++++++ 4 files changed, 202 insertions(+) diff --git a/src/gui/ConflictDelegate.qml b/src/gui/ConflictDelegate.qml index a8cbe9dd5cbe4..eeebc3f5011c7 100644 --- a/src/gui/ConflictDelegate.qml +++ b/src/gui/ConflictDelegate.qml @@ -32,6 +32,7 @@ Item { required property bool conflictSelected required property url existingPreviewUrl required property url conflictPreviewUrl + required property var model EnforcedPlainTextLabel { id: existingFileNameLabel @@ -66,6 +67,10 @@ Item { spacing: 0 checked: root.existingSelected + + onToggled: function() { + model.existingSelected = checked + } } Image { @@ -139,6 +144,10 @@ Item { spacing: 0 checked: root.conflictSelected + + onToggled: function() { + model.conflictSelected = checked + } } Image { diff --git a/src/gui/ResolveConflictsDialog.qml b/src/gui/ResolveConflictsDialog.qml index 5e83d951a1fbc..d94541601147f 100644 --- a/src/gui/ResolveConflictsDialog.qml +++ b/src/gui/ResolveConflictsDialog.qml @@ -80,6 +80,11 @@ QtWindow.Window { implicitWidth: 100 font.pixelSize: 15 + + checked: realModel.allExistingsSelected + onToggled: function() { + realModel.selectAllExisting(checked) + } } CheckBox { @@ -93,6 +98,11 @@ QtWindow.Window { implicitWidth: 100 font.pixelSize: 15 + + checked: realModel.allConflictingSelected + onToggled: function() { + realModel.selectAllConflicting(checked) + } } } diff --git a/src/gui/syncconflictsmodel.cpp b/src/gui/syncconflictsmodel.cpp index b03d35371970a..cc9f4302b325c 100644 --- a/src/gui/syncconflictsmodel.cpp +++ b/src/gui/syncconflictsmodel.cpp @@ -82,6 +82,50 @@ QVariant SyncConflictsModel::data(const QModelIndex &index, int role) const return result; } +bool SyncConflictsModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + auto result = false; + + Q_ASSERT(checkIndex(index, CheckIndexOption::IndexIsValid | CheckIndexOption::ParentIsInvalid)); + + if (index.parent().isValid()) { + return result; + } + + if (role >= static_cast(SyncConflictRoles::ExistingFileName) && role <= static_cast(SyncConflictRoles::ConflictPreviewUrl)) { + auto convertedRole = static_cast(role); + + switch(convertedRole) { + case SyncConflictRoles::ExistingFileName: + break; + case SyncConflictRoles::ExistingSize: + break; + case SyncConflictRoles::ConflictSize: + break; + case SyncConflictRoles::ExistingDate: + break; + case SyncConflictRoles::ConflictDate: + break; + case SyncConflictRoles::ExistingSelected: + setExistingSelected(value.toBool(), index, role); + result = true; + break; + case SyncConflictRoles::ConflictSelected: + setConflictingSelected(value.toBool(), index, role); + result = true; + break; + case SyncConflictRoles::ExistingPreviewUrl: + break; + case SyncConflictRoles::ConflictPreviewUrl: + break; + } + + result = false; + } + + return result; +} + QHash SyncConflictsModel::roleNames() const { auto result = QAbstractListModel::roleNames(); @@ -99,11 +143,34 @@ QHash SyncConflictsModel::roleNames() const return result; } +Qt::ItemFlags SyncConflictsModel::flags(const QModelIndex &index) const +{ + auto result = Qt::ItemFlags{}; + + if (!index.parent().isValid()) { + result = QAbstractListModel::flags(index); + return result; + } + + result = Qt::ItemIsSelectable | Qt::ItemIsEditable; + return result; +} + ActivityList SyncConflictsModel::conflictActivities() const { return mData; } +bool SyncConflictsModel::allExistingsSelected() const +{ + return mAllExistingsSelected; +} + +bool SyncConflictsModel::allConflictingSelected() const +{ + return mAllConflictingsSelected; +} + void SyncConflictsModel::setConflictActivities(ActivityList conflicts) { if (mData == conflicts) { @@ -120,6 +187,40 @@ void SyncConflictsModel::setConflictActivities(ActivityList conflicts) endResetModel(); } +void SyncConflictsModel::selectAllExisting(bool selected) +{ + for (auto &singleConflict : mConflictData) { + singleConflict.mExistingSelected = selected; + } + + Q_EMIT dataChanged(index(0), index(rowCount() - 1), {static_cast(SyncConflictRoles::ExistingSelected)}); + + if (selected && !mAllExistingsSelected) { + mAllExistingsSelected = true; + Q_EMIT allExistingsSelectedChanged(); + } else if (!selected && mAllExistingsSelected) { + mAllExistingsSelected = false; + Q_EMIT allExistingsSelectedChanged(); + } +} + +void SyncConflictsModel::selectAllConflicting(bool selected) +{ + for (auto &singleConflict : mConflictData) { + singleConflict.mConflictSelected = selected; + } + + Q_EMIT dataChanged(index(0), index(rowCount() - 1), {static_cast(SyncConflictRoles::ConflictSelected)}); + + if (selected && !mAllConflictingsSelected) { + mAllConflictingsSelected = true; + Q_EMIT allConflictingSelectedChanged(); + } else if (!selected && mAllConflictingsSelected) { + mAllConflictingsSelected = false; + Q_EMIT allConflictingSelectedChanged(); + } +} + void SyncConflictsModel::updateConflictsData() { mConflictData.clear(); @@ -165,4 +266,54 @@ void SyncConflictsModel::updateConflictsData() } } +void SyncConflictsModel::setExistingSelected(bool value, + const QModelIndex &index, + int role) +{ + mConflictData[index.row()].mExistingSelected = value; + Q_EMIT dataChanged(index, index, {role}); + + if (!mConflictData[index.row()].mExistingSelected && mAllExistingsSelected) { + mAllExistingsSelected = false; + Q_EMIT allExistingsSelectedChanged(); + } else if (mConflictData[index.row()].mExistingSelected && !mAllExistingsSelected) { + auto allSelected = true; + for (const auto &singleConflict : qAsConst(mConflictData)) { + if (!singleConflict.mExistingSelected) { + allSelected = false; + break; + } + } + if (allSelected) { + mAllExistingsSelected = true; + Q_EMIT allExistingsSelectedChanged(); + } + } +} + +void SyncConflictsModel::setConflictingSelected(bool value, + const QModelIndex &index, + int role) +{ + mConflictData[index.row()].mConflictSelected = value; + Q_EMIT dataChanged(index, index, {role}); + + if (!mConflictData[index.row()].mConflictSelected && mAllConflictingsSelected) { + mAllConflictingsSelected = false; + Q_EMIT allConflictingSelectedChanged(); + } else if (mConflictData[index.row()].mConflictSelected && !mAllConflictingsSelected) { + auto allSelected = true; + for (const auto &singleConflict : qAsConst(mConflictData)) { + if (!singleConflict.mConflictSelected) { + allSelected = false; + break; + } + } + if (allSelected) { + mAllConflictingsSelected = true; + Q_EMIT allConflictingSelectedChanged(); + } + } +} + } diff --git a/src/gui/syncconflictsmodel.h b/src/gui/syncconflictsmodel.h index d7ca61961d97d..2586ab17b03f2 100644 --- a/src/gui/syncconflictsmodel.h +++ b/src/gui/syncconflictsmodel.h @@ -29,6 +29,10 @@ class SyncConflictsModel : public QAbstractListModel Q_PROPERTY(OCC::ActivityList conflictActivities READ conflictActivities WRITE setConflictActivities NOTIFY conflictActivitiesChanged) + Q_PROPERTY(bool allExistingsSelected READ allExistingsSelected NOTIFY allExistingsSelectedChanged) + + Q_PROPERTY(bool allConflictingSelected READ allConflictingSelected NOTIFY allConflictingSelectedChanged) + struct ConflictInfo { QString mExistingFileName; QString mExistingSize; @@ -62,19 +66,43 @@ class SyncConflictsModel : public QAbstractListModel [[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + [[nodiscard]] bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; + [[nodiscard]] QHash roleNames() const override; + [[nodiscard]] Qt::ItemFlags flags(const QModelIndex &index) const override; + [[nodiscard]] OCC::ActivityList conflictActivities() const; + [[nodiscard]] bool allExistingsSelected() const; + + [[nodiscard]] bool allConflictingSelected() const; + public slots: void setConflictActivities(OCC::ActivityList conflicts); + void selectAllExisting(bool selected); + + void selectAllConflicting(bool selected); + signals: void conflictActivitiesChanged(); + void allExistingsSelectedChanged(); + + void allConflictingSelectedChanged(); + private: void updateConflictsData(); + void setExistingSelected(bool value, + const QModelIndex &index, + int role); + + void setConflictingSelected(bool value, + const QModelIndex &index, + int role); + OCC::ActivityList mData; QVector mConflictData; @@ -82,6 +110,10 @@ public slots: QMimeDatabase mMimeDb; QLocale mLocale; + + bool mAllExistingsSelected = false; + + bool mAllConflictingsSelected = false; }; } From 230e2de23a8456362d6525fe9e001f5a199aba76 Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Fri, 12 May 2023 15:01:28 +0200 Subject: [PATCH 19/28] solve multiple conflicts from choose resolution Signed-off-by: Matthieu Gallien --- src/gui/ResolveConflictsDialog.qml | 3 +-- src/gui/conflictsolver.h | 1 + src/gui/syncconflictsmodel.cpp | 36 ++++++++++++++++++++++++++++++ src/gui/syncconflictsmodel.h | 9 ++++++++ 4 files changed, 47 insertions(+), 2 deletions(-) diff --git a/src/gui/ResolveConflictsDialog.qml b/src/gui/ResolveConflictsDialog.qml index d94541601147f..b0a320e9f3ff6 100644 --- a/src/gui/ResolveConflictsDialog.qml +++ b/src/gui/ResolveConflictsDialog.qml @@ -156,12 +156,11 @@ QtWindow.Window { } onAccepted: function() { - console.log("Ok clicked") + realModel.applyResolution() Systray.destroyDialog(conflictsDialog) } onRejected: function() { - console.log("Cancel clicked") Systray.destroyDialog(conflictsDialog) } } diff --git a/src/gui/conflictsolver.h b/src/gui/conflictsolver.h index b000e78dcc2f7..018741b05ec89 100644 --- a/src/gui/conflictsolver.h +++ b/src/gui/conflictsolver.h @@ -32,6 +32,7 @@ class ConflictSolver : public QObject KeepRemoteVersion, KeepBothVersions }; + Q_ENUM(Solution); explicit ConflictSolver(QWidget *parent = nullptr); diff --git a/src/gui/syncconflictsmodel.cpp b/src/gui/syncconflictsmodel.cpp index cc9f4302b325c..0a80e9a6dabcc 100644 --- a/src/gui/syncconflictsmodel.cpp +++ b/src/gui/syncconflictsmodel.cpp @@ -13,6 +13,7 @@ */ #include "syncconflictsmodel.h" + #include "folderman.h" #include @@ -221,6 +222,19 @@ void SyncConflictsModel::selectAllConflicting(bool selected) } } +void SyncConflictsModel::applyResolution() +{ + for(const auto &syncConflict : qAsConst(mConflictData)) { + if (syncConflict.isValid()) { + qCInfo(lcSyncConflictsModel) << syncConflict.mExistingFilePath << syncConflict.mConflictingFilePath << syncConflict.solution(); + ConflictSolver solver; + solver.setLocalVersionFilename(syncConflict.mConflictingFilePath); + solver.setRemoteVersionFilename(syncConflict.mExistingFilePath); + solver.exec(syncConflict.solution()); + } + } +} + void SyncConflictsModel::updateConflictsData() { mConflictData.clear(); @@ -260,6 +274,8 @@ void SyncConflictsModel::updateConflictsData() QUrl{QStringLiteral("image://tray-image-provider/:/fileicon") + conflictFileInfo.filePath()}, false, false, + existingFileInfo.filePath(), + conflictFileInfo.filePath(), }; mConflictData.push_back(std::move(newConflictData)); @@ -316,4 +332,24 @@ void SyncConflictsModel::setConflictingSelected(bool value, } } +ConflictSolver::Solution SyncConflictsModel::ConflictInfo::solution() const +{ + auto result = ConflictSolver::Solution{}; + + if (mConflictSelected && mExistingSelected) { + result = ConflictSolver::KeepBothVersions; + } else if (!mConflictSelected && mExistingSelected) { + result = ConflictSolver::KeepLocalVersion; + } else if (mConflictSelected && !mExistingSelected) { + result = ConflictSolver::KeepRemoteVersion; + } + + return result; +} + +bool SyncConflictsModel::ConflictInfo::isValid() const +{ + return mConflictSelected || mExistingSelected; +} + } diff --git a/src/gui/syncconflictsmodel.h b/src/gui/syncconflictsmodel.h index 2586ab17b03f2..e77d132babac2 100644 --- a/src/gui/syncconflictsmodel.h +++ b/src/gui/syncconflictsmodel.h @@ -17,6 +17,8 @@ #include "tray/activitydata.h" +#include "conflictsolver.h" + #include #include #include @@ -43,6 +45,11 @@ class SyncConflictsModel : public QAbstractListModel QUrl mConflictPreviewUrl; bool mExistingSelected = false; bool mConflictSelected = false; + QString mExistingFilePath; + QString mConflictingFilePath; + + [[nodiscard]] ConflictSolver::Solution solution() const; + [[nodiscard]] bool isValid() const; }; public: @@ -85,6 +92,8 @@ public slots: void selectAllConflicting(bool selected); + void applyResolution(); + signals: void conflictActivitiesChanged(); From 31e4dede62465d229f99c1c5280b399c4b68d24c Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Tue, 16 May 2023 09:37:45 +0200 Subject: [PATCH 20/28] refactor common parts of conflict delegate into a separate component Signed-off-by: Matthieu Gallien --- resources.qrc | 1 + src/gui/ConflictDelegate.qml | 155 ++++--------------------------- src/gui/ConflictItemFileInfo.qml | 98 +++++++++++++++++++ 3 files changed, 115 insertions(+), 139 deletions(-) create mode 100644 src/gui/ConflictItemFileInfo.qml diff --git a/resources.qrc b/resources.qrc index 12ed9ae1d9229..fdd4bcf570e63 100644 --- a/resources.qrc +++ b/resources.qrc @@ -58,5 +58,6 @@ src/gui/tray/TrayFolderListItem.qml src/gui/ResolveConflictsDialog.qml src/gui/ConflictDelegate.qml + src/gui/ConflictItemFileInfo.qml diff --git a/src/gui/ConflictDelegate.qml b/src/gui/ConflictDelegate.qml index eeebc3f5011c7..6786fb998c8df 100644 --- a/src/gui/ConflictDelegate.qml +++ b/src/gui/ConflictDelegate.qml @@ -53,156 +53,33 @@ Item { anchors.right: parent.right anchors.bottomMargin: 8 - Item { + ConflictItemFileInfo { Layout.fillWidth: true Layout.fillHeight: true - CheckBox { - id: selectExisting + itemSelected: root.existingSelected + itemPreviewUrl: root.existingPreviewUrl + itemVersionLabel: qsTr('Local version') + itemDateLabel: root.existingDate + itemFileSizeLabel: root.existingSize - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - - leftPadding: 0 - spacing: 0 - - checked: root.existingSelected - - onToggled: function() { - model.existingSelected = checked - } - } - - Image { - id: existingPreview - - anchors.left: selectExisting.right - anchors.verticalCenter: parent.verticalCenter - - source: root.existingPreviewUrl - width: 48 - height: 48 - sourceSize.width: 48 - sourceSize.height: 48 - } - - ColumnLayout { - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.left: existingPreview.right - anchors.right: parent.right - anchors.leftMargin: 10 - - spacing: 0 - - Item { - Layout.fillHeight: true - } - - EnforcedPlainTextLabel { - Layout.fillWidth: true - - text: qsTr('Local version') - - font.pixelSize: 15 - } - - EnforcedPlainTextLabel { - Layout.fillWidth: true - - text: root.existingDate - - font.pixelSize: 15 - } - - EnforcedPlainTextLabel { - Layout.fillWidth: true - - text: existingSize - - font.pixelSize: 15 - } - - Item { - Layout.fillHeight: true - } + onSelectedChanged: function() { + model.existingSelected = itemSelected } } - Item { + ConflictItemFileInfo { Layout.fillWidth: true Layout.fillHeight: true - CheckBox { - id: selectConflict - - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: 0 - - leftPadding: 0 - spacing: 0 - - checked: root.conflictSelected - - onToggled: function() { - model.conflictSelected = checked - } - } - - Image { - id: conflictPreview - - anchors.left: selectConflict.right - anchors.verticalCenter: parent.verticalCenter - - source: root.conflictPreviewUrl - width: 48 - height: 48 - sourceSize.width: 48 - sourceSize.height: 48 - } - - ColumnLayout { - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.left: conflictPreview.right - anchors.right: parent.right - anchors.leftMargin: 10 - - spacing: 0 - - Item { - Layout.fillHeight: true - } - - EnforcedPlainTextLabel { - Layout.fillWidth: true - - text: qsTr('Server version') - - font.pixelSize: 15 - } - - EnforcedPlainTextLabel { - Layout.fillWidth: true - - text: root.conflictDate - - font.pixelSize: 15 - } - - EnforcedPlainTextLabel { - Layout.fillWidth: true - - text: conflictSize - - font.pixelSize: 15 - } + itemSelected: root.conflictSelected + itemPreviewUrl: root.conflictPreviewUrl + itemVersionLabel: qsTr('Server version') + itemDateLabel: root.conflictDate + itemFileSizeLabel: root.conflictSize - Item { - Layout.fillHeight: true - } + onSelectedChanged: function() { + model.conflictSelected = itemSelected } } } diff --git a/src/gui/ConflictItemFileInfo.qml b/src/gui/ConflictItemFileInfo.qml new file mode 100644 index 0000000000000..156f1273c5521 --- /dev/null +++ b/src/gui/ConflictItemFileInfo.qml @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2023 by Matthieu Gallien + * + * 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 QtQuick.Controls 2.15 +import Style 1.0 +import "./tray" + +Item { + property alias itemSelected: selectItem.checked + property alias itemPreviewUrl: itemPreview.source + property alias itemVersionLabel: versionLabel.text + property alias itemDateLabel: dateLabel.text + property alias itemFileSizeLabel: fileSizeLabel.text + + signal selectedChanged() + + CheckBox { + id: selectItem + + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + + leftPadding: 0 + spacing: 0 + + onToggled: function() { + selectedChanged() + } + } + + Image { + id: itemPreview + + anchors.left: selectItem.right + anchors.verticalCenter: parent.verticalCenter + + width: 48 + height: 48 + sourceSize.width: 48 + sourceSize.height: 48 + } + + ColumnLayout { + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.left: itemPreview.right + anchors.right: parent.right + anchors.leftMargin: 10 + + spacing: 0 + + Item { + Layout.fillHeight: true + } + + EnforcedPlainTextLabel { + id: versionLabel + + Layout.fillWidth: true + + font.pixelSize: 15 + } + + EnforcedPlainTextLabel { + id: dateLabel + + Layout.fillWidth: true + + font.pixelSize: 15 + } + + EnforcedPlainTextLabel { + id: fileSizeLabel + + Layout.fillWidth: true + + font.pixelSize: 15 + } + + Item { + Layout.fillHeight: true + } + } +} From 635b1ec729f5cc51d9f54dae8e2c3ed0a2844f6f Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Tue, 16 May 2023 09:04:11 +0200 Subject: [PATCH 21/28] define constants specific for font sizes in conflict dialog Signed-off-by: Matthieu Gallien --- src/gui/ConflictDelegate.qml | 2 +- src/gui/ConflictItemFileInfo.qml | 6 +++--- src/gui/ResolveConflictsDialog.qml | 8 ++++---- theme/Style/Style.qml | 3 +++ 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/gui/ConflictDelegate.qml b/src/gui/ConflictDelegate.qml index 6786fb998c8df..ff6b48eb00e71 100644 --- a/src/gui/ConflictDelegate.qml +++ b/src/gui/ConflictDelegate.qml @@ -43,7 +43,7 @@ Item { text: root.existingFileName font.weight: Font.Bold - font.pixelSize: 15 + font.pixelSize: Style.fontPixelSizeResolveConflictsDialog } RowLayout { diff --git a/src/gui/ConflictItemFileInfo.qml b/src/gui/ConflictItemFileInfo.qml index 156f1273c5521..be345f7e8a945 100644 --- a/src/gui/ConflictItemFileInfo.qml +++ b/src/gui/ConflictItemFileInfo.qml @@ -72,7 +72,7 @@ Item { Layout.fillWidth: true - font.pixelSize: 15 + font.pixelSize: Style.fontPixelSizeResolveConflictsDialog } EnforcedPlainTextLabel { @@ -80,7 +80,7 @@ Item { Layout.fillWidth: true - font.pixelSize: 15 + font.pixelSize: Style.fontPixelSizeResolveConflictsDialog } EnforcedPlainTextLabel { @@ -88,7 +88,7 @@ Item { Layout.fillWidth: true - font.pixelSize: 15 + font.pixelSize: Style.fontPixelSizeResolveConflictsDialog } Item { diff --git a/src/gui/ResolveConflictsDialog.qml b/src/gui/ResolveConflictsDialog.qml index b0a320e9f3ff6..7b6efb18088f4 100644 --- a/src/gui/ResolveConflictsDialog.qml +++ b/src/gui/ResolveConflictsDialog.qml @@ -53,14 +53,14 @@ QtWindow.Window { EnforcedPlainTextLabel { text: qsTr("%1 files in conflict").arg(delegateModel.count) font.bold: true - font.pixelSize: 20 + font.pixelSize: Style.bigFontPixelSizeResolveConflictsDialog Layout.fillWidth: true } EnforcedPlainTextLabel { text: qsTr("Choose if you want to keep the local version, server version, or both. If you choose both, the local file will have a number added to its name.") wrapMode: Text.WrapAtWordBoundaryOrAnywhere - font.pixelSize: 15 + font.pixelSize: Style.fontPixelSizeResolveConflictsDialog Layout.fillWidth: true Layout.topMargin: -15 } @@ -79,7 +79,7 @@ QtWindow.Window { leftPadding: 0 implicitWidth: 100 - font.pixelSize: 15 + font.pixelSize: Style.fontPixelSizeResolveConflictsDialog checked: realModel.allExistingsSelected onToggled: function() { @@ -97,7 +97,7 @@ QtWindow.Window { leftPadding: 0 implicitWidth: 100 - font.pixelSize: 15 + font.pixelSize: Style.fontPixelSizeResolveConflictsDialog checked: realModel.allConflictingSelected onToggled: function() { diff --git a/theme/Style/Style.qml b/theme/Style/Style.qml index 21bfeb460e883..34f2470abb12e 100644 --- a/theme/Style/Style.qml +++ b/theme/Style/Style.qml @@ -131,6 +131,9 @@ QtObject { readonly property var fontMetrics: FontMetrics {} + readonly property int bigFontPixelSizeResolveConflictsDialog: 20 + readonly property int fontPixelSizeResolveConflictsDialog: 15 + readonly property int activityContentSpace: 4 readonly property double smallIconScaleFactor: 0.6 From b285b89605a016223004b0a14aaf3440e1334534 Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Tue, 16 May 2023 09:37:45 +0200 Subject: [PATCH 22/28] define minimum sizes of conflict dialogs in Style component Signed-off-by: Matthieu Gallien --- src/gui/ResolveConflictsDialog.qml | 8 ++++---- theme/Style/Style.qml | 2 ++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/gui/ResolveConflictsDialog.qml b/src/gui/ResolveConflictsDialog.qml index 7b6efb18088f4..77b01eecfd0d4 100644 --- a/src/gui/ResolveConflictsDialog.qml +++ b/src/gui/ResolveConflictsDialog.qml @@ -30,10 +30,10 @@ QtWindow.Window { flags: Qt.Window | Qt.Dialog visible: true - width: 600 - height: 800 - minimumWidth: 600 - minimumHeight: 800 + width: Style.minimumWidthResolveConflictsDialog + height: Style.minimumHeightResolveConflictsDialog + minimumWidth: Style.minimumWidthResolveConflictsDialog + minimumHeight: Style.minimumHeightResolveConflictsDialog title: qsTr('Solve sync conflicts') onClosing: function(close) { diff --git a/theme/Style/Style.qml b/theme/Style/Style.qml index 34f2470abb12e..e9fbdbf2ee68c 100644 --- a/theme/Style/Style.qml +++ b/theme/Style/Style.qml @@ -133,6 +133,8 @@ QtObject { readonly property int bigFontPixelSizeResolveConflictsDialog: 20 readonly property int fontPixelSizeResolveConflictsDialog: 15 + readonly property int minimumWidthResolveConflictsDialog: 600 + readonly property int minimumHeightResolveConflictsDialog: 800 readonly property int activityContentSpace: 4 From 8d81257db0f01a9aabee27ab6861bf5adc25dcfd Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Tue, 16 May 2023 09:54:09 +0200 Subject: [PATCH 23/28] improve translations for strings with placeholder to display numbers Signed-off-by: Matthieu Gallien --- src/gui/ResolveConflictsDialog.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gui/ResolveConflictsDialog.qml b/src/gui/ResolveConflictsDialog.qml index 77b01eecfd0d4..0a736856a95d7 100644 --- a/src/gui/ResolveConflictsDialog.qml +++ b/src/gui/ResolveConflictsDialog.qml @@ -51,7 +51,7 @@ QtWindow.Window { z: 2 EnforcedPlainTextLabel { - text: qsTr("%1 files in conflict").arg(delegateModel.count) + text: qsTr("%1 files in conflict", 'indicate the number of conflicts to resolve', delegateModel.count).arg(delegateModel.count) font.bold: true font.pixelSize: Style.bigFontPixelSizeResolveConflictsDialog Layout.fillWidth: true From 534939506359e26c3534e0916cc3d9b91c541e9b Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Tue, 16 May 2023 10:37:40 +0200 Subject: [PATCH 24/28] model improvements Signed-off-by: Matthieu Gallien --- src/gui/ResolveConflictsDialog.qml | 2 +- src/gui/syncconflictsmodel.cpp | 138 +++++++++++++---------------- src/gui/syncconflictsmodel.h | 29 +++--- 3 files changed, 79 insertions(+), 90 deletions(-) diff --git a/src/gui/ResolveConflictsDialog.qml b/src/gui/ResolveConflictsDialog.qml index 0a736856a95d7..99bd61da5a8ed 100644 --- a/src/gui/ResolveConflictsDialog.qml +++ b/src/gui/ResolveConflictsDialog.qml @@ -156,7 +156,7 @@ QtWindow.Window { } onAccepted: function() { - realModel.applyResolution() + realModel.applySolution() Systray.destroyDialog(conflictsDialog) } diff --git a/src/gui/syncconflictsmodel.cpp b/src/gui/syncconflictsmodel.cpp index 0a80e9a6dabcc..3d455d8729269 100644 --- a/src/gui/syncconflictsmodel.cpp +++ b/src/gui/syncconflictsmodel.cpp @@ -33,7 +33,7 @@ int SyncConflictsModel::rowCount(const QModelIndex &parent) const return 0; } - return mData.size(); + return _data.size(); } QVariant SyncConflictsModel::data(const QModelIndex &index, int role) const @@ -47,35 +47,35 @@ QVariant SyncConflictsModel::data(const QModelIndex &index, int role) const } if (role >= static_cast(SyncConflictRoles::ExistingFileName) && role <= static_cast(SyncConflictRoles::ConflictPreviewUrl)) { - auto convertedRole = static_cast(role); + const auto convertedRole = static_cast(role); switch (convertedRole) { case SyncConflictRoles::ExistingFileName: - result = mConflictData[index.row()].mExistingFileName; + result = _conflictData[index.row()].mExistingFileName; break; case SyncConflictRoles::ExistingSize: - result = mConflictData[index.row()].mExistingSize; + result = _conflictData[index.row()].mExistingSize; break; case SyncConflictRoles::ConflictSize: - result = mConflictData[index.row()].mConflictSize; + result = _conflictData[index.row()].mConflictSize; break; case SyncConflictRoles::ExistingDate: - result = mConflictData[index.row()].mExistingDate; + result = _conflictData[index.row()].mExistingDate; break; case SyncConflictRoles::ConflictDate: - result = mConflictData[index.row()].mConflictDate; + result = _conflictData[index.row()].mConflictDate; break; case SyncConflictRoles::ExistingSelected: - result = mConflictData[index.row()].mExistingSelected; + result = _conflictData[index.row()].mExistingSelected == ConflictInfo::ConflictSolution::SolutionSelected; break; case SyncConflictRoles::ConflictSelected: - result = mConflictData[index.row()].mConflictSelected; + result = _conflictData[index.row()].mConflictSelected == ConflictInfo::ConflictSolution::SolutionSelected; break; case SyncConflictRoles::ExistingPreviewUrl: - result = mConflictData[index.row()].mExistingPreviewUrl; + result = _conflictData[index.row()].mExistingPreviewUrl; break; case SyncConflictRoles::ConflictPreviewUrl: - result = mConflictData[index.row()].mConflictPreviewUrl; + result = _conflictData[index.row()].mConflictPreviewUrl; break; } } @@ -94,7 +94,7 @@ bool SyncConflictsModel::setData(const QModelIndex &index, const QVariant &value } if (role >= static_cast(SyncConflictRoles::ExistingFileName) && role <= static_cast(SyncConflictRoles::ConflictPreviewUrl)) { - auto convertedRole = static_cast(role); + const auto convertedRole = static_cast(role); switch(convertedRole) { case SyncConflictRoles::ExistingFileName: @@ -159,28 +159,28 @@ Qt::ItemFlags SyncConflictsModel::flags(const QModelIndex &index) const ActivityList SyncConflictsModel::conflictActivities() const { - return mData; + return _data; } bool SyncConflictsModel::allExistingsSelected() const { - return mAllExistingsSelected; + return _allExistingsSelected; } bool SyncConflictsModel::allConflictingSelected() const { - return mAllConflictingsSelected; + return _allConflictingsSelected; } void SyncConflictsModel::setConflictActivities(ActivityList conflicts) { - if (mData == conflicts) { + if (_data == conflicts) { return; } beginResetModel(); - mData = conflicts; + _data = conflicts; emit conflictActivitiesChanged(); updateConflictsData(); @@ -190,41 +190,41 @@ void SyncConflictsModel::setConflictActivities(ActivityList conflicts) void SyncConflictsModel::selectAllExisting(bool selected) { - for (auto &singleConflict : mConflictData) { - singleConflict.mExistingSelected = selected; + for (auto &singleConflict : _conflictData) { + singleConflict.mExistingSelected = selected ? ConflictInfo::ConflictSolution::SolutionSelected : ConflictInfo::ConflictSolution::SolutionDeselected; } Q_EMIT dataChanged(index(0), index(rowCount() - 1), {static_cast(SyncConflictRoles::ExistingSelected)}); - if (selected && !mAllExistingsSelected) { - mAllExistingsSelected = true; + if (selected && !_allExistingsSelected) { + _allExistingsSelected = true; Q_EMIT allExistingsSelectedChanged(); - } else if (!selected && mAllExistingsSelected) { - mAllExistingsSelected = false; + } else if (!selected && _allExistingsSelected) { + _allExistingsSelected = false; Q_EMIT allExistingsSelectedChanged(); } } void SyncConflictsModel::selectAllConflicting(bool selected) { - for (auto &singleConflict : mConflictData) { - singleConflict.mConflictSelected = selected; + for (auto &singleConflict : _conflictData) { + singleConflict.mConflictSelected = selected ? ConflictInfo::ConflictSolution::SolutionSelected : ConflictInfo::ConflictSolution::SolutionDeselected; } Q_EMIT dataChanged(index(0), index(rowCount() - 1), {static_cast(SyncConflictRoles::ConflictSelected)}); - if (selected && !mAllConflictingsSelected) { - mAllConflictingsSelected = true; + if (selected && !_allConflictingsSelected) { + _allConflictingsSelected = true; Q_EMIT allConflictingSelectedChanged(); - } else if (!selected && mAllConflictingsSelected) { - mAllConflictingsSelected = false; + } else if (!selected && _allConflictingsSelected) { + _allConflictingsSelected = false; Q_EMIT allConflictingSelectedChanged(); } } -void SyncConflictsModel::applyResolution() +void SyncConflictsModel::applySolution() { - for(const auto &syncConflict : qAsConst(mConflictData)) { + for(const auto &syncConflict : qAsConst(_conflictData)) { if (syncConflict.isValid()) { qCInfo(lcSyncConflictsModel) << syncConflict.mExistingFilePath << syncConflict.mConflictingFilePath << syncConflict.solution(); ConflictSolver solver; @@ -237,25 +237,19 @@ void SyncConflictsModel::applyResolution() void SyncConflictsModel::updateConflictsData() { - mConflictData.clear(); - mConflictData.reserve(mData.size()); + _conflictData.clear(); + _conflictData.reserve(_data.size()); - for (const auto &oneConflict : qAsConst(mData)) { - if (!FolderMan::instance()) { - qCWarning(lcSyncConflictsModel) << "no FolderMan instance"; - mConflictData.push_back({}); - continue; - } + for (const auto &oneConflict : qAsConst(_data)) { const auto folder = FolderMan::instance()->folder(oneConflict._folder); if (!folder) { qCWarning(lcSyncConflictsModel) << "no Folder instance for" << oneConflict._folder; - mConflictData.push_back({}); + _conflictData.push_back({}); continue; } const auto conflictedRelativePath = oneConflict._file; - const auto dbRecord = folder->journalDb(); - const auto baseRelativePath = dbRecord ? dbRecord->conflictFileBaseName(conflictedRelativePath.toUtf8()) : QString{}; + const auto baseRelativePath = folder->journalDb() ? folder->journalDb()->conflictFileBaseName(conflictedRelativePath.toUtf8()) : QString{}; const auto dir = QDir(folder->path()); const auto conflictedPath = dir.filePath(conflictedRelativePath); @@ -266,19 +260,19 @@ void SyncConflictsModel::updateConflictsData() auto newConflictData = ConflictInfo{ existingFileInfo.fileName(), - mLocale.formattedDataSize(existingFileInfo.size()), - mLocale.formattedDataSize(conflictFileInfo.size()), + _locale.formattedDataSize(existingFileInfo.size()), + _locale.formattedDataSize(conflictFileInfo.size()), existingFileInfo.lastModified().toString(), conflictFileInfo.lastModified().toString(), QUrl{QStringLiteral("image://tray-image-provider/:/fileicon") + existingFileInfo.filePath()}, QUrl{QStringLiteral("image://tray-image-provider/:/fileicon") + conflictFileInfo.filePath()}, - false, - false, + ConflictInfo::ConflictSolution::SolutionDeselected, + ConflictInfo::ConflictSolution::SolutionDeselected, existingFileInfo.filePath(), conflictFileInfo.filePath(), }; - mConflictData.push_back(std::move(newConflictData)); + _conflictData.push_back(std::move(newConflictData)); } } @@ -286,22 +280,19 @@ void SyncConflictsModel::setExistingSelected(bool value, const QModelIndex &index, int role) { - mConflictData[index.row()].mExistingSelected = value; + _conflictData[index.row()].mExistingSelected = value ? ConflictInfo::ConflictSolution::SolutionSelected : ConflictInfo::ConflictSolution::SolutionDeselected; Q_EMIT dataChanged(index, index, {role}); - if (!mConflictData[index.row()].mExistingSelected && mAllExistingsSelected) { - mAllExistingsSelected = false; + if (_conflictData[index.row()].mExistingSelected == ConflictInfo::ConflictSolution::SolutionDeselected && _allExistingsSelected) { + _allExistingsSelected = false; Q_EMIT allExistingsSelectedChanged(); - } else if (mConflictData[index.row()].mExistingSelected && !mAllExistingsSelected) { - auto allSelected = true; - for (const auto &singleConflict : qAsConst(mConflictData)) { - if (!singleConflict.mExistingSelected) { - allSelected = false; - break; - } - } + } else if (_conflictData[index.row()].mExistingSelected == ConflictInfo::ConflictSolution::SolutionSelected && !_allExistingsSelected) { + const auto deselectedConflictIt = std::find_if(_conflictData.constBegin(), _conflictData.constEnd(), [] (const auto singleConflict) { + return singleConflict.mExistingSelected == ConflictInfo::ConflictSolution::SolutionDeselected; + }); + const auto allSelected = (deselectedConflictIt == _conflictData.constEnd()); if (allSelected) { - mAllExistingsSelected = true; + _allExistingsSelected = true; Q_EMIT allExistingsSelectedChanged(); } } @@ -311,22 +302,19 @@ void SyncConflictsModel::setConflictingSelected(bool value, const QModelIndex &index, int role) { - mConflictData[index.row()].mConflictSelected = value; + _conflictData[index.row()].mConflictSelected = value ? ConflictInfo::ConflictSolution::SolutionSelected : ConflictInfo::ConflictSolution::SolutionDeselected; Q_EMIT dataChanged(index, index, {role}); - if (!mConflictData[index.row()].mConflictSelected && mAllConflictingsSelected) { - mAllConflictingsSelected = false; + if (_conflictData[index.row()].mConflictSelected == ConflictInfo::ConflictSolution::SolutionDeselected && _allConflictingsSelected) { + _allConflictingsSelected = false; Q_EMIT allConflictingSelectedChanged(); - } else if (mConflictData[index.row()].mConflictSelected && !mAllConflictingsSelected) { - auto allSelected = true; - for (const auto &singleConflict : qAsConst(mConflictData)) { - if (!singleConflict.mConflictSelected) { - allSelected = false; - break; - } - } + } else if (_conflictData[index.row()].mConflictSelected == ConflictInfo::ConflictSolution::SolutionSelected && !_allConflictingsSelected) { + const auto deselectedConflictIt = std::find_if(_conflictData.constBegin(), _conflictData.constEnd(), [] (const auto singleConflict) { + return singleConflict.mConflictSelected == ConflictInfo::ConflictSolution::SolutionDeselected; + }); + const auto allSelected = (deselectedConflictIt == _conflictData.constEnd()); if (allSelected) { - mAllConflictingsSelected = true; + _allConflictingsSelected = true; Q_EMIT allConflictingSelectedChanged(); } } @@ -336,11 +324,11 @@ ConflictSolver::Solution SyncConflictsModel::ConflictInfo::solution() const { auto result = ConflictSolver::Solution{}; - if (mConflictSelected && mExistingSelected) { + if (mConflictSelected == ConflictSolution::SolutionSelected && mExistingSelected == ConflictSolution::SolutionSelected) { result = ConflictSolver::KeepBothVersions; - } else if (!mConflictSelected && mExistingSelected) { + } else if (mConflictSelected == ConflictSolution::SolutionDeselected && mExistingSelected == ConflictSolution::SolutionSelected) { result = ConflictSolver::KeepLocalVersion; - } else if (mConflictSelected && !mExistingSelected) { + } else if (mConflictSelected == ConflictSolution::SolutionSelected && mExistingSelected == ConflictSolution::SolutionDeselected) { result = ConflictSolver::KeepRemoteVersion; } @@ -349,7 +337,7 @@ ConflictSolver::Solution SyncConflictsModel::ConflictInfo::solution() const bool SyncConflictsModel::ConflictInfo::isValid() const { - return mConflictSelected || mExistingSelected; + return mConflictSelected == ConflictInfo::ConflictSolution::SolutionSelected || mExistingSelected == ConflictInfo::ConflictSolution::SolutionSelected; } } diff --git a/src/gui/syncconflictsmodel.h b/src/gui/syncconflictsmodel.h index e77d132babac2..af7d8a73eb845 100644 --- a/src/gui/syncconflictsmodel.h +++ b/src/gui/syncconflictsmodel.h @@ -12,8 +12,7 @@ * for more details. */ -#ifndef SYNCCONFLICTSMODEL_H -#define SYNCCONFLICTSMODEL_H +#pragma once #include "tray/activitydata.h" @@ -36,6 +35,12 @@ class SyncConflictsModel : public QAbstractListModel Q_PROPERTY(bool allConflictingSelected READ allConflictingSelected NOTIFY allConflictingSelectedChanged) struct ConflictInfo { + enum class ConflictSolution : bool{ + SolutionSelected = true, + SolutionDeselected = false, + }; + + QString mExistingFileName; QString mExistingSize; QString mConflictSize; @@ -43,8 +48,8 @@ class SyncConflictsModel : public QAbstractListModel QString mConflictDate; QUrl mExistingPreviewUrl; QUrl mConflictPreviewUrl; - bool mExistingSelected = false; - bool mConflictSelected = false; + ConflictSolution mExistingSelected = ConflictSolution::SolutionDeselected; + ConflictSolution mConflictSelected = ConflictSolution::SolutionDeselected; QString mExistingFilePath; QString mConflictingFilePath; @@ -92,7 +97,7 @@ public slots: void selectAllConflicting(bool selected); - void applyResolution(); + void applySolution(); signals: void conflictActivitiesChanged(); @@ -112,19 +117,15 @@ public slots: const QModelIndex &index, int role); - OCC::ActivityList mData; + OCC::ActivityList _data; - QVector mConflictData; + QVector _conflictData; - QMimeDatabase mMimeDb; + QLocale _locale; - QLocale mLocale; + bool _allExistingsSelected = false; - bool mAllExistingsSelected = false; - - bool mAllConflictingsSelected = false; + bool _allConflictingsSelected = false; }; } - -#endif // SYNCCONFLICTSMODEL_H From 14bce6fdc26c5b28cf77d16bad8a14cb2aca1288 Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Tue, 16 May 2023 11:30:45 +0200 Subject: [PATCH 25/28] improve reliability of memeory management when creating conflicts dialog use more automatyed memory management to reduce possible errors Signed-off-by: Matthieu Gallien --- src/gui/systray.cpp | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/gui/systray.cpp b/src/gui/systray.cpp index 4e14c2b5d2523..08872c28925fc 100644 --- a/src/gui/systray.cpp +++ b/src/gui/systray.cpp @@ -287,38 +287,32 @@ void Systray::destroyEditFileLocallyLoadingDialog() void Systray::createResolveConflictsDialog(const OCC::ActivityList &allConflicts) { - const auto conflictsDialog = new QQmlComponent(_trayEngine, QStringLiteral("qrc:/qml/src/gui/ResolveConflictsDialog.qml")); + const auto conflictsDialog = std::make_unique(_trayEngine, QStringLiteral("qrc:/qml/src/gui/ResolveConflictsDialog.qml")); const QVariantMap initialProperties{ {"allConflicts", QVariant::fromValue(allConflicts)}, }; if(conflictsDialog->isError()) { qCWarning(lcSystray) << conflictsDialog->errorString(); - delete conflictsDialog; return; } // This call dialog gets deallocated on close conditions // by a call from the QML side to the destroyDialog slot - auto dialog = conflictsDialog->createWithInitialProperties(initialProperties); + auto dialog = QScopedPointer(conflictsDialog->createWithInitialProperties(initialProperties)); if (!dialog) { - delete dialog; - delete conflictsDialog; return; } dialog->setParent(QGuiApplication::instance()); - auto dialogWindow = qobject_cast(dialog); + auto dialogWindow = qobject_cast(dialog.data()); if (!dialogWindow) { - delete dialog; - delete conflictsDialog; return; } dialogWindow->show(); dialogWindow->raise(); dialogWindow->requestActivate(); - - delete conflictsDialog; + dialog.take(); } bool Systray::raiseDialogs() From 8faab817a093758860f45240107a4cfe4b3b6baf Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Tue, 16 May 2023 11:36:46 +0200 Subject: [PATCH 26/28] improve automated tests Signed-off-by: Matthieu Gallien --- test/testsyncconflictsmodel.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/testsyncconflictsmodel.cpp b/test/testsyncconflictsmodel.cpp index 0eb9f6d075e6a..de1596cc38e5f 100644 --- a/test/testsyncconflictsmodel.cpp +++ b/test/testsyncconflictsmodel.cpp @@ -53,7 +53,7 @@ private slots: { } - void testSuccessfulFetchShares() + void testSettingConflicts() { auto dir = QTemporaryDir {}; ConfigFile::setConfDir(dir.path()); // we don't want to pollute the user's config file @@ -109,7 +109,6 @@ private slots: QCOMPARE(model.data(model.index(0), static_cast(SyncConflictsModel::SyncConflictRoles::ExistingSelected)), false); QCOMPARE(model.data(model.index(0), static_cast(SyncConflictsModel::SyncConflictRoles::ConflictSelected)), false); } - }; QTEST_GUILESS_MAIN(TestSyncConflictsModel) From 8f60518d6fbb2e0f73969ea817813b19c0c1a4a3 Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Tue, 16 May 2023 11:36:59 +0200 Subject: [PATCH 27/28] small qml cleanup to make code faster to read Signed-off-by: Matthieu Gallien --- src/gui/tray/SyncStatus.qml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/gui/tray/SyncStatus.qml b/src/gui/tray/SyncStatus.qml index 40c6553c6b0f3..40167322c8968 100644 --- a/src/gui/tray/SyncStatus.qml +++ b/src/gui/tray/SyncStatus.qml @@ -151,8 +151,6 @@ RowLayout { NC.UserModel.currentUser.hasLocalFolder && NC.UserModel.currentUser.isConnected enabled: visible - onClicked: { - NC.Systray.createResolveConflictsDialog(activityModel.allConflicts); - } + onClicked: NC.Systray.createResolveConflictsDialog(activityModel.allConflicts); } } From 6c7f81009523866f6a6e912bfb0434626739b74e Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Tue, 16 May 2023 11:54:32 +0200 Subject: [PATCH 28/28] improve code in activity list model Signed-off-by: Matthieu Gallien --- src/gui/tray/activitylistmodel.cpp | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/gui/tray/activitylistmodel.cpp b/src/gui/tray/activitylistmodel.cpp index 8adae711f620b..7b152a9842b04 100644 --- a/src/gui/tray/activitylistmodel.cpp +++ b/src/gui/tray/activitylistmodel.cpp @@ -551,13 +551,10 @@ void ActivityListModel::addEntriesToActivityList(const ActivityList &activityLis } endInsertRows(); - auto conflictsFound = false; - for(const auto &activity : _finalList) { - if (activity._syncFileItemStatus == SyncFileItem::Conflict) { - conflictsFound = true; - break; - } - } + const auto deselectedConflictIt = std::find_if(_finalList.constBegin(), _finalList.constEnd(), [] (const auto activity) { + return activity._syncFileItemStatus == SyncFileItem::Conflict; + }); + const auto conflictsFound = (deselectedConflictIt != _finalList.constEnd()); setHasSyncConflicts(conflictsFound); }