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> </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"