File indexing completed on 2024-05-12 09:56:57

0001 /*
0002     SPDX-FileCopyrightText: 2007-2008 Robert Knight <robertknight@gmail.com>
0003     SPDX-FileCopyrightText: 2020 Tomaz Canabrava <tcanabrava@gmail.com>
0004 
0005     SPDX-License-Identifier: GPL-2.0-or-later
0006 */
0007 
0008 #include "FileFilterHotspot.h"
0009 
0010 #include <QAction>
0011 #include <QApplication>
0012 #include <QBuffer>
0013 #include <QClipboard>
0014 #include <QDrag>
0015 #include <QKeyEvent>
0016 #include <QMenu>
0017 #include <QMimeData>
0018 #include <QMimeDatabase>
0019 #include <QMouseEvent>
0020 #include <QRegularExpression>
0021 #include <QTimer>
0022 #include <QToolTip>
0023 
0024 #include <KIO/ApplicationLauncherJob>
0025 #include <KIO/OpenUrlJob>
0026 
0027 #include <KFileItemListProperties>
0028 #include <KIO/JobUiDelegateFactory>
0029 #include <KLocalizedString>
0030 #include <KMessageBox>
0031 #include <KShell>
0032 
0033 #include <kio_version.h>
0034 
0035 #include "KonsoleSettings.h"
0036 #include "konsoledebug.h"
0037 #include "profile/Profile.h"
0038 #include "session/SessionManager.h"
0039 #include "terminalDisplay/TerminalDisplay.h"
0040 
0041 using namespace Konsole;
0042 
0043 FileFilterHotSpot::FileFilterHotSpot(int startLine,
0044                                      int startColumn,
0045                                      int endLine,
0046                                      int endColumn,
0047                                      const QStringList &capturedTexts,
0048                                      const QString &filePath,
0049                                      Session *session)
0050     : RegExpFilterHotSpot(startLine, startColumn, endLine, endColumn, capturedTexts)
0051     , _filePath(filePath)
0052     , _session(session)
0053     , _thumbnailFinished(false)
0054 {
0055     setType(File);
0056 }
0057 
0058 void FileFilterHotSpot::activate(QObject *)
0059 {
0060     if (!_session) { // The Session is dead, nothing to do
0061         return;
0062     }
0063 
0064     QString editorExecPath;
0065     int firstBlankIdx = -1;
0066     QString fullCmd;
0067 
0068     Profile::Ptr profile = SessionManager::instance()->sessionProfile(_session);
0069     const QString editorCmd = profile->textEditorCmd();
0070     if (!editorCmd.isEmpty()) {
0071         firstBlankIdx = editorCmd.indexOf(QLatin1Char(' '));
0072         if (firstBlankIdx != -1) {
0073             editorExecPath = QStandardPaths::findExecutable(editorCmd.mid(0, firstBlankIdx));
0074         } else { // No spaces, e.g. just a binary name "foo"
0075             editorExecPath = QStandardPaths::findExecutable(editorCmd);
0076         }
0077     }
0078 
0079     // Output of e.g.:
0080     // - grep with line numbers: "path/to/some/file:123:"
0081     //   grep with long lines e.g. "path/to/some/file:123:void blah" i.e. no space after 123:
0082     // - compiler errors with line/column numbers: "/path/to/file.cpp:123:123:"
0083     // - ctest failing unit tests: "/path/to/file(204)"
0084     static const QRegularExpression re(QStringLiteral(R"foo([:\(](\d+)(?:\)\])?(?::(\d+):|:[^\d]*)?$)foo"));
0085     const QRegularExpressionMatch match = re.match(_filePath);
0086     if (match.hasMatch()) {
0087         // The file path without the ":123" ... etc part
0088         const QString path = _filePath.mid(0, match.capturedStart(0));
0089 
0090         // TODO: show an error message to the user?
0091         if (editorExecPath.isEmpty()) { // Couldn't find the specified binary, fallback
0092             openWithSysDefaultApp(path);
0093             return;
0094         }
0095         if (firstBlankIdx != -1) {
0096             fullCmd = editorCmd;
0097             // Substitute e.g. "fooBinary" with full path, "/usr/bin/fooBinary"
0098             fullCmd.replace(0, firstBlankIdx, editorExecPath);
0099 
0100             fullCmd.replace(QLatin1String("PATH"), path);
0101             fullCmd.replace(QLatin1String("LINE"), match.captured(1));
0102 
0103             const QString col = match.captured(2);
0104             fullCmd.replace(QLatin1String("COLUMN"), !col.isEmpty() ? col : QLatin1String("0"));
0105         } else { // The editorCmd is just the binary name, no PATH, LINE or COLUMN
0106             // Add the "path" here, so it becomes "/path/to/fooBinary path"
0107             fullCmd += QLatin1Char(' ') + path;
0108         }
0109 
0110         openWithEditorFromProfile(fullCmd, path);
0111         return;
0112     }
0113 
0114     // There was no match, i.e. regular url "path/to/file"
0115     // Clean up the file path; the second branch in the regex is for "path/to/file:"
0116     QString path(_filePath);
0117     static const QRegularExpression cleanupRe(QStringLiteral(R"foo((:\d+[:]?|:)$)foo"), QRegularExpression::DontCaptureOption);
0118     path.remove(cleanupRe);
0119     if (!editorExecPath.isEmpty()) { // Use the editor from the profile settings
0120         const QString fCmd = editorExecPath + QLatin1Char(' ') + path;
0121         openWithEditorFromProfile(fCmd, path);
0122     } else { // Fallback
0123         openWithSysDefaultApp(path);
0124     }
0125 }
0126 
0127 void FileFilterHotSpot::openWithSysDefaultApp(const QString &filePath) const
0128 {
0129     auto *job = new KIO::OpenUrlJob(QUrl::fromLocalFile(filePath));
0130     job->setUiDelegate(KIO::createDefaultJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, QApplication::activeWindow()));
0131     job->setRunExecutables(false); // Always open scripts, shell/python/perl... etc, as text
0132     job->start();
0133 }
0134 
0135 void FileFilterHotSpot::openWithEditorFromProfile(const QString &fullCmd, const QString &path) const
0136 {
0137     // Here we are mostly interested in text-based files, e.g. if it's a
0138     // PDF we should let the system default app open it.
0139     QMimeDatabase mdb;
0140     const auto mimeType = mdb.mimeTypeForFile(path);
0141     qCDebug(KonsoleDebug) << "FileFilterHotSpot: mime type for" << path << ":" << mimeType;
0142 
0143     if (!mimeType.inherits(QStringLiteral("text/plain"))) {
0144         openWithSysDefaultApp(path);
0145         return;
0146     }
0147 
0148     qCDebug(KonsoleDebug) << "fullCmd:" << fullCmd;
0149 
0150     KService::Ptr service(new KService(QString(), fullCmd, QString()));
0151 
0152     // ApplicationLauncherJob is better at reporting errors to the user than
0153     // CommandLauncherJob; no need to call job->setUrls() because the url is
0154     // already part of fullCmd
0155     auto *job = new KIO::ApplicationLauncherJob(service);
0156     connect(job, &KJob::result, this, [this, path, job]() {
0157         if (job->error() != 0) {
0158             // TODO: use KMessageWidget (like the "terminal is read-only" message)
0159             KMessageBox::information(QApplication::activeWindow(),
0160                                      i18n("Could not open file with the text editor specified in the profile settings;\n"
0161                                           "it will be opened with the system default editor."));
0162 
0163             openWithSysDefaultApp(path);
0164         }
0165     });
0166 
0167     job->start();
0168 }
0169 
0170 FileFilterHotSpot::~FileFilterHotSpot() = default;
0171 
0172 QList<QAction *> FileFilterHotSpot::actions()
0173 {
0174     QAction *action = new QAction(i18n("Copy Location"), this);
0175     action->setIcon(QIcon::fromTheme(QStringLiteral("edit-copy-path")));
0176     connect(action, &QAction::triggered, this, [this] {
0177         QGuiApplication::clipboard()->setText(_filePath);
0178     });
0179     return {action};
0180 }
0181 
0182 QList<QAction *> FileFilterHotSpot::setupMenu(QMenu *menu)
0183 {
0184     if (!_menuActions)
0185         _menuActions = new KFileItemActions;
0186     const QList<QAction *> currentActions = menu->actions();
0187 
0188     const KFileItem fileItem(QUrl::fromLocalFile(_filePath));
0189     const KFileItemList itemList({fileItem});
0190     const KFileItemListProperties itemProperties(itemList);
0191     _menuActions->setParent(this);
0192     _menuActions->setItemListProperties(itemProperties);
0193 
0194     const QList<QAction *> actionList = menu->actions();
0195     _menuActions->insertOpenWithActionsTo(!actionList.isEmpty() ? actionList.at(0) : nullptr, menu, QStringList());
0196 
0197     QList<QAction *> addedActions = menu->actions();
0198     // addedActions will only contain the open-with actions
0199     for (auto *act : currentActions) {
0200         addedActions.removeOne(act);
0201     }
0202 
0203     return addedActions;
0204 }
0205 
0206 // Static variables for the HotSpot
0207 bool FileFilterHotSpot::_canGenerateThumbnail = false;
0208 QPointer<KIO::PreviewJob> FileFilterHotSpot::_previewJob;
0209 
0210 void FileFilterHotSpot::requestThumbnail(Qt::KeyboardModifiers modifiers, const QPoint &pos)
0211 {
0212     if (!KonsoleSettings::self()->enableThumbnails()) {
0213         return;
0214     }
0215 
0216     _canGenerateThumbnail = true;
0217     _eventModifiers = modifiers;
0218     _eventPos = pos;
0219 
0220     // Defer the real creation of the thumbnail by a few msec.
0221     QTimer::singleShot(250, this, [this] {
0222         thumbnailRequested();
0223     });
0224 }
0225 
0226 void FileFilterHotSpot::stopThumbnailGeneration()
0227 {
0228     _canGenerateThumbnail = false;
0229     if (_previewJob != nullptr) {
0230         _previewJob->deleteLater();
0231         QToolTip::hideText();
0232     }
0233 }
0234 
0235 void FileFilterHotSpot::showThumbnail(const KFileItem &item, const QPixmap &preview)
0236 {
0237     if (!_canGenerateThumbnail) {
0238         return;
0239     }
0240     _thumbnailFinished = true;
0241     Q_UNUSED(item)
0242     QByteArray data;
0243     QBuffer buffer(&data);
0244     preview.save(&buffer, "PNG", 100);
0245 
0246     const auto tooltipString = QStringLiteral("<img src='data:image/png;base64, %0'>").arg(QString::fromLocal8Bit(data.toBase64()));
0247 
0248     QToolTip::showText(_thumbnailPos, tooltipString, qApp->focusWidget());
0249 }
0250 
0251 void FileFilterHotSpot::thumbnailRequested()
0252 {
0253     if (!_canGenerateThumbnail) {
0254         return;
0255     }
0256 
0257     auto *settings = KonsoleSettings::self();
0258 
0259     Qt::KeyboardModifiers modifiers = settings->thumbnailCtrl() ? Qt::ControlModifier : Qt::NoModifier;
0260     modifiers |= settings->thumbnailAlt() ? Qt::AltModifier : Qt::NoModifier;
0261     modifiers |= settings->thumbnailShift() ? Qt::ShiftModifier : Qt::NoModifier;
0262 
0263     if (_eventModifiers != modifiers) {
0264         return;
0265     }
0266 
0267     _thumbnailPos = QPoint(_eventPos.x() + 100, _eventPos.y() - settings->thumbnailSize() / 2);
0268 
0269     const int size = KonsoleSettings::thumbnailSize();
0270     if (_previewJob != nullptr) {
0271         _previewJob->deleteLater();
0272     }
0273 
0274     _thumbnailFinished = false;
0275 
0276     // Show a "Loading" if Preview takes a long time.
0277     QTimer::singleShot(10, this, [this] {
0278         if (_previewJob == nullptr) {
0279             return;
0280         }
0281         if (!_thumbnailFinished) {
0282             QToolTip::showText(_thumbnailPos, i18n("Generating Thumbnail"), qApp->focusWidget());
0283         }
0284     });
0285 
0286     _previewJob = new KIO::PreviewJob(KFileItemList({fileItem()}), QSize(size, size));
0287     connect(_previewJob, &KIO::PreviewJob::gotPreview, this, &FileFilterHotSpot::showThumbnail);
0288     connect(_previewJob, &KIO::PreviewJob::failed, this, [] {
0289         qCDebug(KonsoleDebug) << "Error generating the preview" << _previewJob->errorString();
0290         QToolTip::hideText();
0291     });
0292 
0293     _previewJob->setAutoDelete(true);
0294     _previewJob->start();
0295 }
0296 
0297 KFileItem FileFilterHotSpot::fileItem() const
0298 {
0299     return KFileItem(QUrl::fromLocalFile(_filePath));
0300 }
0301 
0302 void FileFilterHotSpot::mouseEnterEvent(TerminalDisplay *td, QMouseEvent *ev)
0303 {
0304     HotSpot::mouseEnterEvent(td, ev);
0305     requestThumbnail(ev->modifiers(), ev->globalPosition().toPoint());
0306 }
0307 
0308 void FileFilterHotSpot::mouseLeaveEvent(TerminalDisplay *td, QMouseEvent *ev)
0309 {
0310     HotSpot::mouseLeaveEvent(td, ev);
0311     stopThumbnailGeneration();
0312 }
0313 
0314 void FileFilterHotSpot::keyPressEvent(Konsole::TerminalDisplay *td, QKeyEvent *ev)
0315 {
0316     HotSpot::keyPressEvent(td, ev);
0317     requestThumbnail(ev->modifiers(), QCursor::pos());
0318 }
0319 
0320 bool FileFilterHotSpot::hasDragOperation() const
0321 {
0322     return true;
0323 }
0324 
0325 void FileFilterHotSpot::startDrag()
0326 {
0327     auto *drag = new QDrag(this);
0328     auto *mimeData = new QMimeData();
0329     mimeData->setUrls({QUrl::fromLocalFile(_filePath)});
0330 
0331     drag->setMimeData(mimeData);
0332     drag->exec(Qt::CopyAction);
0333 }
0334 
0335 #include "moc_FileFilterHotspot.cpp"