Add a conflict dialog to help the user solve them
authorKevin Ottens <kevin.ottens@nextcloud.com>
Wed, 7 Oct 2020 15:51:18 +0000 (17:51 +0200)
committerKevin Ottens <kevin.ottens@nextcloud.com>
Thu, 22 Oct 2020 14:40:46 +0000 (16:40 +0200)
Signed-off-by: Kevin Ottens <kevin.ottens@nextcloud.com>
src/gui/CMakeLists.txt
src/gui/conflictdialog.cpp [new file with mode: 0644]
src/gui/conflictdialog.h [new file with mode: 0644]
src/gui/conflictdialog.ui [new file with mode: 0644]

index 7ae36a983c646ecd59453879c9612e0b390ccd77..000d00752f696ebbe7c2ff85571dde1f793f09aa 100644 (file)
@@ -21,6 +21,7 @@ endif()
 
 set(client_UI_SRCS
     accountsettings.ui
+    conflictdialog.ui
     folderwizardsourcepage.ui
     folderwizardtargetpage.ui
     generalsettings.ui
@@ -54,6 +55,7 @@ set(client_SRCS
     accountmanager.cpp
     accountsettings.cpp
     application.cpp
+    conflictdialog.cpp
     conflictsolver.cpp
     connectionvalidator.cpp
     folder.cpp
diff --git a/src/gui/conflictdialog.cpp b/src/gui/conflictdialog.cpp
new file mode 100644 (file)
index 0000000..bad4e7c
--- /dev/null
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) by Kevin Ottens <kevin.ottens@nextcloud.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+
+#include "conflictdialog.h"
+#include "ui_conflictdialog.h"
+
+#include "conflictsolver.h"
+
+#include <QDateTime>
+#include <QDebug>
+#include <QDesktopServices>
+#include <QFileInfo>
+#include <QMimeDatabase>
+#include <QPushButton>
+#include <QUrl>
+
+namespace {
+void forceHeaderFont(QWidget *widget)
+{
+    auto font = widget->font();
+    font.setPointSizeF(font.pointSizeF() * 1.5);
+    widget->setFont(font);
+}
+
+void setBoldFont(QWidget *widget, bool bold)
+{
+    auto font = widget->font();
+    font.setBold(bold);
+    widget->setFont(font);
+}
+}
+
+namespace OCC {
+
+ConflictDialog::ConflictDialog(QWidget *parent)
+    : QDialog(parent)
+    , _ui(new Ui::ConflictDialog)
+    , _solver(new ConflictSolver(this))
+{
+    _ui->setupUi(this);
+    forceHeaderFont(_ui->conflictMessage);
+    _ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
+    _ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Keep selected version"));
+
+    connect(_ui->localVersionRadio, &QCheckBox::toggled, this, &ConflictDialog::updateButtonStates);
+    connect(_ui->localVersionButton, &QToolButton::clicked, this, [=] {
+        QDesktopServices::openUrl(QUrl::fromLocalFile(_solver->localVersionFilename()));
+    });
+
+    connect(_ui->remoteVersionRadio, &QCheckBox::toggled, this, &ConflictDialog::updateButtonStates);
+    connect(_ui->remoteVersionButton, &QToolButton::clicked, this, [=] {
+        QDesktopServices::openUrl(QUrl::fromLocalFile(_solver->remoteVersionFilename()));
+    });
+
+    connect(_solver, &ConflictSolver::localVersionFilenameChanged, this, &ConflictDialog::updateWidgets);
+    connect(_solver, &ConflictSolver::remoteVersionFilenameChanged, this, &ConflictDialog::updateWidgets);
+}
+
+QString ConflictDialog::baseFilename() const
+{
+    return _baseFilename;
+}
+
+ConflictDialog::~ConflictDialog() = default;
+
+QString ConflictDialog::localVersionFilename() const
+{
+    return _solver->localVersionFilename();
+}
+
+QString ConflictDialog::remoteVersionFilename() const
+{
+    return _solver->remoteVersionFilename();
+}
+
+void ConflictDialog::setBaseFilename(const QString &baseFilename)
+{
+    if (_baseFilename == baseFilename) {
+        return;
+    }
+
+    _baseFilename = baseFilename;
+    _ui->conflictMessage->setText(tr("Conflicting versions of %1.").arg(_baseFilename));
+}
+
+void ConflictDialog::setLocalVersionFilename(const QString &localVersionFilename)
+{
+    _solver->setLocalVersionFilename(localVersionFilename);
+}
+
+void ConflictDialog::setRemoteVersionFilename(const QString &remoteVersionFilename)
+{
+    _solver->setRemoteVersionFilename(remoteVersionFilename);
+}
+
+void ConflictDialog::accept()
+{
+    const auto isLocalPicked = _ui->localVersionRadio->isChecked();
+    const auto isRemotePicked = _ui->remoteVersionRadio->isChecked();
+
+    Q_ASSERT(isLocalPicked || isRemotePicked);
+    if (!isLocalPicked && !isRemotePicked) {
+        return;
+    }
+
+    const auto solution = isLocalPicked && isRemotePicked ? ConflictSolver::KeepBothVersions
+                        : isLocalPicked ? ConflictSolver::KeepLocalVersion
+                        : ConflictSolver::KeepRemoteVersion;
+    if (_solver->exec(solution)) {
+        QDialog::accept();
+    }
+}
+
+void ConflictDialog::updateWidgets()
+{
+    QMimeDatabase mimeDb;
+
+    const auto updateGroup = [this, &mimeDb](const QString &filename, QLabel *linkLabel, const QString &linkText, QLabel *mtimeLabel, QLabel *sizeLabel, QToolButton *button) {
+        const auto fileUrl = QUrl::fromLocalFile(filename).toString();
+        linkLabel->setText(QStringLiteral("<a href='%1'>%2</a>").arg(fileUrl).arg(linkText));
+
+        const auto info = QFileInfo(filename);
+        mtimeLabel->setText(info.lastModified().toString());
+        sizeLabel->setText(locale().formattedDataSize(info.size()));
+
+        const auto mime = mimeDb.mimeTypeForFile(filename);
+        if (QIcon::hasThemeIcon(mime.iconName())) {
+            button->setIcon(QIcon::fromTheme(mime.iconName()));
+        } else {
+            button->setIcon(QIcon(":/qt-project.org/styles/commonstyle/images/file-128.png"));
+        }
+    };
+
+    const auto localVersion = _solver->localVersionFilename();
+    updateGroup(localVersion,
+                _ui->localVersionLink,
+                tr("Open local version"),
+                _ui->localVersionMtime,
+                _ui->localVersionSize,
+                _ui->localVersionButton);
+
+    const auto remoteVersion = _solver->remoteVersionFilename();
+    updateGroup(remoteVersion,
+                _ui->remoteVersionLink,
+                tr("Open remote version"),
+                _ui->remoteVersionMtime,
+                _ui->remoteVersionSize,
+                _ui->remoteVersionButton);
+
+    const auto localMtime = QFileInfo(localVersion).lastModified();
+    const auto remoteMtime = QFileInfo(remoteVersion).lastModified();
+
+    setBoldFont(_ui->localVersionMtime, localMtime > remoteMtime);
+    setBoldFont(_ui->remoteVersionMtime, remoteMtime > localMtime);
+}
+
+void ConflictDialog::updateButtonStates()
+{
+    const auto isLocalPicked = _ui->localVersionRadio->isChecked();
+    const auto isRemotePicked = _ui->remoteVersionRadio->isChecked();
+    _ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(isLocalPicked || isRemotePicked);
+
+    const auto text = isLocalPicked && isRemotePicked ? tr("Keep both versions")
+                    : isLocalPicked ? tr("Keep local version")
+                    : isRemotePicked ? tr("Keep server version")
+                    : tr("Keep selected version");
+    _ui->buttonBox->button(QDialogButtonBox::Ok)->setText(text);
+}
+
+} // namespace OCC
diff --git a/src/gui/conflictdialog.h b/src/gui/conflictdialog.h
new file mode 100644 (file)
index 0000000..5c5b763
--- /dev/null
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) by Kevin Ottens <kevin.ottens@nextcloud.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+
+#ifndef CONFLICTDIALOG_H
+#define CONFLICTDIALOG_H
+
+#include <QDialog>
+
+namespace OCC {
+
+class ConflictSolver;
+
+namespace Ui {
+    class ConflictDialog;
+}
+
+class ConflictDialog : public QDialog
+{
+    Q_OBJECT
+public:
+    explicit ConflictDialog(QWidget *parent = nullptr);
+    ~ConflictDialog() override;
+
+    QString baseFilename() const;
+    QString localVersionFilename() const;
+    QString remoteVersionFilename() const;
+
+public slots:
+    void setBaseFilename(const QString &baseFilename);
+    void setLocalVersionFilename(const QString &localVersionFilename);
+    void setRemoteVersionFilename(const QString &remoteVersionFilename);
+
+    void accept() override;
+
+private:
+    void updateWidgets();
+    void updateButtonStates();
+
+    QString _baseFilename;
+    QScopedPointer<Ui::ConflictDialog> _ui;
+    ConflictSolver *_solver;
+};
+
+} // namespace OCC
+
+#endif // CONFLICTDIALOG_H
diff --git a/src/gui/conflictdialog.ui b/src/gui/conflictdialog.ui
new file mode 100644 (file)
index 0000000..bd69c60
--- /dev/null
@@ -0,0 +1,305 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>OCC::ConflictDialog</class>
+ <widget class="QDialog" name="OCC::ConflictDialog">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>504</width>
+    <height>407</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Sync Conflict</string>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout_7">
+   <item>
+    <widget class="QLabel" name="conflictMessage">
+     <property name="text">
+      <string>Conflicting versions of %1.</string>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <layout class="QVBoxLayout" name="verticalLayout_8">
+     <property name="spacing">
+      <number>16</number>
+     </property>
+     <item>
+      <widget class="QLabel" name="label">
+       <property name="text">
+        <string>Which version of the file do you want to keep?&lt;br/&gt;If you select both versions, the local file will have a number added to its name.</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <layout class="QHBoxLayout" name="horizontalLayout_3">
+       <property name="spacing">
+        <number>16</number>
+       </property>
+       <item>
+        <layout class="QVBoxLayout" name="verticalLayout_5">
+         <item>
+          <widget class="QCheckBox" name="localVersionRadio">
+           <property name="text">
+            <string>Local version</string>
+           </property>
+          </widget>
+         </item>
+         <item>
+          <layout class="QHBoxLayout" name="horizontalLayout">
+           <item>
+            <layout class="QVBoxLayout" name="verticalLayout_4">
+             <item>
+              <widget class="QToolButton" name="localVersionButton">
+               <property name="toolTip">
+                <string>Click to open the file</string>
+               </property>
+               <property name="text">
+                <string/>
+               </property>
+               <property name="iconSize">
+                <size>
+                 <width>64</width>
+                 <height>64</height>
+                </size>
+               </property>
+               <property name="autoRaise">
+                <bool>true</bool>
+               </property>
+              </widget>
+             </item>
+             <item>
+              <spacer name="verticalSpacer_3">
+               <property name="orientation">
+                <enum>Qt::Vertical</enum>
+               </property>
+               <property name="sizeHint" stdset="0">
+                <size>
+                 <width>20</width>
+                 <height>40</height>
+                </size>
+               </property>
+              </spacer>
+             </item>
+            </layout>
+           </item>
+           <item>
+            <layout class="QVBoxLayout" name="verticalLayout_2" stretch="0,0,0,1">
+             <property name="spacing">
+              <number>0</number>
+             </property>
+             <item>
+              <widget class="QLabel" name="localVersionMtime">
+               <property name="text">
+                <string>today</string>
+               </property>
+              </widget>
+             </item>
+             <item>
+              <widget class="QLabel" name="localVersionSize">
+               <property name="text">
+                <string>0 byte</string>
+               </property>
+              </widget>
+             </item>
+             <item>
+              <widget class="QLabel" name="localVersionLink">
+               <property name="text">
+                <string>&lt;a href=&quot;%1&quot;&gt;Open local version&lt;/a&gt;</string>
+               </property>
+               <property name="openExternalLinks">
+                <bool>true</bool>
+               </property>
+              </widget>
+             </item>
+             <item>
+              <spacer name="verticalSpacer">
+               <property name="orientation">
+                <enum>Qt::Vertical</enum>
+               </property>
+               <property name="sizeHint" stdset="0">
+                <size>
+                 <width>20</width>
+                 <height>40</height>
+                </size>
+               </property>
+              </spacer>
+             </item>
+            </layout>
+           </item>
+           <item>
+            <spacer name="horizontalSpacer">
+             <property name="orientation">
+              <enum>Qt::Horizontal</enum>
+             </property>
+             <property name="sizeHint" stdset="0">
+              <size>
+               <width>40</width>
+               <height>20</height>
+              </size>
+             </property>
+            </spacer>
+           </item>
+          </layout>
+         </item>
+        </layout>
+       </item>
+       <item>
+        <layout class="QVBoxLayout" name="verticalLayout_6">
+         <item>
+          <widget class="QCheckBox" name="remoteVersionRadio">
+           <property name="text">
+            <string>Server version</string>
+           </property>
+          </widget>
+         </item>
+         <item>
+          <layout class="QHBoxLayout" name="horizontalLayout_2">
+           <item>
+            <layout class="QVBoxLayout" name="verticalLayout_3">
+             <item>
+              <widget class="QToolButton" name="remoteVersionButton">
+               <property name="toolTip">
+                <string>Click to open the file</string>
+               </property>
+               <property name="text">
+                <string/>
+               </property>
+               <property name="iconSize">
+                <size>
+                 <width>64</width>
+                 <height>64</height>
+                </size>
+               </property>
+               <property name="autoRaise">
+                <bool>true</bool>
+               </property>
+              </widget>
+             </item>
+             <item>
+              <spacer name="verticalSpacer_4">
+               <property name="orientation">
+                <enum>Qt::Vertical</enum>
+               </property>
+               <property name="sizeHint" stdset="0">
+                <size>
+                 <width>20</width>
+                 <height>40</height>
+                </size>
+               </property>
+              </spacer>
+             </item>
+            </layout>
+           </item>
+           <item>
+            <layout class="QVBoxLayout" name="verticalLayout" stretch="0,0,0,1">
+             <property name="spacing">
+              <number>0</number>
+             </property>
+             <item>
+              <widget class="QLabel" name="remoteVersionMtime">
+               <property name="text">
+                <string>today</string>
+               </property>
+              </widget>
+             </item>
+             <item>
+              <widget class="QLabel" name="remoteVersionSize">
+               <property name="text">
+                <string>0 byte</string>
+               </property>
+              </widget>
+             </item>
+             <item>
+              <widget class="QLabel" name="remoteVersionLink">
+               <property name="text">
+                <string>&lt;a href=&quot;%1&quot;&gt;Open server version&lt;/a&gt;</string>
+               </property>
+               <property name="openExternalLinks">
+                <bool>true</bool>
+               </property>
+              </widget>
+             </item>
+             <item>
+              <spacer name="verticalSpacer_2">
+               <property name="orientation">
+                <enum>Qt::Vertical</enum>
+               </property>
+               <property name="sizeHint" stdset="0">
+                <size>
+                 <width>20</width>
+                 <height>40</height>
+                </size>
+               </property>
+              </spacer>
+             </item>
+            </layout>
+           </item>
+           <item>
+            <spacer name="horizontalSpacer_2">
+             <property name="orientation">
+              <enum>Qt::Horizontal</enum>
+             </property>
+             <property name="sizeHint" stdset="0">
+              <size>
+               <width>40</width>
+               <height>20</height>
+              </size>
+             </property>
+            </spacer>
+           </item>
+          </layout>
+         </item>
+        </layout>
+       </item>
+      </layout>
+     </item>
+    </layout>
+   </item>
+   <item>
+    <widget class="QDialogButtonBox" name="buttonBox">
+     <property name="standardButtons">
+      <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>accepted()</signal>
+   <receiver>OCC::ConflictDialog</receiver>
+   <slot>accept()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>123</x>
+     <y>218</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>148</x>
+     <y>248</y>
+    </hint>
+   </hints>
+  </connection>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>rejected()</signal>
+   <receiver>OCC::ConflictDialog</receiver>
+   <slot>reject()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>181</x>
+     <y>215</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>240</x>
+     <y>254</y>
+    </hint>
+   </hints>
+  </connection>
+ </connections>
+</ui>