X-Git-Url: https://cloud.milkyroute.net/gitweb/dolphin.git/blobdiff_plain/f37ecd6ecfab9bc1d2929504b4f6e4363f8137b9..cebcf8db:/src/panels/terminal/terminalpanel.cpp diff --git a/src/panels/terminal/terminalpanel.cpp b/src/panels/terminal/terminalpanel.cpp index cc3b85318..e9184e738 100644 --- a/src/panels/terminal/terminalpanel.cpp +++ b/src/panels/terminal/terminalpanel.cpp @@ -1,96 +1,347 @@ -/*************************************************************************** - * Copyright (C) 2007 by Peter Penz * - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation; either version 2 of the License, or * - * (at your option) any later version. * - * * - * This program 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 General Public License for more details. * - * * - * You should have received a copy of the GNU General Public License * - * along with this program; if not, write to the * - * Free Software Foundation, Inc., * - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * - ***************************************************************************/ +/* + * SPDX-FileCopyrightText: 2007-2010 Peter Penz + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ #include "terminalpanel.h" -#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include -#include -#include -#include +#include +#include +#include #include +#include +#include -TerminalPanel::TerminalPanel(QWidget* parent) : - Panel(parent), - m_layout(0), - m_terminal(0), - m_terminalWidget(0) +TerminalPanel::TerminalPanel(QWidget *parent) + : Panel(parent) + , m_clearTerminal(true) + , m_mostLocalUrlJob(nullptr) + , m_layout(nullptr) + , m_terminal(nullptr) + , m_terminalWidget(nullptr) + , m_konsolePartMissingMessage(nullptr) + , m_konsolePart(nullptr) + , m_konsolePartCurrentDirectory() + , m_sendCdToTerminalHistory() + , m_kiofuseInterface(QStringLiteral("org.kde.KIOFuse"), QStringLiteral("/org/kde/KIOFuse"), QDBusConnection::sessionBus()) { m_layout = new QVBoxLayout(this); - m_layout->setMargin(0); + m_layout->setContentsMargins(0, 0, 0, 0); } TerminalPanel::~TerminalPanel() { + if (m_konsolePart) { + // Avoid when QObject cleanup, which comes after our destructor, deletes the konsolePart + // and subsequently calls back into our slot when the destructor has already run. + disconnect(m_konsolePart, &KParts::ReadOnlyPart::destroyed, this, &TerminalPanel::terminalExited); + } } -QSize TerminalPanel::sizeHint() const +void TerminalPanel::goHome() { - QSize size = Panel::sizeHint(); - size.setHeight(200); - return size; + sendCdToTerminal(QDir::homePath(), HistoryPolicy::SkipHistory); } -void TerminalPanel::setUrl(const KUrl& url) +bool TerminalPanel::currentWorkingDirectoryIsChildOf(const QString &path) const { - if (!url.isValid() || (url == Panel::url())) { - return; + if (m_terminal) { + return m_terminal->currentWorkingDirectory().startsWith(path); } + return false; +} - Panel::setUrl(url); - if ((m_terminal != 0) && isVisible() && url.isLocalFile()) { - m_terminal->sendInput("cd " + KShell::quoteArg(url.path()) + '\n'); +void TerminalPanel::terminalExited() +{ + m_terminal = nullptr; + m_konsolePart = nullptr; + Q_EMIT hideTerminalPanel(); +} + +bool TerminalPanel::isHiddenInVisibleWindow() const +{ + return parentWidget() && parentWidget()->isHidden(); +} + +void TerminalPanel::dockVisibilityChanged() +{ + // Only react when the DockWidget itself (not some parent) is hidden. This way we don't + // respond when e.g. Dolphin is minimized. + if (isHiddenInVisibleWindow()) { + if (m_konsolePartMissingMessage) { + m_konsolePartMissingMessage->hide(); + } else if (m_terminal && !hasProgramRunning()) { + // Make sure that the following "cd /" command will not affect the view. + disconnect(m_konsolePart, SIGNAL(currentDirectoryChanged(QString)), this, SLOT(slotKonsolePartCurrentDirectoryChanged(QString))); + + // Make sure this terminal does not prevent unmounting any removable drives + changeDir(QUrl::fromLocalFile(QStringLiteral("/"))); + + // Because we have disconnected from the part's currentDirectoryChanged() + // signal, we have to update m_konsolePartCurrentDirectory manually. If this + // was not done, showing the panel again might not set the part's working + // directory correctly. + m_konsolePartCurrentDirectory = '/'; + } } } -void TerminalPanel::terminalExited() +QString TerminalPanel::runningProgramName() const +{ + return m_terminal ? m_terminal->foregroundProcessName() : QString(); +} + +KActionCollection *TerminalPanel::actionCollection() +{ + // m_terminal is the only reference reset to nullptr in case the terminal is + // closed again + if (m_terminal && m_konsolePart && m_terminalWidget) { + const auto guiClients = m_konsolePart->childClients(); + for (auto *client : guiClients) { + if (client->actionCollection()->associatedWidgets().contains(m_terminalWidget)) { + return client->actionCollection(); + } + } + } + return nullptr; +} + +bool TerminalPanel::hasProgramRunning() const { - emit hideTerminalPanel(); - m_terminal = 0; + return m_terminal && (m_terminal->foregroundProcessId() != -1); } -void TerminalPanel::showEvent(QShowEvent* event) +bool TerminalPanel::urlChanged() +{ + if (!url().isValid()) { + return false; + } + + const bool sendInput = m_terminal && !hasProgramRunning() && isVisible(); + if (sendInput) { + changeDir(url()); + } + + return true; +} + +void TerminalPanel::showEvent(QShowEvent *event) { if (event->spontaneous()) { Panel::showEvent(event); return; } - if (m_terminal == 0) { - KPluginFactory* factory = KPluginLoader("libkonsolepart").factory(); - KParts::ReadOnlyPart* part = factory ? (factory->create(this)) : 0; - if (part != 0) { - connect(part, SIGNAL(destroyed(QObject*)), this, SLOT(terminalExited())); - m_terminalWidget = part->widget(); + if (!m_terminal) { + m_clearTerminal = true; + KPluginFactory *factory = KPluginFactory::loadFactory(KPluginMetaData(QStringLiteral("kf6/parts/konsolepart"))).plugin; + m_konsolePart = factory ? (factory->create(this)) : nullptr; + if (m_konsolePart) { + connect(m_konsolePart, &KParts::ReadOnlyPart::destroyed, this, &TerminalPanel::terminalExited); + m_terminalWidget = m_konsolePart->widget(); + setFocusProxy(m_terminalWidget); m_layout->addWidget(m_terminalWidget); - m_terminal = qobject_cast(part); - m_terminal->showShellInDir(url().path()); + if (m_konsolePartMissingMessage) { + m_layout->removeWidget(m_konsolePartMissingMessage); + } + m_terminal = qobject_cast(m_konsolePart); + + // needed to collect the correct KonsolePart actionCollection + // namely the one of the single inner terminal and not the outer KonsolePart + if (!m_konsolePart->factory() && m_terminalWidget) { + if (!m_konsolePart->clientBuilder()) { + m_konsolePart->setClientBuilder(new KXMLGUIBuilder(m_terminalWidget)); + } + + auto factory = new KXMLGUIFactory(m_konsolePart->clientBuilder(), this); + factory->addClient(m_konsolePart); + + // Prevents the KXMLGui warning about removing the client + connect(m_terminalWidget, &QObject::destroyed, this, [factory, this] { + factory->removeClient(m_konsolePart); + }); + } + + } else { + if (!m_konsolePartMissingMessage) { + const auto konsoleInstallUrl = QUrl("appstream://org.kde.konsole.desktop"); + const auto konsoleNotInstalledText = i18n( + "Terminal cannot be shown because Konsole is not installed. " + "Please install it and then reopen the panel."); + m_konsolePartMissingMessage = new KMessageWidget(konsoleNotInstalledText, this); + m_konsolePartMissingMessage->setPosition(KMessageWidget::Footer); + m_konsolePartMissingMessage->setCloseButtonVisible(false); + m_konsolePartMissingMessage->hide(); + if (KIO::DesktopExecParser::hasSchemeHandler(konsoleInstallUrl)) { + auto installKonsoleAction = new QAction(i18n("Install Konsole"), this); + connect(installKonsoleAction, &QAction::triggered, [konsoleInstallUrl]() { + QDesktopServices::openUrl(konsoleInstallUrl); + }); + m_konsolePartMissingMessage->addAction(installKonsoleAction); + } + m_layout->addWidget(m_konsolePartMissingMessage); + m_layout->setSizeConstraint(QLayout::SetMaximumSize); + } + + m_konsolePartMissingMessage->animatedShow(); } } - if (m_terminal != 0) { - m_terminal->sendInput("cd " + KShell::quoteArg(url().path()) + '\n'); - m_terminal->sendInput("clear\n"); + if (m_terminal) { + m_terminal->showShellInDir(url().toLocalFile()); + if (!hasProgramRunning()) { + changeDir(url()); + } m_terminalWidget->setFocus(); + connect(m_konsolePart, SIGNAL(currentDirectoryChanged(QString)), this, SLOT(slotKonsolePartCurrentDirectoryChanged(QString))); } Panel::showEvent(event); } -#include "terminalpanel.moc" +void TerminalPanel::changeDir(const QUrl &url) +{ + delete m_mostLocalUrlJob; + m_mostLocalUrlJob = nullptr; + + if (url.isLocalFile()) { + sendCdToTerminal(url.toLocalFile()); + return; + } + + // Try stat'ing the url; note that mostLocalUrl only works with ":local" protocols + if (KProtocolInfo::protocolClass(url.scheme()) == QLatin1String(":local")) { + m_mostLocalUrlJob = KIO::mostLocalUrl(url, KIO::HideProgressInfo); + if (m_mostLocalUrlJob->uiDelegate()) { + KJobWidgets::setWindow(m_mostLocalUrlJob, this); + } + connect(m_mostLocalUrlJob, &KIO::StatJob::result, this, &TerminalPanel::slotMostLocalUrlResult); + return; + } + + // Last chance, try KIOFuse + sendCdToTerminalKIOFuse(url); +} + +void TerminalPanel::sendCdToTerminal(const QString &dir, HistoryPolicy addToHistory) +{ + if (dir == m_konsolePartCurrentDirectory // We are already there + && m_sendCdToTerminalHistory.isEmpty() // …and that is not because the terminal couldn't keep up + ) { + m_clearTerminal = false; + return; + } + + // Send prior Ctrl-E, Ctrl-U to ensure the line is empty. This is + // mandatory, otherwise sending a 'cd x\n' to a prompt with 'rm -rf *' + // would result in data loss. + m_terminal->sendInput(QStringLiteral("\x05\x15")); + + // We want to ignore the currentDirectoryChanged(QString) signal, which we will receive after + // the directory change, because this directory change is not caused by a "cd" command that the + // user entered in the panel. Therefore, we have to remember 'dir'. Note that it could also be + // a symbolic link -> remember the 'canonical' path. + if (addToHistory == HistoryPolicy::AddToHistory) + m_sendCdToTerminalHistory.enqueue(QDir(dir).canonicalPath()); + + m_terminal->sendInput(" cd " + KShell::quoteArg(dir) + '\r'); + + if (m_clearTerminal) { + m_terminal->sendInput(QStringLiteral(" clear\r")); + m_clearTerminal = false; + } +} + +void TerminalPanel::sendCdToTerminalKIOFuse(const QUrl &url) +{ + // URL isn't local, only hope for the terminal to be in sync with the + // DolphinView is to mount the remote URL in KIOFuse and point to it. + // If we can't do that for any reason, silently fail. + auto reply = m_kiofuseInterface.mountUrl(url.toString()); + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply, this); + QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, [=](QDBusPendingCallWatcher *watcher) { + watcher->deleteLater(); + if (!reply.isError()) { + // Successfully mounted, point to the KIOFuse equivalent path. + sendCdToTerminal(reply.value()); + } + }); +} + +void TerminalPanel::slotMostLocalUrlResult(KJob *job) +{ + KIO::StatJob *statJob = static_cast(job); + const QUrl url = statJob->mostLocalUrl(); + if (url.isLocalFile()) { + sendCdToTerminal(url.toLocalFile()); + } else { + sendCdToTerminalKIOFuse(url); + } + + m_mostLocalUrlJob = nullptr; +} + +void TerminalPanel::slotKonsolePartCurrentDirectoryChanged(const QString &dir) +{ + m_konsolePartCurrentDirectory = QDir(dir).canonicalPath(); + + // Only emit a changeUrl signal if the directory change was caused by the user inside the + // terminal, and not by sendCdToTerminal(QString). + while (!m_sendCdToTerminalHistory.empty()) { + if (m_konsolePartCurrentDirectory == m_sendCdToTerminalHistory.dequeue()) { + return; + } + } + + // User may potentially be browsing inside a KIOFuse mount. + // If so lets try and change the DolphinView to point to the remote URL equivalent. + // instead of into the KIOFuse mount itself (which can cause performance issues!) + const QUrl url(QUrl::fromLocalFile(dir)); + + KMountPoint::Ptr mountPoint = KMountPoint::currentMountPoints().findByPath(m_konsolePartCurrentDirectory); + if (mountPoint && mountPoint->mountType() != QStringLiteral("fuse.kio-fuse")) { + // Not in KIOFUse mount, so just switch to the corresponding URL. + Q_EMIT changeUrl(url); + return; + } + + auto reply = m_kiofuseInterface.remoteUrl(m_konsolePartCurrentDirectory); + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply, this); + QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, [=](QDBusPendingCallWatcher *watcher) { + watcher->deleteLater(); + if (reply.isError()) { + // KIOFuse errored out... just show the normal URL + Q_EMIT changeUrl(url); + } else { + // Our location happens to be in a KIOFuse mount and is mounted. + // Let's change the DolphinView to point to the remote URL equivalent. + Q_EMIT changeUrl(QUrl::fromUserInput(reply.value())); + } + }); +} + +bool TerminalPanel::terminalHasFocus() const +{ + if (m_terminalWidget) { + return m_terminalWidget->hasFocus(); + } + + return hasFocus(); +} + +#include "moc_terminalpanel.cpp"