Commit e8496c60 authored by Gregor Mi's avatar Gregor Mi

Add file related actions to Tabbar context menu and File menu: Rename, Delete,...

Add file related actions to Tabbar context menu and File menu: Rename, Delete, Compare (new), and some more

Summary:
Add some actions to Tabbar context menu and File menu

New items in Tabbar context menu:
- 'Rename file'
- 'Delete file'
- 'Properties'
- 'Compare'

'Compare' does the following: compare two files with an external diff tool. It works like this: right click on a tab which is NOT active and select the appropriate menu item (see screenshot). Then, the external tool opens and compares the active document with the one which was clicked on. Currently, three diff tools are supported: kdiff3, kompare and meld.

New items in File main menu:
- 'Rename file'
- 'Delete file'
- 'Compare'
- 'Copy File Path'
- 'Open Containing Folder'
- 'Properties'

Screenshots:

Tab context menu:

{F6414794}

Tab context menu when no file is associated:

{F6414793}

Tab context menu with the compare feature:

{F6431032}

Diff tools not found:

{F6450993}

Actions are also available in the main File menu:

{F6450928}

NOTE: Some code of kfileactions.cpp was copied and adapted from katefiletree.cpp. TODO: reuse code there. Probably, this means kfileactions.h must be moved to KTextEditor.

TODO: in a later change the 'Open with...' menu item which is currently available in the Projects and FileTree plugin could also be extracted and be added to the Tabbar context menu and File menu (Gwenview has it, too).

Reviewers: #kate, #vdg, ngraham, cullmann

Reviewed By: #kate, #vdg, ngraham, cullmann

Subscribers: cullmann, anthonyfieroni, dhaumann, mmustac, ngraham, kwrite-devel

Tags: #kate

Differential Revision: https://phabricator.kde.org/D16830
parent 53dd31b4
......@@ -420,6 +420,10 @@ void KateFileTree::slotDocumentReload()
void KateFileTree::slotCopyFilename()
{
KTextEditor::Document *doc = model()->data(m_indexContextMenu, KateFileTreeModel::DocumentRole).value<KTextEditor::Document *>();
// 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::slotCopyFilename()
void KateFileTree::slotRenameFile() {
KTextEditor::Document *doc = model()->data(m_indexContextMenu, KateFileTreeModel::DocumentRole).value<KTextEditor::Document *>();
// TODO: the following code was improved in kate/katefileactions.cpp and should be reused here
if (!doc) {
return;
}
......@@ -676,6 +683,8 @@ void KateFileTree::slotDocumentDelete()
{
KTextEditor::Document *doc = model()->data(m_indexContextMenu, KateFileTreeModel::DocumentRole).value<KTextEditor::Document *>();
// TODO: the following code was improved in kate/katefileactions.cpp and should be reused here
if (!doc) {
return;
}
......
......@@ -76,7 +76,7 @@ void KateProjectTreeViewContextMenu::exec(const QString &filename, const QPoint
* 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
......
......@@ -20,6 +20,7 @@ set (KATE_LIBRARY_SRCS
kateconfigdialog.cpp
kateconfigplugindialogpage.cpp
katedocmanager.cpp
katefileactions.cpp
katemainwindow.cpp
katepluginmanager.cpp
kateviewmanager.cpp
......
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE kpartgui SYSTEM "kpartgui.dtd">
<kpartgui name="kate" version="83" translationDomain="kate">
<kpartgui name="kate" version="84" translationDomain="kate">
<MenuBar>
<Menu name="file" noMerge="1">
<text>&amp;File</text>
......@@ -20,6 +20,14 @@
<DefineGroup name="print_merge"/>
<DefineGroup name="export_merge"/>
<Separator/>
<Action name="file_rename"/>
<Action name="file_delete"/>
<Action name="file_compare"/>
<Separator/>
<Action name="file_copy_filepath"/>
<Action name="file_open_containing_folder"/>
<Action name="file_properties"/>
<Separator/>
<Action name="file_close"/>
<Action name="file_close_other"/>
<Action name="file_close_all"/>
......
/* This file is part of the KDE project
*
* Copyright (C) 2018 Gregor Mi <codestruct@posteo.org>
*
* 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 <http://www.gnu.org/licenses/>.
**/
#include "katefileactions.h"
#include <ktexteditor/application.h>
#include <ktexteditor/document.h>
#include <ktexteditor/editor.h>
#include <KIO/CopyJob>
#include <KIO/DeleteJob>
#include <KIO/OpenFileManagerWindowJob>
#include <KLocalizedString>
#include <KMessageBox>
#include <KPropertiesDialog>
#include <QApplication>
#include <QClipboard>
#include <QDebug>
#include <QInputDialog>
#include <QProcess>
#include <QUrl>
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<QMetaObject::Connection> 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);
}
/* This file is part of the KDE project
*
* Copyright (C) 2018 Gregor Mi <codestruct@posteo.org>
*
* 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 <http://www.gnu.org/licenses/>.
**/
#ifndef KATE_FILEACTIONS_H
#define KATE_FILEACTIONS_H
#include <vector>
#include <utility>
#include <QString>
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
......@@ -37,6 +37,7 @@
#include "kateupdatedisabler.h"
#include "katedebug.h"
#include "katecolorschemechooser.h"
#include "katefileactions.h"
#include <KActionMenu>
#include <KAboutApplicationDialog>
......@@ -306,6 +307,64 @@ void KateMainWindow::setupActions()
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 @@ void KateMainWindow::setupActions()
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::slotUpdateOpenWith()
}
}
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()) {
......
......@@ -190,6 +190,7 @@ private Q_SLOTS:
void slotEditToolbars();
void slotNewToolbarConfig();
void slotUpdateOpenWith();
void slotUpdateActionsNeedingUrl();
void slotOpenDocument(QUrl);
void slotDropEvent(QDropEvent *);
......
......@@ -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 <KAcceleratorManager>
#include <KConfigGroup>
#include <KLocalizedString>
#include <KIO/OpenFileManagerWindowJob>
#include <QApplication>
#include <QClipboard>
#include <QHelpEvent>
#include <QMenu>
#include <QMessageBox>
#include <QStackedWidget>
#include <QToolButton>
#include <QToolTip>
......@@ -584,31 +585,79 @@ void KateViewSpace::showContextMenu(int id, const QPoint & globalPos)
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);
}
}
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment