You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

326 lines
11 KiB

/*
SPDX-FileCopyrightText: 2007-2008 Robert Knight <robertknight@gmail.com>
SPDX-FileCopyrightText: 2020 Tomaz Canabrava <tcanabrava@gmail.com>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "FileFilterHotspot.h"
#include <QApplication>
#include <QAction>
#include <QBuffer>
#include <QClipboard>
#include <QMenu>
#include <QTimer>
#include <QToolTip>
#include <QMimeDatabase>
#include <QMouseEvent>
#include <QKeyEvent>
#include <QRegularExpression>
#include <QDrag>
#include <QMimeData>
#include <KIO/ApplicationLauncherJob>
#include <KIO/OpenUrlJob>
#include <KIO/JobUiDelegate>
#include <KLocalizedString>
#include <KFileItemListProperties>
#include <KMessageBox>
#include <KShell>
#include "konsoledebug.h"
#include "KonsoleSettings.h"
#include "profile/Profile.h"
#include "session/SessionManager.h"
#include "terminalDisplay/TerminalDisplay.h"
using namespace Konsole;
FileFilterHotSpot::FileFilterHotSpot(int startLine, int startColumn, int endLine, int endColumn,
const QStringList &capturedTexts, const QString &filePath,
Session *session)
: RegExpFilterHotSpot(startLine, startColumn, endLine, endColumn, capturedTexts),
_filePath(filePath),
_session(session),
_thumbnailFinished(false)
{
setType(Link);
}
void FileFilterHotSpot::activate(QObject *)
{
if (!_session) { // The Session is dead, nothing to do
return;
}
QString editorExecPath;
int firstBlankIdx = -1;
QString fullCmd;
Profile::Ptr profile = SessionManager::instance()->sessionProfile(_session);
const QString editorCmd = profile->textEditorCmd();
if (!editorCmd.isEmpty()) {
firstBlankIdx = editorCmd.indexOf(QLatin1Char(' '));
if (firstBlankIdx != -1) {
editorExecPath = QStandardPaths::findExecutable(editorCmd.mid(0, firstBlankIdx));
} else { // No spaces, e.g. just a binary name "foo"
editorExecPath = QStandardPaths::findExecutable(editorCmd);
}
}
// Output of e.g.:
// - grep with line numbers: "path/to/some/file:123:"
// grep with long lines e.g. "path/to/some/file:123:void blah" i.e. no space after 123:
// - compiler errors with line/column numbers: "/path/to/file.cpp:123:123:"
// - ctest failing unit tests: "/path/to/file(204)"
static const QRegularExpression re(QStringLiteral(R"foo([:\(](\d+)(?:\)\])?(?::(\d+):|:[^\d]*)?$)foo"));
const QRegularExpressionMatch match = re.match(_filePath);
if (match.hasMatch()) {
// The file path without the ":123" ... etc part
const QString path = _filePath.mid(0, match.capturedStart(0));
// TODO: show an error message to the user?
if (editorExecPath.isEmpty()) { // Couldn't find the specified binary, fallback
openWithSysDefaultApp(path);
return;
}
if (firstBlankIdx != -1) {
fullCmd = editorCmd;
// Substitute e.g. "fooBinary" with full path, "/usr/bin/fooBinary"
fullCmd.replace(0, firstBlankIdx, editorExecPath);
fullCmd.replace(QLatin1String("PATH"), path);
fullCmd.replace(QLatin1String("LINE"), match.captured(1));
const QString col = match.captured(2);
fullCmd.replace(QLatin1String("COLUMN"), !col.isEmpty() ? col : QLatin1String("0"));
} else { // The editorCmd is just the binary name, no PATH, LINE or COLUMN
// Add the "path" here, so it becomes "/path/to/fooBinary path"
fullCmd += QLatin1Char(' ') + path;
}
openWithEditorFromProfile(fullCmd, path);
return;
}
// There was no match, i.e. regular url "path/to/file"
// Clean up the file path; the second branch in the regex is for "path/to/file:"
QString path(_filePath);
static const QRegularExpression cleanupRe(QStringLiteral(R"foo((:\d+[:]?|:)$)foo"),
QRegularExpression::DontCaptureOption);
path.remove(cleanupRe);
if (!editorExecPath.isEmpty()) { // Use the editor from the profile settings
const QString fCmd = editorExecPath + QLatin1Char(' ') + path;
openWithEditorFromProfile(fCmd, path);
} else { // Fallback
openWithSysDefaultApp(path);
}
}
void FileFilterHotSpot::openWithSysDefaultApp(const QString &filePath) const
{
auto *job = new KIO::OpenUrlJob(QUrl::fromLocalFile(filePath));
job->setUiDelegate(new KIO::JobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, QApplication::activeWindow()));
job->setRunExecutables(false); // Always open scripts, shell/python/perl... etc, as text
job->start();
}
void FileFilterHotSpot::openWithEditorFromProfile(const QString &fullCmd, const QString &path) const
{
// Here we are mostly interested in text-based files, e.g. if it's a
// PDF we should let the system default app open it.
QMimeDatabase mdb;
const auto mimeType = mdb.mimeTypeForFile(path);
qCDebug(KonsoleDebug) << "FileFilterHotSpot: mime type for" << path << ":" << mimeType;
if (!mimeType.inherits(QStringLiteral("text/plain"))) {
openWithSysDefaultApp(path);
return;
}
qCDebug(KonsoleDebug) << "fullCmd:" << fullCmd;
KService::Ptr service(new KService(QString(), fullCmd, QString()));
// ApplicationLauncherJob is better at reporting errors to the user than
// CommandLauncherJob; no need to call job->setUrls() because the url is
// already part of fullCmd
auto *job = new KIO::ApplicationLauncherJob(service);
connect(job, &KJob::result, this, [this, path, job]() {
if (job->error() != 0) {
// TODO: use KMessageWidget (like the "terminal is read-only" message)
KMessageBox::sorry(QApplication::activeWindow(),
i18n("Could not open file with the text editor specified in the profile settings;\n"
"it will be opened with the system default editor."));
openWithSysDefaultApp(path);
}
});
job->start();
}
FileFilterHotSpot::~FileFilterHotSpot() = default;
QList<QAction *> FileFilterHotSpot::actions()
{
QAction *action = new QAction(i18n("Copy Location"), this);
action->setIcon(QIcon::fromTheme(QStringLiteral("edit-copy")));
connect(action, &QAction::triggered, this, [this] {
QGuiApplication::clipboard()->setText(_filePath);
});
return {action};
}
void FileFilterHotSpot::setupMenu(QMenu *menu)
{
const KFileItem fileItem(QUrl::fromLocalFile(_filePath));
const KFileItemList itemList({fileItem});
const KFileItemListProperties itemProperties(itemList);
_menuActions.setParent(this);
_menuActions.setItemListProperties(itemProperties);
_menuActions.addOpenWithActionsTo(menu);
// Here we added the actions to the last part of the menu, but we need to move them up.
// TODO: As soon as addOpenWithActionsTo accepts a index, change this.
// https://bugs.kde.org/show_bug.cgi?id=423765
QAction *firstAction = menu->actions().at(0);
for (auto *action : menu->actions()) {
if (action->text().toLower().remove(QLatin1Char('&')).contains(i18n("open with"))) {
menu->removeAction(action);
menu->insertAction(firstAction, action);
}
}
auto *separator = new QAction(this);
separator->setSeparator(true);
menu->insertAction(firstAction, separator);
}
// Static variables for the HotSpot
bool FileFilterHotSpot::_canGenerateThumbnail = false;
QPointer<KIO::PreviewJob> FileFilterHotSpot::_previewJob;
void FileFilterHotSpot::requestThumbnail(Qt::KeyboardModifiers modifiers, const QPoint &pos) {
_canGenerateThumbnail = true;
_eventModifiers = modifiers;
_eventPos = pos;
// Defer the real creation of the thumbnail by a few msec.
QTimer::singleShot(250, this, [this]{
thumbnailRequested();
});
}
void FileFilterHotSpot::stopThumbnailGeneration()
{
_canGenerateThumbnail = false;
if (_previewJob != nullptr) {
_previewJob->deleteLater();
QToolTip::hideText();
}
}
void FileFilterHotSpot::showThumbnail(const KFileItem& item, const QPixmap& preview)
{
if (!_canGenerateThumbnail) {
return;
}
_thumbnailFinished = true;
Q_UNUSED(item)
QByteArray data;
QBuffer buffer(&data);
preview.save(&buffer, "PNG", 100);
const auto tooltipString = QStringLiteral("<img src='data:image/png;base64, %0'>")
.arg(QString::fromLocal8Bit(data.toBase64()));
QToolTip::showText(_thumbnailPos, tooltipString, qApp->focusWidget());
}
void FileFilterHotSpot::thumbnailRequested() {
if (!_canGenerateThumbnail) {
return;
}
auto *settings = KonsoleSettings::self();
Qt::KeyboardModifiers modifiers = settings->thumbnailCtrl() ? Qt::ControlModifier : Qt::NoModifier;
modifiers |= settings->thumbnailAlt() ? Qt::AltModifier : Qt::NoModifier;
modifiers |= settings->thumbnailShift() ? Qt::ShiftModifier : Qt::NoModifier;
if (_eventModifiers != modifiers) {
return;
}
_thumbnailPos = QPoint(_eventPos.x() + 100, _eventPos.y() - settings->thumbnailSize() / 2);
const int size = KonsoleSettings::thumbnailSize();
if (_previewJob != nullptr) {
_previewJob->deleteLater();
}
_thumbnailFinished = false;
// Show a "Loading" if Preview takes a long time.
QTimer::singleShot(10, this, [this]{
if (_previewJob == nullptr) {
return;
}
if (!_thumbnailFinished) {
QToolTip::showText(_thumbnailPos, i18n("Generating Thumbnail"), qApp->focusWidget());
}
});
_previewJob = new KIO::PreviewJob(KFileItemList({fileItem()}), QSize(size, size));
connect(_previewJob, &KIO::PreviewJob::gotPreview, this, &FileFilterHotSpot::showThumbnail);
connect(_previewJob, &KIO::PreviewJob::failed, this, []{
qCDebug(KonsoleDebug) << "Error generating the preview" << _previewJob->errorString();
QToolTip::hideText();
});
_previewJob->setAutoDelete(true);
_previewJob->start();
}
KFileItem FileFilterHotSpot::fileItem() const
{
return KFileItem(QUrl::fromLocalFile(_filePath));
}
void FileFilterHotSpot::mouseEnterEvent(TerminalDisplay *td, QMouseEvent *ev)
{
HotSpot::mouseEnterEvent(td, ev);
requestThumbnail(ev->modifiers(), ev->globalPos());
}
void FileFilterHotSpot::mouseLeaveEvent(TerminalDisplay *td, QMouseEvent *ev)
{
HotSpot::mouseLeaveEvent(td, ev);
stopThumbnailGeneration();
}
void FileFilterHotSpot::keyPressEvent(Konsole::TerminalDisplay* td, QKeyEvent* ev)
{
HotSpot::keyPressEvent(td, ev);
requestThumbnail(ev->modifiers(), QCursor::pos());
}
bool FileFilterHotSpot::hasDragOperation() const
{
return true;
}
void FileFilterHotSpot::startDrag()
{
auto *drag = new QDrag(this);
auto *mimeData = new QMimeData();
mimeData->setUrls({QUrl::fromLocalFile(_filePath)});
drag->setMimeData(mimeData);
drag->exec(Qt::CopyAction);
}