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(i18nc("@label:textbox",
217 "<qt>Your changes conflict with those made by someone else meanwhile.<br>"
218 "Unless one version can just be thrown away, you will have to integrate those changes manually.<br>"
219 "Click on <a href=\"opentexteditor\">\"Open text editor\"</a> to keep a copy of the texts, then select which version is most correct, "
220 "then re-open it and modify it again to add what's missing."));
221 connect(docuLabel, &QLabel::linkActivated, this, &ConflictResolveDialog::slotOpenEditor);
222 docuLabel->setContextMenuPolicy(Qt::NoContextMenu);
223
224 docuLabel->setWordWrap(true);
225 docuLabel->setObjectName(QLatin1StringView("doculabel"));
226
227 mainLayout->addWidget(mView);
228 mainLayout->addWidget(docuLabel);
229 mainLayout->addLayout(buttonLayout);
230
231 // default size is tiny, and there's usually lots of text, so make it much bigger
232 create(); // ensure a window is created
233 const QSize availableSize = windowHandle()->screen()->availableSize();
234 windowHandle()->resize(static_cast<int>(availableSize.width() * 0.7), static_cast<int>(availableSize.height() * 0.5));
235 KWindowConfig::restoreWindowSize(windowHandle(), KSharedConfig::openConfig()->group(QStringLiteral("ConflictResolveDialog")));
236 resize(windowHandle()->size()); // workaround for QTBUG-40584
237}
238
239ConflictResolveDialog::~ConflictResolveDialog()
240{
241 KConfigGroup group(KSharedConfig::openConfig()->group(QStringLiteral("ConflictResolveDialog")));
243}
244
245void ConflictResolveDialog::setConflictingItems(const Akonadi::Item &localItem, const Akonadi::Item &otherItem)
246{
247 mLocalItem = localItem;
248 mOtherItem = otherItem;
249
250 HtmlDifferencesReporter reporter;
251 compareItems(&reporter, localItem, otherItem);
252
253 if (mLocalItem.hasPayload() && mOtherItem.hasPayload()) {
254 QObject *object = TypePluginLoader::objectForMimeTypeAndClass(localItem.mimeType(), localItem.availablePayloadMetaTypeIds());
255 if (object) {
256 DifferencesAlgorithmInterface *algorithm = qobject_cast<DifferencesAlgorithmInterface *>(object);
257 if (algorithm) {
258 algorithm->compare(&reporter, localItem, otherItem);
259 mView->setHtml(reporter.toHtml());
260 mTextContent = reporter.plainText();
261 return;
262 }
263 }
264
265 reporter.addProperty(HtmlDifferencesReporter::NormalMode,
266 i18n("Data"),
267 QString::fromUtf8(mLocalItem.payloadData()),
268 QString::fromUtf8(mOtherItem.payloadData()));
269 }
270
271 mView->setHtml(reporter.toHtml());
272 mTextContent = reporter.plainText();
273}
274
275void ConflictResolveDialog::slotOpenEditor()
276{
277 QTemporaryFile file(QDir::tempPath() + QStringLiteral("/akonadi-XXXXXX.txt"));
278 if (file.open()) {
279 file.setAutoRemove(false);
280 file.write(mTextContent.toLocal8Bit());
281 const QString fileName = file.fileName();
282 file.close();
284 }
285}
286
287ConflictHandler::ResolveStrategy ConflictResolveDialog::resolveStrategy() const
288{
289 return mResolveStrategy;
290}
291
292void ConflictResolveDialog::slotUseLocalItemChoosen()
293{
294 mResolveStrategy = ConflictHandler::UseLocalItem;
295 accept();
296}
297
298void ConflictResolveDialog::slotUseOtherItemChoosen()
299{
300 mResolveStrategy = ConflictHandler::UseOtherItem;
301 accept();
302}
303
304void ConflictResolveDialog::slotUseBothItemsChoosen()
305{
306 mResolveStrategy = ConflictHandler::UseBothItems;
307 accept();
308}
309
310#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:100
QString mimeType() const
Returns the mime type of the item.
Definition item.cpp:326
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:504
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)
QWindow * windowHandle(QObject *job)
QString name(StandardAction id)
QAction * create(StandardAction id, const Receiver *recvr, Func slot, QObject *parent, std::optional< Qt::ConnectionType > connectionType=std::nullopt)
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)
void resize(const QSize &newSize)
QScreen * screen() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Mon Nov 18 2024 12:08:30 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.