Akonadi

conflictresolvedialog.cpp
1 /*
2  SPDX-FileCopyrightText: 2010 KDAB
3  SPDX-FileContributor: Tobias Koenig <[email protected]>
4 
5  SPDX-License-Identifier: LGPL-2.0-or-later
6 */
7 
8 #include "conflictresolvedialog_p.h"
9 
10 #include "abstractdifferencesreporter.h"
11 #include "differencesalgorithminterface.h"
12 #include "typepluginloader_p.h"
13 
14 #include <shared/akranges.h>
15 
16 #include <QDesktopServices>
17 #include <QDir>
18 #include <QLabel>
19 #include <QPushButton>
20 #include <QScreen>
21 #include <QTemporaryFile>
22 #include <QVBoxLayout>
23 #include <QWindow>
24 
25 #include <KColorScheme>
26 #include <KLocalizedString>
27 #include <KWindowConfig>
28 #include <QDialogButtonBox>
29 #include <QTextBrowser>
30 
31 using namespace Akonadi;
32 using namespace AkRanges;
33 
34 static inline QString textToHTML(const QString &text)
35 {
36  return Qt::convertFromPlainText(text);
37 }
38 
39 class HtmlDifferencesReporter : public AbstractDifferencesReporter
40 {
41 public:
42  HtmlDifferencesReporter() = default;
43 
44  Q_REQUIRED_RESULT QString toHtml() const
45  {
46  return header() + mContent + footer();
47  }
48 
49  Q_REQUIRED_RESULT QString plainText() const
50  {
51  return mTextContent;
52  }
53 
54  void setPropertyNameTitle(const QString &title) override
55  {
56  mNameTitle = title;
57  }
58 
59  void setLeftPropertyValueTitle(const QString &title) override
60  {
61  mLeftTitle = title;
62  }
63 
64  void setRightPropertyValueTitle(const QString &title) override
65  {
66  mRightTitle = title;
67  }
68 
69  void addProperty(Mode mode, const QString &name, const QString &leftValue, const QString &rightValue) override
70  {
71  switch (mode) {
72  case NormalMode:
73  mContent.append(QStringLiteral("<tr><td align=\"right\"><b>%1:</b></td><td>%2</td><td></td><td>%3</td></tr>")
74  .arg(name, textToHTML(leftValue), textToHTML(rightValue)));
75  mTextContent.append(QStringLiteral("%1:\n%2\n%3\n\n").arg(name, leftValue, rightValue));
76  break;
77  case ConflictMode:
78  mContent.append(
79  QStringLiteral("<tr><td align=\"right\"><b>%1:</b></td><td bgcolor=\"#ff8686\">%2</td><td></td><td bgcolor=\"#ff8686\">%3</td></tr>")
80  .arg(name, textToHTML(leftValue), textToHTML(rightValue)));
81  mTextContent.append(QStringLiteral("%1:\n%2\n%3\n\n").arg(name, leftValue, rightValue));
82  break;
83  case AdditionalLeftMode:
84  mContent.append(QStringLiteral("<tr><td align=\"right\"><b>%1:</b></td><td bgcolor=\"#9cff83\">%2</td><td></td><td></td></tr>")
85  .arg(name, textToHTML(leftValue)));
86  mTextContent.append(QStringLiteral("%1:\n%2\n\n").arg(name, leftValue));
87  break;
88  case AdditionalRightMode:
89  mContent.append(QStringLiteral("<tr><td align=\"right\"><b>%1:</b></td><td></td><td></td><td bgcolor=\"#9cff83\">%2</td></tr>")
90  .arg(name, textToHTML(rightValue)));
91  mTextContent.append(QStringLiteral("%1:\n%2\n\n").arg(name, rightValue));
92  break;
93  }
94  }
95 
96 private:
97  QString header() const
98  {
99  QString header = QStringLiteral("<html>");
100  header += QStringLiteral("<body text=\"%1\" bgcolor=\"%2\">")
101  .arg(KColorScheme(QPalette::Active, KColorScheme::View).foreground().color().name(),
102  KColorScheme(QPalette::Active, KColorScheme::View).background().color().name());
103  header += QLatin1String("<center><table>");
104  header += QStringLiteral("<tr><th align=\"center\">%1</th><th align=\"center\">%2</th><td>&nbsp;</td><th align=\"center\">%3</th></tr>")
105  .arg(mNameTitle, mLeftTitle, mRightTitle);
106 
107  return header;
108  }
109 
110  QString footer() const
111  {
112  return QStringLiteral(
113  "</table></center>"
114  "</body>"
115  "</html>");
116  }
117 
118  QString mContent;
119  QString mNameTitle;
120  QString mLeftTitle;
121  QString mRightTitle;
122  QString mTextContent;
123 };
124 
125 static void compareItems(AbstractDifferencesReporter *reporter, const Akonadi::Item &localItem, const Akonadi::Item &otherItem)
126 {
127  if (localItem.modificationTime() != otherItem.modificationTime()) {
129  i18n("Modification Time"),
132  }
133 
134  if (localItem.flags() != otherItem.flags()) {
135  const auto toQString = [](const QByteArray &s) {
136  return QString::fromUtf8(s);
137  };
138  const auto localFlags = localItem.flags() | Views::transform(toQString) | Actions::toQList;
139  const auto otherFlags = otherItem.flags() | Views::transform(toQString) | Actions::toQList;
141  i18n("Flags"),
142  localFlags.join(QLatin1String(", ")),
143  otherFlags.join(QLatin1String(", ")));
144  }
145 
146  const auto toPair = [](Attribute *attr) {
147  return std::pair{attr->type(), attr->serialized()};
148  };
149  const auto localAttributes = localItem.attributes() | Views::transform(toPair) | Actions::toQHash;
150  const auto otherAttributes = otherItem.attributes() | Views::transform(toPair) | Actions::toQHash;
151 
152  if (localAttributes != otherAttributes) {
153  for (const QByteArray &localKey : localAttributes) {
154  if (!otherAttributes.contains(localKey)) {
156  i18n("Attribute: %1", QString::fromUtf8(localKey)),
157  QString::fromUtf8(localAttributes.value(localKey)),
158  QString());
159  } else {
160  const QByteArray localValue = localAttributes.value(localKey);
161  const QByteArray otherValue = otherAttributes.value(localKey);
162  if (localValue != otherValue) {
164  i18n("Attribute: %1", QString::fromUtf8(localKey)),
165  QString::fromUtf8(localValue),
166  QString::fromUtf8(otherValue));
167  }
168  }
169  }
170 
171  for (const QByteArray &otherKey : otherAttributes) {
172  if (!localAttributes.contains(otherKey)) {
174  i18n("Attribute: %1", QString::fromUtf8(otherKey)),
175  QString(),
176  QString::fromUtf8(otherAttributes.value(otherKey)));
177  }
178  }
179  }
180 }
181 
182 ConflictResolveDialog::ConflictResolveDialog(QWidget *parent)
183  : QDialog(parent)
184  , mResolveStrategy(ConflictHandler::UseBothItems)
185 {
186  setWindowTitle(i18nc("@title:window", "Conflict Resolution"));
187 
188  auto mainLayout = new QVBoxLayout(this);
189  // Don't use QDialogButtonBox, order is very important (left on the left, right on the right)
190  auto buttonLayout = new QHBoxLayout();
191  auto takeLeftButton = new QPushButton(this);
192  takeLeftButton->setText(i18nc("@action:button", "Take my version"));
193  connect(takeLeftButton, &QPushButton::clicked, this, &ConflictResolveDialog::slotUseLocalItemChoosen);
194  buttonLayout->addWidget(takeLeftButton);
195  takeLeftButton->setObjectName(QStringLiteral("takeLeftButton"));
196 
197  auto takeRightButton = new QPushButton(this);
198  takeRightButton->setText(i18nc("@action:button", "Take their version"));
199  takeRightButton->setObjectName(QStringLiteral("takeRightButton"));
200  connect(takeRightButton, &QPushButton::clicked, this, &ConflictResolveDialog::slotUseOtherItemChoosen);
201  buttonLayout->addWidget(takeRightButton);
202 
203  auto keepBothButton = new QPushButton(this);
204  keepBothButton->setText(i18nc("@action:button", "Keep both versions"));
205  keepBothButton->setObjectName(QStringLiteral("keepBothButton"));
206  buttonLayout->addWidget(keepBothButton);
207  connect(keepBothButton, &QPushButton::clicked, this, &ConflictResolveDialog::slotUseBothItemsChoosen);
208 
209  keepBothButton->setDefault(true);
210 
211  mView = new QTextBrowser(this);
212  mView->setObjectName(QStringLiteral("view"));
213  mView->setOpenLinks(false);
214 
215  auto docuLabel =
216  new QLabel(i18n("<qt>Your changes conflict with those made by someone else meanwhile.<br>"
217  "Unless one version can just be thrown away, you will have to integrate those changes manually.<br>"
218  "Click on <a href=\"opentexteditor\">\"Open text editor\"</a> to keep a copy of the texts, then select which version is most correct, "
219  "then re-open it and modify it again to add what's missing."));
220  connect(docuLabel, &QLabel::linkActivated, this, &ConflictResolveDialog::slotOpenEditor);
221  docuLabel->setContextMenuPolicy(Qt::NoContextMenu);
222 
223  docuLabel->setWordWrap(true);
224  docuLabel->setObjectName(QStringLiteral("doculabel"));
225 
226  mainLayout->addWidget(mView);
227  mainLayout->addWidget(docuLabel);
228  mainLayout->addLayout(buttonLayout);
229 
230  // default size is tiny, and there's usually lots of text, so make it much bigger
231  create(); // ensure a window is created
232  const QSize availableSize = windowHandle()->screen()->availableSize();
233  windowHandle()->resize(static_cast<int>(availableSize.width() * 0.7), static_cast<int>(availableSize.height() * 0.5));
234  KWindowConfig::restoreWindowSize(windowHandle(), KSharedConfig::openConfig()->group("ConflictResolveDialog"));
235  resize(windowHandle()->size()); // workaround for QTBUG-40584
236 }
237 
238 ConflictResolveDialog::~ConflictResolveDialog()
239 {
240  KConfigGroup group(KSharedConfig::openConfig()->group("ConflictResolveDialog"));
241  KWindowConfig::saveWindowSize(windowHandle(), group);
242 }
243 
244 void ConflictResolveDialog::setConflictingItems(const Akonadi::Item &localItem, const Akonadi::Item &otherItem)
245 {
246  mLocalItem = localItem;
247  mOtherItem = otherItem;
248 
249  HtmlDifferencesReporter reporter;
250  compareItems(&reporter, localItem, otherItem);
251 
252  if (mLocalItem.hasPayload() && mOtherItem.hasPayload()) {
253  QObject *object = TypePluginLoader::objectForMimeTypeAndClass(localItem.mimeType(), localItem.availablePayloadMetaTypeIds());
254  if (object) {
255  DifferencesAlgorithmInterface *algorithm = qobject_cast<DifferencesAlgorithmInterface *>(object);
256  if (algorithm) {
257  algorithm->compare(&reporter, localItem, otherItem);
258  mView->setHtml(reporter.toHtml());
259  mTextContent = reporter.plainText();
260  return;
261  }
262  }
263 
264  reporter.addProperty(HtmlDifferencesReporter::NormalMode,
265  i18n("Data"),
266  QString::fromUtf8(mLocalItem.payloadData()),
267  QString::fromUtf8(mOtherItem.payloadData()));
268  }
269 
270  mView->setHtml(reporter.toHtml());
271  mTextContent = reporter.plainText();
272 }
273 
274 void ConflictResolveDialog::slotOpenEditor()
275 {
276  QTemporaryFile file(QDir::tempPath() + QStringLiteral("/akonadi-XXXXXX.txt"));
277  if (file.open()) {
278  file.setAutoRemove(false);
279  file.write(mTextContent.toLocal8Bit());
280  const QString fileName = file.fileName();
281  file.close();
283  }
284 }
285 
286 ConflictHandler::ResolveStrategy ConflictResolveDialog::resolveStrategy() const
287 {
288  return mResolveStrategy;
289 }
290 
291 void ConflictResolveDialog::slotUseLocalItemChoosen()
292 {
293  mResolveStrategy = ConflictHandler::UseLocalItem;
294  accept();
295 }
296 
297 void ConflictResolveDialog::slotUseOtherItemChoosen()
298 {
299  mResolveStrategy = ConflictHandler::UseOtherItem;
300  accept();
301 }
302 
303 void ConflictResolveDialog::slotUseBothItemsChoosen()
304 {
305  mResolveStrategy = ConflictHandler::UseBothItems;
306  accept();
307 }
308 
309 #include "moc_conflictresolvedialog_p.cpp"
QString fromUtf8(const char *str, int size)
Flags flags() const
Returns all flags of this item.
Definition: item.cpp:175
void clicked(bool checked)
QDateTime modificationTime() const
Returns the timestamp of the last modification of this item.
Definition: item.cpp:220
virtual void compare(AbstractDifferencesReporter *reporter, const Akonadi::Item &leftItem, const Akonadi::Item &rightItem)=0
Calculates the differences between two Akonadi objects and reports them to a reporter object.
NoContextMenu
bool openUrl(const QUrl &url)
Provides interface for custom attributes for Entity.
Definition: attribute.h:124
int width() const const
static KSharedConfig::Ptr openConfig(const QString &fileName=QString(), OpenFlags mode=FullConfig, QStandardPaths::StandardLocation type=QStandardPaths::GenericConfigLocation)
void linkActivated(const QString &link)
QString tempPath()
QString mimeType() const
Returns the mime type of the item.
Definition: item.cpp:331
Attribute::List attributes() const
Returns a list of all attributes of the item.
Definition: item.cpp:133
virtual void addProperty(Mode mode, const QString &name, const QString &leftValue, const QString &rightValue)=0
Adds a new property entry to the table.
QString i18n(const char *text, const TYPE &arg...)
int height() const const
char * toString(const T &value)
QUrl fromLocalFile(const QString &localFile)
QAction * create(StandardGameAction id, const QObject *recvr, const char *slot, QObject *parent)
QString convertFromPlainText(const QString &plain, Qt::WhiteSpaceMode mode)
An interface to find out differences between two Akonadi objects.
@ ConflictMode
The left and right column show conflicting property values.
QString arg(qlonglong a, int fieldWidth, int base, QChar fillChar) const const
KCONFIGGUI_EXPORT void restoreWindowSize(QWindow *window, const KConfigGroup &config)
QString name(StandardShortcut id)
@ AdditionalRightMode
The right column contains a property value that is not available in the left column.
An interface to report differences between two arbitrary objects.
QString i18nc(const char *context, const char *text, const TYPE &arg...)
@ AdditionalLeftMode
The left column contains a property value that is not available in the right column.
QVector< int > availablePayloadMetaTypeIds() const
Returns a list of metatype-ids, describing the different variants of payload that are currently conta...
Definition: item.cpp:496
QString & append(QChar ch)
Represents a PIM item stored in Akonadi storage.
Definition: item.h:104
Helper integration between Akonadi and Qt.
KCONFIGGUI_EXPORT void saveWindowSize(const QWindow *window, KConfigGroup &config, KConfigGroup::WriteConfigFlags options=KConfigGroup::Normal)
This file is part of the KDE documentation.
Documentation copyright © 1996-2022 The KDE developers.
Generated on Sat Jul 2 2022 06:41:47 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.