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"