File indexing completed on 2024-04-28 05:49:32

0001 /*
0002     SPDX-License-Identifier: LGPL-2.0-only
0003     SPDX-FileCopyrightText: 2004 Anders Lund <anders@alweb.dk>
0004     SPDX-FileCopyrightText: 2023 Waqar Ahmed <waqar.17a@gmail.com>
0005 */
0006 
0007 #include "katemwmodonhddialog.h"
0008 
0009 #include "diffwidget.h"
0010 #include "gitprocess.h"
0011 #include "hostprocess.h"
0012 #include "kateapp.h"
0013 #include "katedocmanager.h"
0014 #include "katemainwindow.h"
0015 #include "ktexteditor_utils.h"
0016 
0017 #include <KLocalizedString>
0018 #include <KMessageBox>
0019 #include <KTextEditor/Document>
0020 
0021 #include <QDir>
0022 #include <QHeaderView>
0023 #include <QLabel>
0024 #include <QPushButton>
0025 #include <QStandardPaths>
0026 #include <QStyle>
0027 #include <QTemporaryFile>
0028 #include <QTextStream>
0029 #include <QTreeWidget>
0030 #include <QTreeWidgetItem>
0031 
0032 class KateDocItem : public QTreeWidgetItem
0033 {
0034 public:
0035     KateDocItem(KTextEditor::Document *doc, const QString &status, QTreeWidget *tw)
0036         : QTreeWidgetItem(tw)
0037         , document(doc)
0038     {
0039         setText(0, doc->url().toString());
0040         setText(1, status);
0041         if (!doc->isModified()) {
0042             setCheckState(0, Qt::Checked);
0043         } else {
0044             setCheckState(0, Qt::Unchecked);
0045         }
0046     }
0047     QPointer<KTextEditor::Document> document;
0048 };
0049 
0050 KateMwModOnHdDialog::KateMwModOnHdDialog(const QList<KTextEditor::Document *> &docs, QWidget *parent, const char *name)
0051     : QDialog(parent)
0052     , m_blockAddDocument(false)
0053 {
0054     setWindowTitle(i18n("Documents Modified on Disk"));
0055     setObjectName(QString::fromLatin1(name));
0056     setModal(true);
0057 
0058     QVBoxLayout *mainLayout = new QVBoxLayout;
0059     setLayout(mainLayout);
0060 
0061     // Message
0062     QHBoxLayout *hb = new QHBoxLayout;
0063     mainLayout->addLayout(hb);
0064 
0065     // dialog text
0066     QLabel *icon = new QLabel(this);
0067     hb->addWidget(icon);
0068     icon->setPixmap(QIcon::fromTheme(QStringLiteral("dialog-warning")).pixmap(style()->pixelMetric(QStyle::PM_LargeIconSize, nullptr, this)));
0069 
0070     QLabel *t = new QLabel(i18n("<qt>The documents listed below have changed on disk.<p>Select one "
0071                                 "or more at once, and press an action button until the list is empty.</p></qt>"),
0072                            this);
0073     hb->addWidget(t);
0074     hb->setStretchFactor(t, 1000);
0075 
0076     // Document list
0077     docsTreeWidget = new QTreeWidget(this);
0078     mainLayout->addWidget(docsTreeWidget);
0079     QStringList header;
0080     header << i18n("Filename") << i18n("Status on Disk");
0081     docsTreeWidget->setHeaderLabels(header);
0082     docsTreeWidget->setSelectionMode(QAbstractItemView::SingleSelection);
0083     docsTreeWidget->setRootIsDecorated(false);
0084 
0085     m_stateTexts << QString() << i18n("Modified") << i18n("Created") << i18n("Deleted");
0086     for (auto &doc : docs) {
0087         const auto docInfo = KateApp::self()->documentManager()->documentInfo(doc);
0088         if (!docInfo) {
0089             qWarning() << "Unexpected null doc info";
0090             continue;
0091         }
0092         new KateDocItem(doc, m_stateTexts[static_cast<uint>(docInfo->modifiedOnDiscReason)], docsTreeWidget);
0093 
0094         // ensure proper cleanups to avoid dangling pointers, we can arrive here multiple times, use unique connection
0095         connect(doc, &KTextEditor::Document::destroyed, this, &KateMwModOnHdDialog::removeDocument, Qt::UniqueConnection);
0096     }
0097     docsTreeWidget->header()->setStretchLastSection(false);
0098     docsTreeWidget->header()->setSectionResizeMode(0, QHeaderView::Stretch);
0099     docsTreeWidget->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
0100 
0101     connect(docsTreeWidget, &QTreeWidget::currentItemChanged, this, &KateMwModOnHdDialog::slotSelectionChanged);
0102     connect(docsTreeWidget, &QTreeWidget::itemChanged, this, &KateMwModOnHdDialog::slotCheckedFilesChanged);
0103 
0104     // Diff line
0105     hb = new QHBoxLayout;
0106     mainLayout->addLayout(hb);
0107 
0108     btnDiff = new QPushButton(QIcon::fromTheme(QStringLiteral("document-preview")), i18n("&View Difference"), this);
0109     btnDiff->setWhatsThis(i18n("Calculates the difference between the editor contents and the disk file for the selected document."));
0110     hb->addWidget(btnDiff);
0111     connect(btnDiff, &QPushButton::clicked, this, &KateMwModOnHdDialog::slotDiff);
0112 
0113     // Dialog buttons
0114     dlgButtons = new QDialogButtonBox(this);
0115     mainLayout->addWidget(dlgButtons);
0116 
0117     QPushButton *ignoreButton = new QPushButton(QIcon::fromTheme(QStringLiteral("dialog-warning")), i18n("&Ignore Changes"));
0118     ignoreButton->setToolTip(i18n("Remove modified flag from selected documents"));
0119     dlgButtons->addButton(ignoreButton, QDialogButtonBox::RejectRole);
0120     connect(ignoreButton, &QPushButton::clicked, this, &KateMwModOnHdDialog::slotIgnore);
0121 
0122     QPushButton *overwriteButton = new QPushButton;
0123     KGuiItem::assign(overwriteButton, KStandardGuiItem::overwrite());
0124     overwriteButton->setToolTip(i18n("Overwrite selected documents, discarding disk changes"));
0125     dlgButtons->addButton(overwriteButton, QDialogButtonBox::DestructiveRole);
0126     connect(overwriteButton, &QPushButton::clicked, this, &KateMwModOnHdDialog::slotOverwrite);
0127 
0128     QPushButton *reloadButton = new QPushButton(QIcon::fromTheme(QStringLiteral("view-refresh")), i18n("&Reload"));
0129     reloadButton->setDefault(true);
0130     reloadButton->setToolTip(i18n("Reload selected documents from disk"));
0131     dlgButtons->addButton(reloadButton, QDialogButtonBox::DestructiveRole);
0132     connect(reloadButton, &QPushButton::clicked, this, &KateMwModOnHdDialog::slotReload);
0133 
0134     // butons will only be enabled when items are checked. see slotCheckedFilesChanged()
0135     dlgButtons->setEnabled(false);
0136     slotCheckedFilesChanged(nullptr, 0);
0137 
0138     slotSelectionChanged(nullptr, nullptr);
0139 }
0140 
0141 KateMwModOnHdDialog::~KateMwModOnHdDialog()
0142 {
0143     // ensure no cleanup of documents during shutdown
0144     disconnect();
0145 
0146     KateMainWindow::unsetModifiedOnDiscDialogIfIf(this);
0147 
0148     // if there are any living processes, disconnect them now before we get destroyed
0149     const auto children = findChildren<QProcess *>();
0150     for (QProcess *child : children) {
0151         disconnect(child, nullptr, nullptr, nullptr);
0152     }
0153 }
0154 
0155 void KateMwModOnHdDialog::slotIgnore()
0156 {
0157     handleSelected(Ignore);
0158 }
0159 
0160 void KateMwModOnHdDialog::slotOverwrite()
0161 {
0162     handleSelected(Overwrite);
0163 }
0164 
0165 void KateMwModOnHdDialog::slotReload()
0166 {
0167     handleSelected(Reload);
0168 }
0169 
0170 void KateMwModOnHdDialog::handleSelected(int action)
0171 {
0172     // don't alter the treewidget via addDocument, we modify it here!
0173     m_blockAddDocument = true;
0174 
0175     // collect all items we can remove
0176     std::vector<QTreeWidgetItem *> itemsToDelete;
0177     for (QTreeWidgetItemIterator it(docsTreeWidget); *it; ++it) {
0178         KateDocItem *item = static_cast<KateDocItem *>(*it);
0179         auto docInfo = KateApp::self()->documentManager()->documentInfo(item->document);
0180         if (!item->document || !docInfo) {
0181             itemsToDelete.push_back(item);
0182             continue;
0183         }
0184 
0185         if (item->checkState(0) == Qt::Checked) {
0186             KTextEditor::Document::ModifiedOnDiskReason reason = docInfo->modifiedOnDiscReason;
0187             bool success = true;
0188             item->document->setModifiedOnDisk(KTextEditor::Document::OnDiskUnmodified);
0189 
0190             switch (action) {
0191             case Overwrite:
0192                 success = item->document->save();
0193                 if (!success) {
0194                     KMessageBox::error(this, i18n("Could not save the document \n'%1'", item->document->url().toString()));
0195                 }
0196                 break;
0197 
0198             case Reload:
0199                 item->document->documentReload();
0200                 break;
0201 
0202             default:
0203                 break;
0204             }
0205 
0206             if (success) {
0207                 itemsToDelete.push_back(item);
0208             } else {
0209                 item->document->setModifiedOnDisk(reason);
0210             }
0211         }
0212     }
0213 
0214     // remove the marked items, addDocument is blocked, this is save!
0215     qDeleteAll(itemsToDelete);
0216 
0217     // any documents left unhandled?
0218     if (!docsTreeWidget->topLevelItemCount()) {
0219         accept();
0220     }
0221 
0222     // update the dialog buttons
0223     slotCheckedFilesChanged(nullptr, 0);
0224 
0225     // allow addDocument again
0226     m_blockAddDocument = false;
0227 }
0228 
0229 void KateMwModOnHdDialog::slotSelectionChanged(QTreeWidgetItem *current, QTreeWidgetItem *)
0230 {
0231     KateDocItem *currentDocItem = static_cast<KateDocItem *>(current);
0232     if (currentDocItem && currentDocItem->document) {
0233         auto *docInfo = KateApp::self()->documentManager()->documentInfo(currentDocItem->document);
0234         // set the diff button enabled
0235         btnDiff->setEnabled(docInfo && docInfo->modifiedOnDiscReason != KTextEditor::Document::OnDiskDeleted);
0236     }
0237 }
0238 
0239 void KateMwModOnHdDialog::slotCheckedFilesChanged(QTreeWidgetItem *, int column)
0240 {
0241     if (column != 0) {
0242         // we only need to react when the checkbox (in column 0) changes
0243         return;
0244     }
0245 
0246     for (QTreeWidgetItemIterator it(docsTreeWidget); *it; ++it) {
0247         KateDocItem *item = static_cast<KateDocItem *>(*it);
0248         if (item->checkState(0) == Qt::Checked) {
0249             // at least 1 item is checked so we enable the buttons
0250             dlgButtons->setEnabled(true);
0251             return;
0252         }
0253     }
0254 
0255     // no itmes checked, disable all buttons
0256     dlgButtons->setEnabled(false);
0257 }
0258 
0259 // ### the code below is slightly modified from kdelibs/kate/part/katedialogs,
0260 // class KateModOnHdPrompt.
0261 void KateMwModOnHdDialog::slotDiff()
0262 {
0263     if (!btnDiff->isEnabled()) { // diff button already pressed, proc not finished yet
0264         return;
0265     }
0266 
0267     if (!docsTreeWidget->currentItem()) {
0268         return;
0269     }
0270 
0271     QPointer<KTextEditor::Document> doc = (static_cast<KateDocItem *>(docsTreeWidget->currentItem()))->document;
0272     auto docInfo = KateApp::self()->documentManager()->documentInfo(doc);
0273     if (!doc || !docInfo) {
0274         return;
0275     }
0276 
0277     // don't try to diff a deleted file
0278     if (docInfo->modifiedOnDiscReason == KTextEditor::Document::OnDiskDeleted) {
0279         return;
0280     }
0281 
0282     auto f = new QTemporaryFile(this);
0283     f->open();
0284     f->write(doc->text().toUtf8().constData());
0285     f->flush();
0286 
0287     QProcess *p = new QProcess(this);
0288     QStringList args = {QStringLiteral("diff"), QStringLiteral("--no-color"), QStringLiteral("--no-index")};
0289     args << f->fileName();
0290     args << doc->url().toLocalFile();
0291     setupGitProcess(*p, QDir::currentPath(), args);
0292 
0293     connect(p, &QProcess::finished, this, [p, f, this, doc](int, QProcess::ExitStatus) {
0294         f->deleteLater();
0295         p->deleteLater();
0296         slotGitDiffDone(p, doc);
0297     });
0298     setCursor(Qt::WaitCursor);
0299     btnDiff->setEnabled(false);
0300     startHostProcess(*p);
0301 }
0302 
0303 void KateMwModOnHdDialog::slotGitDiffDone(QProcess *p, KTextEditor::Document *doc)
0304 {
0305     setCursor(Qt::ArrowCursor);
0306     slotSelectionChanged(docsTreeWidget->currentItem(), nullptr);
0307 
0308     const QProcess::ExitStatus es = p->exitStatus();
0309 
0310     if (es != QProcess::NormalExit) {
0311         KMessageBox::error(this,
0312                            i18n("The diff command failed. Please make sure that "
0313                                 "git is installed and in your PATH."),
0314                            i18n("Error Creating Diff"));
0315         return;
0316     }
0317 
0318     const QByteArray out = p->readAllStandardOutput();
0319     if (!out.isEmpty()) {
0320         DiffParams params;
0321         QString s = QFileInfo(doc->url().toLocalFile()).fileName() + i18n("[OLD]");
0322         QString t = QFileInfo(doc->url().toLocalFile()).fileName() + i18n("[NEW]");
0323         params.tabTitle = s + QStringLiteral("..") + t;
0324         params.workingDir = p->workingDirectory();
0325         auto mw = static_cast<KateMainWindow *>(parent());
0326         DiffWidgetManager::openDiff(out, params, mw->wrapper());
0327     }
0328 }
0329 
0330 void KateMwModOnHdDialog::addDocument(KTextEditor::Document *doc)
0331 {
0332     // guard this e.g. during handleSelected
0333     if (m_blockAddDocument || !KateApp::self()->documentManager()->documentInfo(doc)) {
0334         return;
0335     }
0336 
0337     // avoid double occurrences, we want a fresh item
0338     removeDocument(doc);
0339 
0340     uint reason = static_cast<uint>(KateApp::self()->documentManager()->documentInfo(doc)->modifiedOnDiscReason);
0341     if (reason) {
0342         new KateDocItem(doc, m_stateTexts[reason], docsTreeWidget);
0343         connect(doc, &KTextEditor::Document::destroyed, this, &KateMwModOnHdDialog::removeDocument, Qt::UniqueConnection);
0344     }
0345 
0346     if (!docsTreeWidget->topLevelItemCount()) {
0347         accept();
0348     }
0349 }
0350 
0351 void KateMwModOnHdDialog::removeDocument(QObject *doc)
0352 {
0353     for (QTreeWidgetItemIterator it(docsTreeWidget); *it; ++it) {
0354         KateDocItem *item = static_cast<KateDocItem *>(*it);
0355         if (item->document == static_cast<KTextEditor::Document *>(doc)) {
0356             disconnect(item->document, nullptr, this, nullptr);
0357             delete item;
0358             break;
0359         }
0360     }
0361 }
0362 
0363 void KateMwModOnHdDialog::keyPressEvent(QKeyEvent *event)
0364 {
0365     if (event->modifiers() == 0) {
0366         if (event->key() == Qt::Key_Escape) {
0367             event->accept();
0368             return;
0369         }
0370     }
0371     QDialog::keyPressEvent(event);
0372 }
0373 
0374 void KateMwModOnHdDialog::closeEvent(QCloseEvent *e)
0375 {
0376     if (!docsTreeWidget->topLevelItemCount()) {
0377         QDialog::closeEvent(e);
0378     } else {
0379         e->ignore();
0380     }
0381 }
0382 
0383 #include "moc_katemwmodonhddialog.cpp"