diff --git a/addons/filetree/katefiletree.cpp b/addons/filetree/katefiletree.cpp --- a/addons/filetree/katefiletree.cpp +++ b/addons/filetree/katefiletree.cpp @@ -420,6 +420,10 @@ void KateFileTree::slotCopyFilename() { KTextEditor::Document *doc = model()->data(m_indexContextMenu, KateFileTreeModel::DocumentRole).value(); + + // TODO: the following code was improved in kate/katefileactions.cpp and should be reused here + // (make sure that the mentioned bug 381052 does not reappear) + if (doc) { // ensure we prefer native separators, bug 381052 if (doc->url().isLocalFile()) { @@ -432,6 +436,9 @@ void KateFileTree::slotRenameFile() { KTextEditor::Document *doc = model()->data(m_indexContextMenu, KateFileTreeModel::DocumentRole).value(); + + // TODO: the following code was improved in kate/katefileactions.cpp and should be reused here + if (!doc) { return; } @@ -676,6 +683,8 @@ { KTextEditor::Document *doc = model()->data(m_indexContextMenu, KateFileTreeModel::DocumentRole).value(); + // TODO: the following code was improved in kate/katefileactions.cpp and should be reused here + if (!doc) { return; } diff --git a/addons/project/kateprojecttreeviewcontextmenu.cpp b/addons/project/kateprojecttreeviewcontextmenu.cpp --- a/addons/project/kateprojecttreeviewcontextmenu.cpp +++ b/addons/project/kateprojecttreeviewcontextmenu.cpp @@ -76,7 +76,7 @@ * Copy Path */ QAction *copyAction = menu.addAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18n("Copy File Path")); - + /** * Handle "open with", * find correct mimetype to query for possible applications diff --git a/kate/CMakeLists.txt b/kate/CMakeLists.txt --- a/kate/CMakeLists.txt +++ b/kate/CMakeLists.txt @@ -20,6 +20,7 @@ kateconfigdialog.cpp kateconfigplugindialogpage.cpp katedocmanager.cpp + katefileactions.cpp katemainwindow.cpp katepluginmanager.cpp kateviewmanager.cpp diff --git a/kate/data/kateui.rc b/kate/data/kateui.rc --- a/kate/data/kateui.rc +++ b/kate/data/kateui.rc @@ -1,6 +1,6 @@ - + &File @@ -20,6 +20,14 @@ + + + + + + + + diff --git a/kate/katefileactions.h b/kate/katefileactions.h new file mode 100644 --- /dev/null +++ b/kate/katefileactions.h @@ -0,0 +1,91 @@ +/* This file is part of the KDE project + * + * Copyright (C) 2018 Gregor Mi + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) version 3, or any + * later version accepted by the membership of KDE e.V. (or its + * successor approved by the membership of KDE e.V.), which shall + * act as a proxy defined in Section 6 of version 3 of the license. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . +**/ + +#ifndef KATE_FILEACTIONS_H +#define KATE_FILEACTIONS_H + +#include +#include + +#include + +class QWidget; +namespace KTextEditor +{ + class Document; +} + +namespace KateFileActions +{ + /** + * Copies the file path to clipboard. + * If the document has no file, the clipboard will be emptied. + */ + void copyFilePathToClipboard(KTextEditor::Document* document); + + /** + * Tries to open and highlight the underlying url in the filemanager + */ + void openContainingFolder(KTextEditor::Document* document); + + /** + * Shows a Rename dialog to rename the file associated with the document. + * The document will be closed an reopened. + * + * Nothing is done if the document is nullptr or has no associated file. + */ + void renameDocumentFile(QWidget* parent, KTextEditor::Document* document); + + void openFilePropertiesDialog(KTextEditor::Document* document); + + /** + * Asks the user if the file should really be deleted. If yes, the file + * is deleted from disk and the document closed. + * + * Nothing is done if the document is nullptr or has no associated file. + */ + void deleteDocumentFile(QWidget* parent, KTextEditor::Document* document); + + /** + * @returns a list of supported diff tools (names of the executables) + */ + QStringList supportedDiffTools(); + + /** + * Runs an external program to compare the underlying files of two given documents. + * + * @param diffExecutable tested to work with "kdiff3", "kompare", and "meld" + * (@see supportedDiffTools()) + * + * The parameters documentA and documentB must not be nullptr. Otherwise an assertion fails. + * + * If documentA or documentB have an empty url, + * then an empty string is passed to the diff program instead of a local file path. + * + * @returns true if program was started successfully; otherwise false + * (which can mean the program is not installed) + * + * IDEA for later: compare with unsaved buffer data instead of underlying file + */ + bool compareWithExternalProgram(KTextEditor::Document* documentA, KTextEditor::Document* documentB, const QString& diffExecutable); +} + +#endif diff --git a/kate/katefileactions.cpp b/kate/katefileactions.cpp new file mode 100644 --- /dev/null +++ b/kate/katefileactions.cpp @@ -0,0 +1,171 @@ +/* This file is part of the KDE project + * + * Copyright (C) 2018 Gregor Mi + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) version 3, or any + * later version accepted by the membership of KDE e.V. (or its + * successor approved by the membership of KDE e.V.), which shall + * act as a proxy defined in Section 6 of version 3 of the license. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . +**/ + +#include "katefileactions.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +void KateFileActions::copyFilePathToClipboard(KTextEditor::Document* doc) +{ + QApplication::clipboard()->setText(doc->url().toDisplayString(QUrl::PreferLocalFile)); +} + +void KateFileActions::openContainingFolder(KTextEditor::Document* doc) +{ + KIO::highlightInFileManager({doc->url()}); +} + +void KateFileActions::openFilePropertiesDialog(KTextEditor::Document* doc) +{ + KFileItem fileItem(doc->url()); + QDialog* dlg = new KPropertiesDialog(fileItem); + dlg->setAttribute(Qt::WA_DeleteOnClose); + dlg->show(); +} + +void KateFileActions::renameDocumentFile(QWidget* parent, KTextEditor::Document* doc) +{ + // TODO: code was copied and adapted from ../addons/filetree/katefiletree.cpp + // (-> DUPLICATE CODE, the new code here should be also used there!) + + if (!doc) { + return; + } + + const QUrl oldFileUrl = doc->url(); + + if (oldFileUrl.isEmpty()) { // NEW + return; + } + + const QString oldFileName = doc->url().fileName(); + bool ok = false; + QString newFileName = QInputDialog::getText(parent, // ADAPTED + i18n("Rename file"), i18n("New file name"), QLineEdit::Normal, oldFileName, &ok); + if (!ok) { + return; + } + + QUrl newFileUrl = oldFileUrl.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash); + newFileUrl.setPath(newFileUrl.path() + QLatin1Char('/') + newFileName); + + if (!newFileUrl.isValid()) { + return; + } + + if (!doc->closeUrl()) { + return; + } + + doc->waitSaveComplete(); + + KIO::CopyJob* job = KIO::move(oldFileUrl, newFileUrl); + QSharedPointer sc(new QMetaObject::Connection()); + auto success = [doc, sc] (KIO::Job*, const QUrl&, const QUrl &realNewFileUrl, const QDateTime&, bool, bool) { + doc->openUrl(realNewFileUrl); + doc->documentSavedOrUploaded(doc, true); + QObject::disconnect(*sc); + }; + *sc = parent->connect(job, &KIO::CopyJob::copyingDone, doc, success); + + if (!job->exec()) { + KMessageBox::sorry(parent, i18n("File \"%1\" could not be moved to \"%2\"", oldFileUrl.toDisplayString(), newFileUrl.toDisplayString())); + doc->openUrl(oldFileUrl); + } +} + +void KateFileActions::deleteDocumentFile(QWidget* parent, KTextEditor::Document* doc) +{ + // TODO: code was copied and adapted from ../addons/filetree/katefiletree.cpp + // (-> DUPLICATE CODE, the new code here should be also used there!) + + if (!doc) { + return; + } + + const auto&& url = doc->url(); + + if (url.isEmpty()) { // NEW + return; + } + + bool go = (KMessageBox::warningContinueCancel(parent, + i18n("Do you really want to delete file \"%1\"?", url.toDisplayString()), + i18n("Delete file"), + KStandardGuiItem::yes(), KStandardGuiItem::no(), QLatin1String("filetreedeletefile") + ) == KMessageBox::Continue); + + if (!go) { + return; + } + + if (!KTextEditor::Editor::instance()->application()->closeDocument(doc)) { + return; // no extra message, the internals of ktexteditor should take care of that. + } + + if (url.isValid()) { + KIO::DeleteJob *job = KIO::del(url); + if (!job->exec()) { + KMessageBox::sorry(parent, i18n("File \"%1\" could not be deleted.", url.toDisplayString())); + } + } +} + +QStringList KateFileActions::supportedDiffTools() +{ + // LATER: check for program existence and set some boolean value accordingly + // Can this be even done in an easy way when we don't use the absolute path to the executable? + // See https://stackoverflow.com/questions/42444055/how-to-check-if-a-program-exists-in-path-using-qt + + QStringList resultList; + resultList.push_back(QStringLiteral("kdiff3")); + resultList.push_back(QStringLiteral("kompare")); + resultList.push_back(QStringLiteral("meld")); + + return resultList; +} + +bool KateFileActions::compareWithExternalProgram(KTextEditor::Document* documentA, KTextEditor::Document* documentB, const QString& diffExecutable) +{ + Q_ASSERT(documentA); + Q_ASSERT(documentB); + + QProcess process; + QStringList arguments; + arguments << documentA->url().toLocalFile() << documentB->url().toLocalFile(); + return process.startDetached(diffExecutable, arguments); +} diff --git a/kate/katemainwindow.h b/kate/katemainwindow.h --- a/kate/katemainwindow.h +++ b/kate/katemainwindow.h @@ -190,6 +190,7 @@ void slotEditToolbars(); void slotNewToolbarConfig(); void slotUpdateOpenWith(); + void slotUpdateActionsNeedingUrl(); void slotOpenDocument(QUrl); void slotDropEvent(QDropEvent *); diff --git a/kate/katemainwindow.cpp b/kate/katemainwindow.cpp --- a/kate/katemainwindow.cpp +++ b/kate/katemainwindow.cpp @@ -37,6 +37,7 @@ #include "kateupdatedisabler.h" #include "katedebug.h" #include "katecolorschemechooser.h" +#include "katefileactions.h" #include #include @@ -306,6 +307,64 @@ connect(a, SIGNAL(triggered()), KateApp::self()->documentManager(), SLOT(reloadAll())); a->setWhatsThis(i18n("Reload all open documents.")); + a = actionCollection()->addAction(QStringLiteral("file_copy_filepath")); + a->setIcon(QIcon::fromTheme(QStringLiteral("edit-copy"))); + a->setText(i18n("Copy File &Path")); + connect(a, &QAction::triggered, KateApp::self()->documentManager(), + [this]() { + auto&& view = viewManager()->activeView(); + KateFileActions::copyFilePathToClipboard(view->document()); + }); + a->setWhatsThis(i18n("Copies the file path of the current file to clipboard.")); + + a = actionCollection()->addAction(QStringLiteral("file_open_containing_folder")); + a->setIcon(QIcon::fromTheme(QStringLiteral("document-open-folder"))); + a->setText(i18n("&Open Containing Folder")); + connect(a, &QAction::triggered, KateApp::self()->documentManager(), + [this]() { + auto&& view = viewManager()->activeView(); + KateFileActions::openContainingFolder(view->document()); + }); + a->setWhatsThis(i18n("Copies the file path of the current file to clipboard.")); + + a = actionCollection()->addAction(QStringLiteral("file_rename")); + a->setIcon(QIcon::fromTheme(QStringLiteral("edit-rename"))); + a->setText(i18nc("@action:inmenu", "Rename File...")); + connect(a, &QAction::triggered, KateApp::self()->documentManager(), + [this]() { + auto&& view = viewManager()->activeView(); + KateFileActions::renameDocumentFile(this, view->document()); + }); + a->setWhatsThis(i18n("Renames the file belonging to the current document.")); + + a = actionCollection()->addAction(QStringLiteral("file_delete")); + a->setIcon(QIcon::fromTheme(QStringLiteral("edit-delete-shred"))); + a->setText(i18nc("@action:inmenu", "Delete File")); + connect(a, &QAction::triggered, KateApp::self()->documentManager(), + [this]() { + auto&& view = viewManager()->activeView(); + KateFileActions::deleteDocumentFile(this, view->document()); + }); + a->setWhatsThis(i18n("Deletes the file belonging to the current document.")); + + a = actionCollection()->addAction(QStringLiteral("file_properties")); + a->setIcon(QIcon::fromTheme(QStringLiteral("dialog-object-properties"))); + a->setText(i18n("Properties")); + connect(a, &QAction::triggered, KateApp::self()->documentManager(), + [this]() { + auto&& view = viewManager()->activeView(); + KateFileActions::openFilePropertiesDialog(view->document()); + }); + a->setWhatsThis(i18n("Deletes the file belonging to the current document.")); + + a = actionCollection()->addAction(QStringLiteral("file_compare")); + a->setText(i18n("Compare")); + connect(a, &QAction::triggered, KateApp::self()->documentManager(), + [this]() { + QMessageBox::information(this, i18n("Compare"), i18n("Use the Tabbar context menu to compare two documents")); + }); + a->setWhatsThis(i18n("Shows a hint how to compare documents.")); + a = actionCollection()->addAction(QStringLiteral("file_close_orphaned")); a->setText(i18n("Close Orphaned")); connect(a, SIGNAL(triggered()), KateApp::self()->documentManager(), SLOT(closeOrphaned())); @@ -368,6 +427,7 @@ connect(m_viewManager, SIGNAL(viewChanged(KTextEditor::View*)), this, SLOT(slotWindowActivated())); connect(m_viewManager, SIGNAL(viewChanged(KTextEditor::View*)), this, SLOT(slotUpdateOpenWith())); + connect(m_viewManager, &KateViewManager::viewChanged, this, &KateMainWindow::slotUpdateActionsNeedingUrl); connect(m_viewManager, SIGNAL(viewChanged(KTextEditor::View*)), this, SLOT(slotUpdateBottomViewBar())); // re-route signals to our wrapper @@ -669,6 +729,19 @@ } } +void KateMainWindow::slotUpdateActionsNeedingUrl() +{ + + auto&& view = viewManager()->activeView(); + const bool hasUrl = view && !view->document()->url().isEmpty(); + + action("file_copy_filepath")->setEnabled(hasUrl); + action("file_open_containing_folder")->setEnabled(hasUrl); + action("file_rename")->setEnabled(hasUrl); + action("file_delete")->setEnabled(hasUrl); + action("file_properties")->setEnabled(hasUrl); +} + void KateMainWindow::dragEnterEvent(QDragEnterEvent *event) { if (!event->mimeData()) { diff --git a/kate/kateviewspace.cpp b/kate/kateviewspace.cpp --- a/kate/kateviewspace.cpp +++ b/kate/kateviewspace.cpp @@ -23,6 +23,7 @@ #include "kateviewmanager.h" #include "katedocmanager.h" #include "kateapp.h" +#include "katefileactions.h" #include "katesessionmanager.h" #include "katedebug.h" #include "katetabbar.h" @@ -32,12 +33,12 @@ #include #include #include -#include #include #include #include #include +#include #include #include #include @@ -584,31 +585,79 @@ KTextEditor::Document *doc = m_docToTabId.key(id); Q_ASSERT(doc); + auto addActionFromCollection = [this](QMenu* menu, const char* action_name) { + QAction* action = m_viewManager->mainWindow()->action(action_name); + return menu->addAction(action->icon(), action->text()); + }; + QMenu menu(this); QAction *aCloseTab = menu.addAction(QIcon::fromTheme(QStringLiteral("tab-close")), i18n("&Close Document")); QAction *aCloseOthers = menu.addAction(QIcon::fromTheme(QStringLiteral("tab-close-other")), i18n("Close Other &Documents")); menu.addSeparator(); - QAction *aCopyPath = menu.addAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18n("Copy File &Path")); - QAction *aOpenFolder = menu.addAction(QIcon::fromTheme(QStringLiteral("document-open-folder")), i18n("&Open Containing Folder")); + QAction *aCopyPath = addActionFromCollection(&menu, "file_copy_filepath"); + QAction *aOpenFolder = addActionFromCollection(&menu, "file_open_containing_folder"); + QAction *aFileProperties = addActionFromCollection(&menu, "file_properties"); + menu.addSeparator(); + QAction *aRenameFile = addActionFromCollection(&menu, "file_rename"); + QAction *aDeleteFile = addActionFromCollection(&menu, "file_delete"); + menu.addSeparator(); + QMenu *mCompareWithActive = new QMenu(i18n("Compare with active document"), &menu); + mCompareWithActive->setIcon(QIcon::fromTheme(QStringLiteral("kompare"))); + menu.addMenu(mCompareWithActive); if (KateApp::self()->documentManager()->documentList().count() < 2) { aCloseOthers->setEnabled(false); } + if (doc->url().isEmpty()) { aCopyPath->setEnabled(false); aOpenFolder->setEnabled(false); + aRenameFile->setEnabled(false); + aDeleteFile->setEnabled(false); + aFileProperties->setEnabled(false); + mCompareWithActive->setEnabled(false); + } + + auto activeDocument = KTextEditor::Editor::instance()->application()->activeMainWindow()->activeView()->document(); // used for mCompareWithActive which is used with another tab which is not active + // both documents must have urls and must not be the same to have the compare feature enabled + if (activeDocument->url().isEmpty() || activeDocument == doc) { + mCompareWithActive->setEnabled(false); + } + + if (mCompareWithActive->isEnabled()) { + for (auto&& diffTool : KateFileActions::supportedDiffTools()) { + QAction *compareAction = mCompareWithActive->addAction(diffTool); + compareAction->setData(diffTool); + } } QAction *choice = menu.exec(globalPos); + if (!choice) { + return; + } + if (choice == aCloseTab) { closeTabRequest(id); } else if (choice == aCloseOthers) { KateApp::self()->documentManager()->closeOtherDocuments(doc); } else if (choice == aCopyPath) { - QApplication::clipboard()->setText(doc->url().toDisplayString(QUrl::PreferLocalFile)); + KateFileActions::copyFilePathToClipboard(doc); } else if (choice == aOpenFolder) { - KIO::highlightInFileManager({doc->url()}); + KateFileActions::openContainingFolder(doc); + } else if (choice == aFileProperties) { + KateFileActions::openFilePropertiesDialog(doc); + } else if (choice == aRenameFile) { + KateFileActions::renameDocumentFile(this, doc); + } else if (choice == aDeleteFile) { + KateFileActions::deleteDocumentFile(this, doc); + } else if (choice->parent() == mCompareWithActive) { + QString actionData = choice->data().toString(); // name of the executable of the diff program + if (!KateFileActions::compareWithExternalProgram(activeDocument, doc, actionData)) { + QMessageBox::information(this, i18n("Could not start program"), + i18n("The selected program could not be started. Maybe it is not installed."), + QMessageBox::StandardButton::Ok); + } } }