Akonadi

conflictresolvedialog.cpp
1/*
2 SPDX-FileCopyrightText: 2010 KDAB
3 SPDX-FileContributor: Tobias Koenig <tokoe@kde.org>
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
31using namespace Akonadi;
32using namespace AkRanges;
33
34static inline QString textToHTML(const QString &text)
35{
36 return Qt::convertFromPlainText(text);
37}
38
39class HtmlDifferencesReporter : public AbstractDifferencesReporter
40{
41public:
42 HtmlDifferencesReporter() = default;
43
44 [[nodiscard]] QString toHtml() const
45 {
46 return header() + mContent + footer();
47 }
48
49 [[nodiscard]] 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;
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;
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
96private:
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 += QLatin1StringView("<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
125static 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(QLatin1StringView(", ")),
143 otherFlags.join(QLatin1StringView(", ")));
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
182ConflictResolveDialog::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(QLatin1StringView("takeLeftButton"));
196
197 auto takeRightButton = new QPushButton(this);
198 takeRightButton->setText(i18nc("@action:button", "Take their version"));
199 takeRightButton->setObjectName(QLatin1StringView("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(QLatin1StringView("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(QLatin1StringView("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(QLatin1StringView("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(QStringLiteral("ConflictResolveDialog")));
235 resize(windowHandle()->size()); // workaround for QTBUG-40584
236}
237
238ConflictResolveDialog::~ConflictResolveDialog()
239{
240 KConfigGroup group(KSharedConfig::openConfig()->group(QStringLiteral("ConflictResolveDialog")));
241 KWindowConfig::saveWindowSize(windowHandle(), group);
242}
243
244void 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
274void 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
286ConflictHandler::ResolveStrategy ConflictResolveDialog::resolveStrategy() const
287{
288 return mResolveStrategy;
289}
290
291void ConflictResolveDialog::slotUseLocalItemChoosen()
292{
293 mResolveStrategy = ConflictHandler::UseLocalItem;
294 accept();
295}
296
297void ConflictResolveDialog::slotUseOtherItemChoosen()
298{
299 mResolveStrategy = ConflictHandler::UseOtherItem;
300 accept();
301}
302
303void ConflictResolveDialog::slotUseBothItemsChoosen()
304{
305 mResolveStrategy = ConflictHandler::UseBothItems;
306 accept();
307}
308
309#include "moc_conflictresolvedialog_p.cpp"
An interface to report differences between two arbitrary objects.
virtual void addProperty(Mode mode, const QString &name, const QString &leftValue, const QString &rightValue)=0
Adds a new property entry to the table.
@ AdditionalLeftMode
The left column contains a property value that is not available in the right column.
@ NormalMode
The left and right column show the same property values.
@ ConflictMode
The left and right column show conflicting property values.
@ AdditionalRightMode
The right column contains a property value that is not available in the left column.
Provides interface for custom attributes for Entity.
Definition attribute.h:132
An interface to find out differences between two Akonadi objects.
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.
Represents a PIM item stored in Akonadi storage.
Definition item.h:101
QString mimeType() const
Returns the mime type of the item.
Definition item.cpp:331
Flags flags() const
Returns all flags of this item.
Definition item.cpp:175
QList< int > availablePayloadMetaTypeIds() const
Returns a list of metatype-ids, describing the different variants of payload that are currently conta...
Definition item.cpp:509
Attribute::List attributes() const
Returns a list of all attributes of the item.
Definition item.cpp:133
QDateTime modificationTime() const
Returns the timestamp of the last modification of this item.
Definition item.cpp:220
static KSharedConfig::Ptr openConfig(const QString &fileName=QString(), OpenFlags mode=FullConfig, QStandardPaths::StandardLocation type=QStandardPaths::GenericConfigLocation)
QString i18nc(const char *context, const char *text, const TYPE &arg...)
QString i18n(const char *text, const TYPE &arg...)
Helper integration between Akonadi and Qt.
char * toString(const EngineQuery &query)
QAction * create(GameStandardAction id, const QObject *recvr, const char *slot, QObject *parent)
QString name(StandardShortcut id)
KCONFIGGUI_EXPORT void saveWindowSize(const QWindow *window, KConfigGroup &config, KConfigGroup::WriteConfigFlags options=KConfigGroup::Normal)
KCONFIGGUI_EXPORT void restoreWindowSize(QWindow *window, const KConfigGroup &config)
void clicked(bool checked)
bool openUrl(const QUrl &url)
QString tempPath()
void linkActivated(const QString &link)
int height() const const
int width() const const
QString & append(QChar ch)
QString arg(Args &&... args) const const
QString fromUtf8(QByteArrayView str)
QString convertFromPlainText(const QString &plain, WhiteSpaceMode mode)
NoContextMenu
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
QUrl fromLocalFile(const QString &localFile)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:13:38 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.