File indexing completed on 2024-05-26 05:14:41

0001 /*
0002     SPDX-FileCopyrightText: 2010 KDAB
0003     SPDX-FileContributor: Tobias Koenig <tokoe@kde.org>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 #include "conflictresolvedialog_p.h"
0009 
0010 #include "abstractdifferencesreporter.h"
0011 #include "differencesalgorithminterface.h"
0012 #include "typepluginloader_p.h"
0013 
0014 #include "shared/akranges.h"
0015 
0016 #include <QDesktopServices>
0017 #include <QDir>
0018 #include <QLabel>
0019 #include <QPushButton>
0020 #include <QScreen>
0021 #include <QTemporaryFile>
0022 #include <QVBoxLayout>
0023 #include <QWindow>
0024 
0025 #include <KColorScheme>
0026 #include <KLocalizedString>
0027 #include <KWindowConfig>
0028 #include <QDialogButtonBox>
0029 #include <QTextBrowser>
0030 
0031 using namespace Akonadi;
0032 using namespace AkRanges;
0033 
0034 static inline QString textToHTML(const QString &text)
0035 {
0036     return Qt::convertFromPlainText(text);
0037 }
0038 
0039 class HtmlDifferencesReporter : public AbstractDifferencesReporter
0040 {
0041 public:
0042     HtmlDifferencesReporter() = default;
0043 
0044     [[nodiscard]] QString toHtml() const
0045     {
0046         return header() + mContent + footer();
0047     }
0048 
0049     [[nodiscard]] QString plainText() const
0050     {
0051         return mTextContent;
0052     }
0053 
0054     void setPropertyNameTitle(const QString &title) override
0055     {
0056         mNameTitle = title;
0057     }
0058 
0059     void setLeftPropertyValueTitle(const QString &title) override
0060     {
0061         mLeftTitle = title;
0062     }
0063 
0064     void setRightPropertyValueTitle(const QString &title) override
0065     {
0066         mRightTitle = title;
0067     }
0068 
0069     void addProperty(Mode mode, const QString &name, const QString &leftValue, const QString &rightValue) override
0070     {
0071         switch (mode) {
0072         case NormalMode:
0073             mContent.append(QStringLiteral("<tr><td align=\"right\"><b>%1:</b></td><td>%2</td><td></td><td>%3</td></tr>")
0074                                 .arg(name, textToHTML(leftValue), textToHTML(rightValue)));
0075             mTextContent.append(QStringLiteral("%1:\n%2\n%3\n\n").arg(name, leftValue, rightValue));
0076             break;
0077         case ConflictMode:
0078             mContent.append(
0079                 QStringLiteral("<tr><td align=\"right\"><b>%1:</b></td><td bgcolor=\"#ff8686\">%2</td><td></td><td bgcolor=\"#ff8686\">%3</td></tr>")
0080                     .arg(name, textToHTML(leftValue), textToHTML(rightValue)));
0081             mTextContent.append(QStringLiteral("%1:\n%2\n%3\n\n").arg(name, leftValue, rightValue));
0082             break;
0083         case AdditionalLeftMode:
0084             mContent.append(QStringLiteral("<tr><td align=\"right\"><b>%1:</b></td><td bgcolor=\"#9cff83\">%2</td><td></td><td></td></tr>")
0085                                 .arg(name, textToHTML(leftValue)));
0086             mTextContent.append(QStringLiteral("%1:\n%2\n\n").arg(name, leftValue));
0087             break;
0088         case AdditionalRightMode:
0089             mContent.append(QStringLiteral("<tr><td align=\"right\"><b>%1:</b></td><td></td><td></td><td bgcolor=\"#9cff83\">%2</td></tr>")
0090                                 .arg(name, textToHTML(rightValue)));
0091             mTextContent.append(QStringLiteral("%1:\n%2\n\n").arg(name, rightValue));
0092             break;
0093         }
0094     }
0095 
0096 private:
0097     QString header() const
0098     {
0099         QString header = QStringLiteral("<html>");
0100         header += QStringLiteral("<body text=\"%1\" bgcolor=\"%2\">")
0101                       .arg(KColorScheme(QPalette::Active, KColorScheme::View).foreground().color().name(),
0102                            KColorScheme(QPalette::Active, KColorScheme::View).background().color().name());
0103         header += QLatin1StringView("<center><table>");
0104         header += QStringLiteral("<tr><th align=\"center\">%1</th><th align=\"center\">%2</th><td>&nbsp;</td><th align=\"center\">%3</th></tr>")
0105                       .arg(mNameTitle, mLeftTitle, mRightTitle);
0106 
0107         return header;
0108     }
0109 
0110     QString footer() const
0111     {
0112         return QStringLiteral(
0113             "</table></center>"
0114             "</body>"
0115             "</html>");
0116     }
0117 
0118     QString mContent;
0119     QString mNameTitle;
0120     QString mLeftTitle;
0121     QString mRightTitle;
0122     QString mTextContent;
0123 };
0124 
0125 static void compareItems(AbstractDifferencesReporter *reporter, const Akonadi::Item &localItem, const Akonadi::Item &otherItem)
0126 {
0127     if (localItem.modificationTime() != otherItem.modificationTime()) {
0128         reporter->addProperty(AbstractDifferencesReporter::ConflictMode,
0129                               i18n("Modification Time"),
0130                               QLocale().toString(localItem.modificationTime(), QLocale::ShortFormat),
0131                               QLocale().toString(otherItem.modificationTime(), QLocale::ShortFormat));
0132     }
0133 
0134     if (localItem.flags() != otherItem.flags()) {
0135         const auto toQString = [](const QByteArray &s) {
0136             return QString::fromUtf8(s);
0137         };
0138         const auto localFlags = localItem.flags() | Views::transform(toQString) | Actions::toQList;
0139         const auto otherFlags = otherItem.flags() | Views::transform(toQString) | Actions::toQList;
0140         reporter->addProperty(AbstractDifferencesReporter::ConflictMode,
0141                               i18n("Flags"),
0142                               localFlags.join(QLatin1StringView(", ")),
0143                               otherFlags.join(QLatin1StringView(", ")));
0144     }
0145 
0146     const auto toPair = [](Attribute *attr) {
0147         return std::pair{attr->type(), attr->serialized()};
0148     };
0149     const auto localAttributes = localItem.attributes() | Views::transform(toPair) | Actions::toQHash;
0150     const auto otherAttributes = otherItem.attributes() | Views::transform(toPair) | Actions::toQHash;
0151 
0152     if (localAttributes != otherAttributes) {
0153         for (const QByteArray &localKey : localAttributes) {
0154             if (!otherAttributes.contains(localKey)) {
0155                 reporter->addProperty(AbstractDifferencesReporter::AdditionalLeftMode,
0156                                       i18n("Attribute: %1", QString::fromUtf8(localKey)),
0157                                       QString::fromUtf8(localAttributes.value(localKey)),
0158                                       QString());
0159             } else {
0160                 const QByteArray localValue = localAttributes.value(localKey);
0161                 const QByteArray otherValue = otherAttributes.value(localKey);
0162                 if (localValue != otherValue) {
0163                     reporter->addProperty(AbstractDifferencesReporter::ConflictMode,
0164                                           i18n("Attribute: %1", QString::fromUtf8(localKey)),
0165                                           QString::fromUtf8(localValue),
0166                                           QString::fromUtf8(otherValue));
0167                 }
0168             }
0169         }
0170 
0171         for (const QByteArray &otherKey : otherAttributes) {
0172             if (!localAttributes.contains(otherKey)) {
0173                 reporter->addProperty(AbstractDifferencesReporter::AdditionalRightMode,
0174                                       i18n("Attribute: %1", QString::fromUtf8(otherKey)),
0175                                       QString(),
0176                                       QString::fromUtf8(otherAttributes.value(otherKey)));
0177             }
0178         }
0179     }
0180 }
0181 
0182 ConflictResolveDialog::ConflictResolveDialog(QWidget *parent)
0183     : QDialog(parent)
0184     , mResolveStrategy(ConflictHandler::UseBothItems)
0185 {
0186     setWindowTitle(i18nc("@title:window", "Conflict Resolution"));
0187 
0188     auto mainLayout = new QVBoxLayout(this);
0189     // Don't use QDialogButtonBox, order is very important (left on the left, right on the right)
0190     auto buttonLayout = new QHBoxLayout();
0191     auto takeLeftButton = new QPushButton(this);
0192     takeLeftButton->setText(i18nc("@action:button", "Take my version"));
0193     connect(takeLeftButton, &QPushButton::clicked, this, &ConflictResolveDialog::slotUseLocalItemChoosen);
0194     buttonLayout->addWidget(takeLeftButton);
0195     takeLeftButton->setObjectName(QLatin1StringView("takeLeftButton"));
0196 
0197     auto takeRightButton = new QPushButton(this);
0198     takeRightButton->setText(i18nc("@action:button", "Take their version"));
0199     takeRightButton->setObjectName(QLatin1StringView("takeRightButton"));
0200     connect(takeRightButton, &QPushButton::clicked, this, &ConflictResolveDialog::slotUseOtherItemChoosen);
0201     buttonLayout->addWidget(takeRightButton);
0202 
0203     auto keepBothButton = new QPushButton(this);
0204     keepBothButton->setText(i18nc("@action:button", "Keep both versions"));
0205     keepBothButton->setObjectName(QLatin1StringView("keepBothButton"));
0206     buttonLayout->addWidget(keepBothButton);
0207     connect(keepBothButton, &QPushButton::clicked, this, &ConflictResolveDialog::slotUseBothItemsChoosen);
0208 
0209     keepBothButton->setDefault(true);
0210 
0211     mView = new QTextBrowser(this);
0212     mView->setObjectName(QLatin1StringView("view"));
0213     mView->setOpenLinks(false);
0214 
0215     auto docuLabel =
0216         new QLabel(i18n("<qt>Your changes conflict with those made by someone else meanwhile.<br>"
0217                         "Unless one version can just be thrown away, you will have to integrate those changes manually.<br>"
0218                         "Click on <a href=\"opentexteditor\">\"Open text editor\"</a> to keep a copy of the texts, then select which version is most correct, "
0219                         "then re-open it and modify it again to add what's missing."));
0220     connect(docuLabel, &QLabel::linkActivated, this, &ConflictResolveDialog::slotOpenEditor);
0221     docuLabel->setContextMenuPolicy(Qt::NoContextMenu);
0222 
0223     docuLabel->setWordWrap(true);
0224     docuLabel->setObjectName(QLatin1StringView("doculabel"));
0225 
0226     mainLayout->addWidget(mView);
0227     mainLayout->addWidget(docuLabel);
0228     mainLayout->addLayout(buttonLayout);
0229 
0230     // default size is tiny, and there's usually lots of text, so make it much bigger
0231     create(); // ensure a window is created
0232     const QSize availableSize = windowHandle()->screen()->availableSize();
0233     windowHandle()->resize(static_cast<int>(availableSize.width() * 0.7), static_cast<int>(availableSize.height() * 0.5));
0234     KWindowConfig::restoreWindowSize(windowHandle(), KSharedConfig::openConfig()->group(QStringLiteral("ConflictResolveDialog")));
0235     resize(windowHandle()->size()); // workaround for QTBUG-40584
0236 }
0237 
0238 ConflictResolveDialog::~ConflictResolveDialog()
0239 {
0240     KConfigGroup group(KSharedConfig::openConfig()->group(QStringLiteral("ConflictResolveDialog")));
0241     KWindowConfig::saveWindowSize(windowHandle(), group);
0242 }
0243 
0244 void ConflictResolveDialog::setConflictingItems(const Akonadi::Item &localItem, const Akonadi::Item &otherItem)
0245 {
0246     mLocalItem = localItem;
0247     mOtherItem = otherItem;
0248 
0249     HtmlDifferencesReporter reporter;
0250     compareItems(&reporter, localItem, otherItem);
0251 
0252     if (mLocalItem.hasPayload() && mOtherItem.hasPayload()) {
0253         QObject *object = TypePluginLoader::objectForMimeTypeAndClass(localItem.mimeType(), localItem.availablePayloadMetaTypeIds());
0254         if (object) {
0255             DifferencesAlgorithmInterface *algorithm = qobject_cast<DifferencesAlgorithmInterface *>(object);
0256             if (algorithm) {
0257                 algorithm->compare(&reporter, localItem, otherItem);
0258                 mView->setHtml(reporter.toHtml());
0259                 mTextContent = reporter.plainText();
0260                 return;
0261             }
0262         }
0263 
0264         reporter.addProperty(HtmlDifferencesReporter::NormalMode,
0265                              i18n("Data"),
0266                              QString::fromUtf8(mLocalItem.payloadData()),
0267                              QString::fromUtf8(mOtherItem.payloadData()));
0268     }
0269 
0270     mView->setHtml(reporter.toHtml());
0271     mTextContent = reporter.plainText();
0272 }
0273 
0274 void ConflictResolveDialog::slotOpenEditor()
0275 {
0276     QTemporaryFile file(QDir::tempPath() + QStringLiteral("/akonadi-XXXXXX.txt"));
0277     if (file.open()) {
0278         file.setAutoRemove(false);
0279         file.write(mTextContent.toLocal8Bit());
0280         const QString fileName = file.fileName();
0281         file.close();
0282         QDesktopServices::openUrl(QUrl::fromLocalFile(fileName));
0283     }
0284 }
0285 
0286 ConflictHandler::ResolveStrategy ConflictResolveDialog::resolveStrategy() const
0287 {
0288     return mResolveStrategy;
0289 }
0290 
0291 void ConflictResolveDialog::slotUseLocalItemChoosen()
0292 {
0293     mResolveStrategy = ConflictHandler::UseLocalItem;
0294     accept();
0295 }
0296 
0297 void ConflictResolveDialog::slotUseOtherItemChoosen()
0298 {
0299     mResolveStrategy = ConflictHandler::UseOtherItem;
0300     accept();
0301 }
0302 
0303 void ConflictResolveDialog::slotUseBothItemsChoosen()
0304 {
0305     mResolveStrategy = ConflictHandler::UseBothItems;
0306     accept();
0307 }
0308 
0309 #include "moc_conflictresolvedialog_p.cpp"