diff --git a/svn/CMakeLists.txt b/svn/CMakeLists.txt --- a/svn/CMakeLists.txt +++ b/svn/CMakeLists.txt @@ -4,6 +4,8 @@ set(fileviewsvnplugin_SRCS fileviewsvnplugin.cpp + svncommands.cpp + svncommitdialog.cpp ) kconfig_add_kcfg_files(fileviewsvnplugin_SRCS diff --git a/svn/fileviewsvnplugin.h b/svn/fileviewsvnplugin.h --- a/svn/fileviewsvnplugin.h +++ b/svn/fileviewsvnplugin.h @@ -47,10 +47,17 @@ signals: /// Invokes m_showUpdatesAction->setChecked(checked) on the UI thread. void setShowUpdatesChecked(bool checked); + + /** + * Is emitted if current SVN directory status got updated. Not necessarily means + * it's changed. Emitted right after #endRetrieval(). + */ + void versionInfoUpdated(); + private slots: void updateFiles(); void showLocalChanges(); - void commitFiles(); + void commitDialog(); void addFiles(); void removeFiles(); void revertFiles(); @@ -60,6 +67,11 @@ void slotShowUpdatesToggled(bool checked); + void revertFiles(const QStringList& filesPath); + void diffFile(const QString& filePath); + void addFiles(const QStringList& filesPath); + void commitFiles(const QStringList& context, const QString& msg); + private: /** * Executes the command "svn {svnCommand}" for the files that have been diff --git a/svn/fileviewsvnplugin.cpp b/svn/fileviewsvnplugin.cpp --- a/svn/fileviewsvnplugin.cpp +++ b/svn/fileviewsvnplugin.cpp @@ -44,6 +44,9 @@ #include #include +#include "svncommitdialog.h" +#include "svncommands.h" + K_PLUGIN_FACTORY(FileViewSvnPluginFactory, registerPlugin();) FileViewSvnPlugin::FileViewSvnPlugin(QObject* parent, const QList& args) : @@ -83,7 +86,7 @@ m_commitAction->setIcon(QIcon::fromTheme("svn-commit")); m_commitAction->setText(i18nc("@item:inmenu", "SVN Commit...")); connect(m_commitAction, SIGNAL(triggered()), - this, SLOT(commitFiles())); + this, SLOT(commitDialog())); m_addAction = new QAction(this); m_addAction->setIcon(QIcon::fromTheme("list-add")); @@ -163,6 +166,7 @@ case 'A': version = AddedVersion; break; case 'D': version = RemovedVersion; break; case 'C': version = ConflictingVersion; break; + case '!': version = MissingVersion; break; default: if (filePath.contains('*')) { version = UpdateRequiredVersion; @@ -205,6 +209,7 @@ void FileViewSvnPlugin::endRetrieval() { + emit versionInfoUpdated(); } KVersionControlPlugin::ItemVersion FileViewSvnPlugin::itemVersion(const KFileItem& item) const @@ -300,7 +305,7 @@ void FileViewSvnPlugin::updateFiles() { - execSvnCommand("update", QStringList(), + execSvnCommand(QLatin1String("update"), QStringList(), i18nc("@info:status", "Updating SVN repository..."), i18nc("@info:status", "Update of SVN repository failed."), i18nc("@info:status", "Updated SVN repository.")); @@ -349,118 +354,28 @@ } } -void FileViewSvnPlugin::commitFiles() +void FileViewSvnPlugin::commitDialog() { - QDialog dialog(0, Qt::Dialog); - - QVBoxLayout* boxLayout = new QVBoxLayout(&dialog); - - boxLayout->addWidget(new QLabel(i18nc("@label", "Description:"), - &dialog)); - QPlainTextEdit* editor = new QPlainTextEdit(&dialog); - boxLayout->addWidget(editor, 1); - - QFrame* line = new QFrame(&dialog); - line->setFrameShape(QFrame::HLine); - line->setFrameShadow(QFrame::Sunken); - boxLayout->addWidget(line); - - const QStringList header = { i18nc("@title:column", "Path"), - i18nc("@title:column", "Status") }; - const int columnPath = 0; - const int columnStatus = 1; - QTableWidget *changes = new QTableWidget(m_versionInfoHash.size(), header.size(), &dialog); - changes->setHorizontalHeaderLabels(header); - changes->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); - changes->horizontalHeader()->setSectionResizeMode(columnStatus, QHeaderView::ResizeToContents); - changes->verticalHeader()->setVisible(false); - changes->setSortingEnabled(false); - - QHash::const_iterator it = m_versionInfoHash.cbegin(); - for ( int row = 0 ; it != m_versionInfoHash.cend(); ++it, ++row ) { - QTableWidgetItem *path = new QTableWidgetItem( it.key() ); - QTableWidgetItem *status = new QTableWidgetItem; - - path->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable); - status->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable); - - changes->setItem(row, columnPath, path); - changes->setItem(row, columnStatus, status); - - switch(it.value()) { - case UnversionedVersion: - status->setText( i18nc("@item:intable", "Unversioned") ); - break; - case LocallyModifiedVersion: - status->setText( i18nc("@item:intable", "Modified") ); - break; - case AddedVersion: - status->setText( i18nc("@item:intable", "Added") ); - break; - case RemovedVersion: - status->setText( i18nc("@item:intable", "Deleted") ); - break; - case ConflictingVersion: - status->setText( i18nc("@item:intable", "Conflict") ); - break; - case MissingVersion: - status->setText( i18nc("@item:intable", "Missing") ); - break; - case UpdateRequiredVersion: - status->setText( i18nc("@item:intable", "Update required") ); - break; - default: - // For SVN normaly we shouldn't be here with: - // NormalVersion, LocallyModifiedUnstagedVersion, IgnoredVersion. - // 'default' is for any future changes in ItemVersion enum. - qWarning() << QString("Unknown SVN status for item %1, ItemVersion = %2").arg(it.key()).arg(it.value()); - status->setText(""); + QStringList context; + if (!m_contextDir.isEmpty()) { + context << m_contextDir; + } else { + for (const auto &i : m_contextItems) { + context << i.localPath(); } } - // Sort by status: unversioned is at the bottom. - changes->sortByColumn(columnStatus, Qt::AscendingOrder); - boxLayout->addWidget(changes, 3); - - dialog.setWindowTitle(i18nc("@title:window", "SVN Commit")); - auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog); - connect(buttonBox, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); - connect(buttonBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); - auto okButton = buttonBox->button(QDialogButtonBox::Ok); - - okButton->setDefault(true); - okButton->setText(i18nc("@action:button", "Commit")); - boxLayout->addWidget(buttonBox); - - KConfigGroup dialogConfig(KSharedConfig::openConfig("dolphinrc"), - "SvnCommitDialog"); - dialog.winId(); // Workaround for QTBUG-40584, line 1/2. See KWindowConfig::restoreWindowSize() docs. - KWindowConfig::restoreWindowSize(dialog.windowHandle(), dialogConfig); - dialog.resize(dialog.windowHandle()->size()); // Workaround for QTBUG-40584, line 2/2. See KWindowConfig::restoreWindowSize() docs. - - if (dialog.exec() == QDialog::Accepted) { - // Write the commit description into a temporary file, so - // that it can be read by the command "svn commit -F". The temporary - // file must stay alive until slotOperationCompleted() is invoked and will - // be destroyed when the version plugin is destructed. - if (!m_tempFile.open()) { - emit errorMessage(i18nc("@info:status", "Commit of SVN changes failed.")); - return; - } - QTextStream out(&m_tempFile); - const QString fileName = m_tempFile.fileName(); - out << editor->toPlainText(); - m_tempFile.close(); - - QStringList arguments; - arguments << "-F" << fileName; - execSvnCommand("commit", arguments, - i18nc("@info:status", "Committing SVN changes..."), - i18nc("@info:status", "Commit of SVN changes failed."), - i18nc("@info:status", "Committed SVN changes.")); - } + SvnCommitDialog *svnCommitDialog = new SvnCommitDialog(&m_versionInfoHash, context); + + connect(this, &FileViewSvnPlugin::versionInfoUpdated, svnCommitDialog, &SvnCommitDialog::refreshChangesList); - KWindowConfig::saveWindowSize(dialog.windowHandle(), dialogConfig, KConfigBase::Persistent); + connect(svnCommitDialog, &SvnCommitDialog::revertFiles, this, QOverload::of(&FileViewSvnPlugin::revertFiles)); + connect(svnCommitDialog, &SvnCommitDialog::diffFile, this, &FileViewSvnPlugin::diffFile); + connect(svnCommitDialog, &SvnCommitDialog::addFiles, this, QOverload::of(&FileViewSvnPlugin::addFiles)); + connect(svnCommitDialog, &SvnCommitDialog::commit, this, &FileViewSvnPlugin::commitFiles); + + svnCommitDialog->setAttribute(Qt::WA_DeleteOnClose); + svnCommitDialog->show(); } void FileViewSvnPlugin::addFiles() @@ -520,6 +435,88 @@ emit itemVersionsChanged(); } +void FileViewSvnPlugin::revertFiles(const QStringList& filesPath) +{ + for (const auto &i : qAsConst(filesPath)) { + m_contextItems.append( QUrl::fromLocalFile(i) ); + } + m_contextDir.clear(); + + execSvnCommand(QLatin1String("revert"), QStringList() << filesPath, + i18nc("@info:status", "Reverting changes to file..."), + i18nc("@info:status", "Revert file failed."), + i18nc("@info:status", "File reverted.")); +} + +void FileViewSvnPlugin::diffFile(const QString& filePath) +{ + // For a diff we will export last known file local revision from a remote and compare. We will + // not use basic SVN action 'svn diff --extensions -U ' because we should count + // lines or set maximum number for this. + // With a maximum number (2147483647) 'svn diff' starts to work slowly. + + QTemporaryFile *file = new QTemporaryFile(this); + // TODO: Calling a blocking operation: with a slow connection this might take some time. Work + // should be done in a separate thread or process. + if (!SVNCommands::exportLocalFile(filePath, SVNCommands::localRevision(filePath), file)) { + emit errorMessage(i18nc("@info:status", "Could not show local SVN changes for a file: could not get file.")); + file->deleteLater(); + } + + const bool started = QProcess::startDetached( + QLatin1String("kompare"), + QStringList { + file->fileName(), + filePath + } + ); + if (!started) { + emit errorMessage(i18nc("@info:status", "Could not show local SVN changes: could not start kompare.")); + file->deleteLater(); + } +} + +void FileViewSvnPlugin::addFiles(const QStringList& filesPath) +{ + for (const auto &i : qAsConst(filesPath)) { + m_contextItems.append( QUrl::fromLocalFile(i) ); + } + m_contextDir.clear(); + + addFiles(); +} + +void FileViewSvnPlugin::commitFiles(const QStringList& context, const QString& msg) +{ + // Write the commit description into a temporary file, so + // that it can be read by the command "svn commit -F". The temporary + // file must stay alive until slotOperationCompleted() is invoked and will + // be destroyed when the version plugin is destructed. + if (!m_tempFile.open()) { + emit errorMessage(i18nc("@info:status", "Commit of SVN changes failed.")); + return; + } + + QTextStream out(&m_tempFile); + const QString fileName = m_tempFile.fileName(); + out << msg; + m_tempFile.close(); + + QStringList arguments; + arguments << context << "-F" << fileName; + + // Lets clear m_contextDir and m_contextItems variables: we will pass everything in arguments. + // This is needed because startSvnCommandProcess() uses only one QString for svn transaction at + // a time but we want to commit everything. + m_contextDir.clear(); + m_contextItems.clear(); + + execSvnCommand(QLatin1String("commit"), arguments, + i18nc("@info:status", "Committing SVN changes..."), + i18nc("@info:status", "Commit of SVN changes failed."), + i18nc("@info:status", "Committed SVN changes.")); +} + void FileViewSvnPlugin::execSvnCommand(const QString& svnCommand, const QStringList& arguments, const QString& infoMsg, @@ -548,10 +545,14 @@ arguments << m_contextDir; m_contextDir.clear(); } else { - const KFileItem item = m_contextItems.takeLast(); - arguments << item.localPath(); - // the remaining items of m_contextItems will be executed - // after the process has finished (see slotOperationFinished()) + // If m_contextDir is empty and m_contextItems is empty then all svn arguments are in + // m_arguments (for example see commitFiles()). + if (!m_contextItems.isEmpty()) { + const KFileItem item = m_contextItems.takeLast(); + arguments << item.localPath(); + // the remaining items of m_contextItems will be executed + // after the process has finished (see slotOperationFinished()) + } } m_process.start(program, arguments); } diff --git a/svn/svncommands.h b/svn/svncommands.h new file mode 100644 --- /dev/null +++ b/svn/svncommands.h @@ -0,0 +1,81 @@ +/*************************************************************************** + * Copyright (C) 2019-2020 * + * by Nikolai Krasheninnikov * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * + ***************************************************************************/ + +#ifndef SVNCOMMANDS_H +#define SVNCOMMANDS_H + +#include +#include + +#include + +class QTemporaryFile; +class QFileDevice; + +/** + * \brief SVN support functions. + * + * \note All functions are synchronous i.e. blocking. Each of them waits for svn process to finish. + */ +class SVNCommands { +public: + /** + * Returns file \p filePath local revision. Local revision means last known file revision, not + * last SVN repository revision. + * + * \return Local revision, 0 in case of error. + * + * \note This function uses only local SVN data without connection to a remote so it's fast. + */ + static ulong localRevision(const QString& filePath); + + /** + * For file \p filePath return its full remote repository URL path. + * + * \return Remote path, empty QString in case of error. + * + * \note This function uses only local SVN data without connection to a remote so it's fast. + */ + static QString remoteItemUrl(const QString& filePath); + + /** + * Export remote URL \p remoteUrl at revision \p rev to a file \p file. File should already be + * opened or ready to be opened. Freeing resources is up to the caller. + * + * \return True if export success, false either. + * + * \note \p file should already be created with \p new. + */ + static bool exportRemoteFile(const QString& remoteUrl, ulong rev, QFileDevice *file); + static bool exportRemoteFile(const QString& remoteUrl, ulong rev, QTemporaryFile *file); + + /** + * Export local file \p filePath at revision \p rev to a file \p file. File should already be + * opened or ready to be opened. Freeing resources is up to the caller. + * + * \return True if export success, false either. + * + * \note \p file should already be created with \p new. + */ + static bool exportLocalFile(const QString& filePath, ulong rev, QFileDevice *file); + static bool exportLocalFile(const QString& filePath, ulong rev, QTemporaryFile *file); +}; + +#endif // SVNCOMMANDS_H diff --git a/svn/svncommands.cpp b/svn/svncommands.cpp new file mode 100644 --- /dev/null +++ b/svn/svncommands.cpp @@ -0,0 +1,165 @@ +/*************************************************************************** + * Copyright (C) 2019-2020 * + * by Nikolai Krasheninnikov * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * + ***************************************************************************/ + +#include "svncommands.h" + +#include +#include +#include +#include + +namespace { + +// Helper function: returns template file name for QTemporaryFile. +QString templateFileName(const QString& url, ulong rev) +{ + const QString tmpFileName = url.section('/', -1); + + return QDir::tempPath() + QString("/%1.r%2.XXXXXX").arg(tmpFileName).arg(rev); +} + +} + +ulong SVNCommands::localRevision(const QString& filePath) +{ + QProcess process; + + process.start( + QLatin1String("svn"), + QStringList { + QStringLiteral("info"), + QStringLiteral("--show-item"), + QStringLiteral("last-changed-revision"), + filePath + } + ); + + if (!process.waitForFinished() || process.exitCode() != 0) { + return 0; + } + + QTextStream stream(&process); + ulong revision = 0; + stream >> revision; + + if (stream.status() == QTextStream::Ok) { + return revision; + } else { + return 0; + } +} + +QString SVNCommands::remoteItemUrl(const QString& filePath) +{ + QProcess process; + + process.start( + QLatin1String("svn"), + QStringList { + QStringLiteral("info"), + QStringLiteral("--show-item"), + QStringLiteral("url"), + filePath + } + ); + + if (!process.waitForFinished() || process.exitCode() != 0) { + return 0; + } + + QTextStream stream(&process); + QString url; + stream >> url; + + if (stream.status() == QTextStream::Ok) { + return url; + } else { + return QString(); + } +} + +bool SVNCommands::exportRemoteFile(const QString& remoteUrl, ulong rev, QFileDevice *file) +{ + if (file == nullptr) { + return false; + } + if (!file->isOpen() && !file->open(QIODevice::Truncate | QIODevice::WriteOnly | QIODevice::Text)) { + return false; + } + + QProcess process; + + process.start( + QLatin1String("svn"), + QStringList { + QStringLiteral("export"), + QStringLiteral("--force"), + QStringLiteral("-r%1").arg(rev), + remoteUrl, + file->fileName() + } + ); + + if (!process.waitForFinished() || process.exitCode() != 0) { + return false; + } else { + return true; + } +} + +bool SVNCommands::exportRemoteFile(const QString& remoteUrl, ulong rev, QTemporaryFile *file) +{ + if (file == nullptr) { + return false; + } + + file->setFileTemplate( templateFileName(remoteUrl, rev) ); + + return exportRemoteFile(remoteUrl, rev, dynamic_cast(file)); +} + +bool SVNCommands::exportLocalFile(const QString& filePath, ulong rev, QFileDevice *file) +{ + if (file == nullptr) { + return false; + } + + const QString fileUrl = remoteItemUrl(filePath); + if (fileUrl.isEmpty()) { + return false; + } + + if (!exportRemoteFile(fileUrl, rev, file)) { + return false; + } else { + return true; + } +} + +bool SVNCommands::exportLocalFile(const QString& filePath, ulong rev, QTemporaryFile *file) +{ + if (file == nullptr) { + return false; + } + + file->setFileTemplate( templateFileName(filePath, rev) ); + + return exportLocalFile(filePath, rev, dynamic_cast(file)); +} diff --git a/svn/svncommitdialog.h b/svn/svncommitdialog.h new file mode 100644 --- /dev/null +++ b/svn/svncommitdialog.h @@ -0,0 +1,84 @@ +/*************************************************************************** + * Copyright (C) 2019-2020 * + * by Nikolai Krasheninnikov * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * + ***************************************************************************/ + +#ifndef SVNCOMMITDIALOG_H +#define SVNCOMMITDIALOG_H + +#include +#include + +#include + +class QPlainTextEdit; +class QTableWidget; + +/** + * \brief SVN Commit dialog class. + */ +class SvnCommitDialog : public QDialog { + Q_OBJECT +public: + /** + * Constructor. + * + * \param versionInfo Pointer to a current full list of SVN plugin changed files. This pointer + * is saved internaly and used for changes list updates. + * \param context List of dirs and files for which this dialog is shown. Every directory entry + * means "every file in this directory", file stands for a file. This context is used + * like a filter for \p versionInfo to make changes list. + * \param parent Parent widget. + */ + SvnCommitDialog(const QHash *versionInfo, const QStringList& context, QWidget *parent = nullptr); + + virtual ~SvnCommitDialog() override; + +signals: + /** + * Is emitted for SVN commit. + * + * \param context List of files and dirs for which this commit is applied. This not necessarily + * the same list which passed to the class contructor because some files might be, for + * example, reverted. + * \param msg Commit message. + */ + void commit(const QStringList& context, const QString& msg); + + void revertFiles(const QStringList& filesPath); + void diffFile(const QString& filePath); + void addFiles(const QStringList& filesPath); + +public slots: + void refreshChangesList(); + void show(); + +private slots: + void contextMenu(const QPoint& pos); + +private: + const QHash *m_versionInfoHash; + const QStringList m_context; + QPlainTextEdit *m_editor; + QTableWidget *m_changes; + QAction *m_actRevertFile; + QAction *m_actDiffFile; + QAction *m_actAddFile; +}; + +#endif // SVNCOMMITDIALOG_H diff --git a/svn/svncommitdialog.cpp b/svn/svncommitdialog.cpp new file mode 100644 --- /dev/null +++ b/svn/svncommitdialog.cpp @@ -0,0 +1,304 @@ +/*************************************************************************** + * Copyright (C) 2019-2020 * + * by Nikolai Krasheninnikov * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * + ***************************************************************************/ + +#include "svncommitdialog.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace { + +// Helper function: returns true if str starts with any string in a list. +bool startsWith(const QStringList &list, const QString &str) +{ + for (const auto &i : qAsConst(list)) { + if (str.startsWith(i)) { + return true; + } + } + + return false; +} + +// Helper function: makes a new list from an existing one and a hashTable. It combines all the list +// records which exists in hashTable. Existence means hashTable entry starts with a list entry. +QStringList makeContext(const QStringList &list, const QHash *hashTable) +{ + QStringList ret; + + for (const auto &i : qAsConst(list)) { + for ( auto it = hashTable->cbegin(); it != hashTable->cend(); ++it ) { + if (it.key().startsWith(i)) { + ret.append(i); + break; + } + } + } + + return ret; +} + +} + +struct svnInfo_t { + QString filePath; + KVersionControlPlugin::ItemVersion fileVersion; +}; +Q_DECLARE_METATYPE(svnInfo_t); + +enum columns_t { + columnPath, + columnStatus +}; + +const QStringList tableHeader = { i18nc("@title:column", "Path"), + i18nc("@title:column", "Status") }; + +SvnCommitDialog::SvnCommitDialog(const QHash *versionInfo, const QStringList& context, QWidget *parent) : + QDialog(parent), + m_versionInfoHash(versionInfo), + m_context(context) +{ + Q_ASSERT(versionInfo); + Q_ASSERT(!context.empty()); + + /* + * Setup UI. + */ + QVBoxLayout* boxLayout = new QVBoxLayout(this); + + boxLayout->addWidget(new QLabel(i18nc("@label", "Description:"), this)); + m_editor = new QPlainTextEdit(this); + boxLayout->addWidget(m_editor, 1); + + QFrame* line = new QFrame(this); + line->setFrameShape(QFrame::HLine); + line->setFrameShadow(QFrame::Sunken); + boxLayout->addWidget(line); + + m_changes = new QTableWidget(this); + boxLayout->addWidget(m_changes, 3); + + auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); + // The more appropriate role is QDialogButtonBox::ActionRole but we use QDialogButtonBox::ResetRole + // because of QDialogButtonBox automatic button layout (button is separated from others). + auto refreshButton = buttonBox->addButton(i18nc("@action:button", "Refresh"), QDialogButtonBox::ResetRole); + refreshButton->setIcon(QIcon::fromTheme("view-refresh")); + + auto okButton = buttonBox->button(QDialogButtonBox::Ok); + okButton->setDefault(true); + okButton->setText(i18nc("@action:button", "Commit")); + boxLayout->addWidget(buttonBox); + + /* + * Add actions, establish connections. + */ + m_actRevertFile = new QAction(i18nc("@item:inmenu", "Revert"), this); + m_actRevertFile->setIcon(QIcon::fromTheme("document-revert")); + connect(m_actRevertFile, &QAction::triggered, [this] () { + const QString filePath = m_actRevertFile->data().value().filePath; + emit revertFiles(QStringList() << filePath); + } ); + + m_actDiffFile = new QAction(i18nc("@item:inmenu", "Show changes"), this); + m_actDiffFile->setIcon(QIcon::fromTheme("view-split-left-right")); + connect(m_actDiffFile, &QAction::triggered, [this] () { + const QString filePath = m_actDiffFile->data().value().filePath; + emit diffFile(filePath); + } ); + + m_actAddFile = new QAction(i18nc("@item:inmenu", "Add file"), this); + m_actAddFile->setIcon(QIcon::fromTheme("list-add")); + connect(m_actAddFile, &QAction::triggered, [this] () { + const QString filePath = m_actAddFile->data().value().filePath; + emit addFiles(QStringList() << filePath); + } ); + + connect(buttonBox, &QDialogButtonBox::accepted, [this] () { + // Form a new context list from an existing one and a possibly modified m_versionInfoHash (some + // files from original context might no longer be in m_versionInfoHash). + QStringList context = makeContext(m_context, m_versionInfoHash); + emit commit(context, m_editor->toPlainText()); + QDialog::accept(); + } ); + connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); + connect(refreshButton, &QPushButton::clicked, this, &SvnCommitDialog::refreshChangesList); + + connect(m_changes, &QWidget::customContextMenuRequested, this, &SvnCommitDialog::contextMenu); + + QShortcut *refreshShortcut = new QShortcut(QKeySequence::Refresh, this, SLOT(refreshChangesList())); + refreshShortcut->setAutoRepeat(false); + + /* + * Additional setup. + */ + setWindowTitle(i18nc("@title:window", "SVN Commit")); + + m_changes->setColumnCount(tableHeader.size()); + m_changes->setHorizontalHeaderLabels(tableHeader); + m_changes->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); + m_changes->horizontalHeader()->setSectionResizeMode(columnStatus, QHeaderView::ResizeToContents); + m_changes->verticalHeader()->setVisible(false); + m_changes->setSortingEnabled(false); + m_changes->setSelectionMode(QAbstractItemView::SingleSelection); + m_changes->setSelectionBehavior(QAbstractItemView::SelectRows); + m_changes->setContextMenuPolicy(Qt::CustomContextMenu); + + refreshChangesList(); +} + +SvnCommitDialog::~SvnCommitDialog() +{ + KConfigGroup dialogConfig(KSharedConfig::openConfig("dolphinrc"), "SvnCommitDialog"); + KWindowConfig::saveWindowSize(windowHandle(), dialogConfig, KConfigBase::Persistent); +} + +void SvnCommitDialog::refreshChangesList() +{ + // Remove all the contents. + m_changes->clearContents(); + m_changes->setRowCount(0); + + auto it = m_versionInfoHash->cbegin(); + for ( int row = 0 ; it != m_versionInfoHash->cend(); ++it ) { + // If current item is not in a context list we skip it. Each file must be in a context dir + // or be in a context itself. + if (!startsWith(m_context, it.key())) { + continue; + } + + QTableWidgetItem *path = new QTableWidgetItem( it.key() ); + QTableWidgetItem *status = new QTableWidgetItem; + + path->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable); + status->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable); + + m_changes->insertRow(row); + m_changes->setItem(row, columnPath, path); + m_changes->setItem(row, columnStatus, status); + row++; + + svnInfo_t info { it.key(), it.value() }; + path->setData(Qt::UserRole, QVariant::fromValue(info)); + status->setData(Qt::UserRole, QVariant::fromValue(info)); + + switch(it.value()) { + case KVersionControlPlugin::UnversionedVersion: + status->setText( i18nc("@item:intable", "Unversioned") ); + break; + case KVersionControlPlugin::LocallyModifiedVersion: + status->setText( i18nc("@item:intable", "Modified") ); + break; + case KVersionControlPlugin::AddedVersion: + status->setText( i18nc("@item:intable", "Added") ); + break; + case KVersionControlPlugin::RemovedVersion: + status->setText( i18nc("@item:intable", "Deleted") ); + break; + case KVersionControlPlugin::ConflictingVersion: + status->setText( i18nc("@item:intable", "Conflict") ); + break; + case KVersionControlPlugin::MissingVersion: + status->setText( i18nc("@item:intable", "Missing") ); + break; + case KVersionControlPlugin::UpdateRequiredVersion: + status->setText( i18nc("@item:intable", "Update required") ); + break; + default: + // For SVN normaly we shouldn't be here with: + // NormalVersion, LocallyModifiedUnstagedVersion, IgnoredVersion. + // 'default' is for any future changes in ItemVersion enum. + qWarning() << QString("Unknown SVN status for item %1, ItemVersion = %2").arg(it.key()).arg(it.value()); + status->setText(""); + } + } + + // Sort by status: unversioned is at the bottom. + m_changes->sortByColumn(columnStatus, Qt::AscendingOrder); +} + +void SvnCommitDialog::show() +{ + QWidget::show(); + + // Restore window size after show() for workaround for QTBUG-40584. See KWindowConfig::restoreWindowSize() docs. + KConfigGroup dialogConfig(KSharedConfig::openConfig("dolphinrc"), "SvnCommitDialog"); + KWindowConfig::restoreWindowSize(windowHandle(), dialogConfig); +} + +void SvnCommitDialog::contextMenu(const QPoint& pos) +{ + QTableWidgetItem *item = m_changes->item( m_changes->currentRow(), 0 ); + if (item == nullptr) { + return; + } + + const QVariant data = item->data(Qt::UserRole); + m_actRevertFile->setData( data ); + m_actDiffFile->setData( data ); + m_actAddFile->setData( data ); + + m_actRevertFile->setEnabled(false); + m_actDiffFile->setEnabled(false); + m_actAddFile->setEnabled(false); + + const svnInfo_t info = data.value(); + switch(info.fileVersion) { + case KVersionControlPlugin::UnversionedVersion: + m_actAddFile->setEnabled(true); + break; + case KVersionControlPlugin::LocallyModifiedVersion: + m_actRevertFile->setEnabled(true); + m_actDiffFile->setEnabled(true); + break; + case KVersionControlPlugin::AddedVersion: + case KVersionControlPlugin::RemovedVersion: + m_actRevertFile->setEnabled(true); + break; + case KVersionControlPlugin::MissingVersion: + m_actRevertFile->setEnabled(true); + break; + default: + // For this items we don't show any menu: return now. + return; + } + + QMenu *menu = new QMenu(this); + menu->addAction(m_actAddFile); + menu->addAction(m_actRevertFile); + menu->addAction(m_actDiffFile); + + // Adjust popup menu position for QTableWidget header height. + const QPoint popupPoint = QPoint(pos.x(), pos.y() + m_changes->horizontalHeader()->height()); + menu->exec( m_changes->mapToGlobal(popupPoint) ); +}