File indexing completed on 2024-04-28 05:49:02

0001 /*
0002  *  SPDX-FileCopyrightText: 2017 Friedrich W. H. Kossebau <kossebau@kde.org>
0003  *
0004  *  SPDX-License-Identifier: LGPL-2.0-or-later
0005  */
0006 
0007 #include "kpartview.h"
0008 
0009 #include <ktexteditorpreview_debug.h>
0010 
0011 // KF
0012 #include <KTextEditor/Document>
0013 
0014 #include <KActionCollection>
0015 #include <KLocalizedString>
0016 #include <KParts/ReadOnlyPart>
0017 #include <KPluginFactory>
0018 #include <kparts_version.h>
0019 
0020 #include <KParts/NavigationExtension>
0021 
0022 // Qt
0023 #include <QDesktopServices>
0024 #include <QEvent>
0025 #include <QKeyEvent>
0026 #include <QKeySequence>
0027 #include <QLabel>
0028 #include <QTemporaryFile>
0029 
0030 using namespace KTextEditorPreview;
0031 
0032 // There are two timers that run on update. One timer is fast, but is
0033 // cancelled each time a new updated comes in. Another timer is slow, but is
0034 // not cancelled if another update comes in. With this, "while typing", the
0035 // preview is updated every 1000ms, thus one sees that something is happening
0036 // from the corner of one's eyes. After stopping typing, the preview is
0037 // updated quickly after 150ms so that the preview has the newest version.
0038 static const int updateDelayFast = 150; // ms
0039 static const int updateDelaySlow = 1000; // ms
0040 
0041 KPartView::KPartView(const KPluginMetaData &service, QObject *parent)
0042     : QObject(parent)
0043 {
0044     auto factoryResult = KPluginFactory::loadFactory(service.fileName());
0045     if (!factoryResult.plugin) {
0046         m_errorLabel = new QLabel(factoryResult.errorString);
0047     } else {
0048         m_part = factoryResult.plugin->create<KParts::ReadOnlyPart>(this);
0049     }
0050 
0051     if (!m_part) {
0052         m_errorLabel = new QLabel(factoryResult.errorString);
0053     } else if (!m_part->widget()) {
0054         // should not happen, but just be safe
0055         delete m_part;
0056         m_errorLabel = new QLabel(QStringLiteral("KPart provides no widget."));
0057     } else {
0058         m_updateSquashingTimerFast.setSingleShot(true);
0059         m_updateSquashingTimerFast.setInterval(updateDelayFast);
0060         connect(&m_updateSquashingTimerFast, &QTimer::timeout, this, &KPartView::updatePreview);
0061 
0062         m_updateSquashingTimerSlow.setSingleShot(true);
0063         m_updateSquashingTimerSlow.setInterval(updateDelaySlow);
0064         connect(&m_updateSquashingTimerSlow, &QTimer::timeout, this, &KPartView::updatePreview);
0065         auto browserExtension = m_part->navigationExtension();
0066         if (browserExtension) {
0067             connect(browserExtension, &KParts::NavigationExtension::openUrlRequestDelayed, this, &KPartView::handleOpenUrlRequest);
0068         }
0069         m_part->widget()->installEventFilter(this);
0070 
0071         // Register all shortcuts of the KParts actionCollection to eat them in the
0072         // event filter before they are handled by the application (and potentially
0073         // identified as ambiguous).
0074         // Also restrict the shortcuts to the m_part widget by setting the shortcut context.
0075         m_shortcuts.clear();
0076         const auto actions = m_part->actionCollection()->actions();
0077         for (auto *action : actions) {
0078             const auto shortcuts = action->shortcuts();
0079             for (const auto &shortcut : shortcuts) {
0080                 m_shortcuts[shortcut] = action;
0081             }
0082             if (action->shortcutContext() != Qt::WidgetShortcut) {
0083                 action->setShortcutContext(Qt::WidgetWithChildrenShortcut);
0084             }
0085         }
0086     }
0087 }
0088 
0089 KPartView::~KPartView()
0090 {
0091     delete m_errorLabel;
0092 }
0093 
0094 QWidget *KPartView::widget() const
0095 {
0096     return m_part ? m_part->widget() : m_errorLabel;
0097 }
0098 
0099 KParts::ReadOnlyPart *KPartView::kPart() const
0100 {
0101     return m_part;
0102 }
0103 
0104 KTextEditor::Document *KPartView::document() const
0105 {
0106     return m_document;
0107 }
0108 
0109 bool KPartView::isAutoUpdating() const
0110 {
0111     return m_autoUpdating;
0112 }
0113 
0114 void KPartView::setDocument(KTextEditor::Document *document)
0115 {
0116     if (m_document == document) {
0117         return;
0118     }
0119     if (!m_part) {
0120         return;
0121     }
0122 
0123     if (m_document) {
0124         disconnect(m_document, &KTextEditor::Document::textChanged, this, &KPartView::triggerUpdatePreview);
0125         m_updateSquashingTimerFast.stop();
0126         m_updateSquashingTimerSlow.stop();
0127     }
0128 
0129     m_document = document;
0130 
0131     // delete any temporary file, to trigger creation of a new if needed
0132     // for some unique url/path of the temporary file for the new document (or use a counter ourselves?)
0133     // but see comment for stream url
0134     delete m_bufferFile;
0135     m_bufferFile = nullptr;
0136 
0137     if (m_document) {
0138         m_previewDirty = true;
0139         updatePreview();
0140         connect(m_document, &KTextEditor::Document::textChanged, this, &KPartView::triggerUpdatePreview);
0141     } else {
0142         m_part->closeUrl();
0143     }
0144 }
0145 
0146 void KPartView::setAutoUpdating(bool autoUpdating)
0147 {
0148     if (m_autoUpdating == autoUpdating) {
0149         return;
0150     }
0151 
0152     m_autoUpdating = autoUpdating;
0153 
0154     if (m_autoUpdating) {
0155         if (m_document && m_part && m_previewDirty) {
0156             updatePreview();
0157         }
0158     } else {
0159         m_updateSquashingTimerSlow.stop();
0160         m_updateSquashingTimerFast.stop();
0161     }
0162 }
0163 
0164 void KPartView::triggerUpdatePreview()
0165 {
0166     m_previewDirty = true;
0167 
0168     if (m_part->widget()->isVisible() && m_autoUpdating) {
0169         // Reset fast timer each time
0170         m_updateSquashingTimerFast.start();
0171         // Start slow timer, if not already running (don't reset!)
0172         if (!m_updateSquashingTimerSlow.isActive()) {
0173             m_updateSquashingTimerSlow.start();
0174         }
0175     }
0176 }
0177 
0178 void KPartView::updatePreview()
0179 {
0180     m_updateSquashingTimerSlow.stop();
0181     m_updateSquashingTimerFast.stop();
0182     if (!m_part->widget()->isVisible()) {
0183         return;
0184     }
0185 
0186     // TODO: some kparts seem to steal the focus after they have loaded a file, sometimes also async
0187     // that possibly needs fixing in the respective kparts, as that could be considered non-cooperative
0188 
0189     // TODO: investigate if pushing of the data to the kpart could be done in a non-gui-thread,
0190     // so their loading of the file (e.g. ReadOnlyPart::openFile() is sync design) does not block
0191 
0192     const auto mimeType = m_document->mimeType();
0193     KParts::OpenUrlArguments arguments;
0194     arguments.setMimeType(mimeType);
0195     m_part->setArguments(arguments);
0196 
0197     // try to stream the data to avoid filesystem I/O
0198     // create url unique for this document
0199     // TODO: encode existing url instead, and for yet-to-be-stored docs some other unique id
0200     const QUrl streamUrl(QStringLiteral("ktexteditorpreview:/object/%1").arg(reinterpret_cast<quintptr>(m_document), 0, 16));
0201     if (m_part->openStream(mimeType, streamUrl)) {
0202         qCDebug(KTEPREVIEW) << "Pushing data via streaming API, url:" << streamUrl.url();
0203         m_part->writeStream(m_document->text().toUtf8());
0204         m_part->closeStream();
0205 
0206         m_previewDirty = false;
0207         return;
0208     }
0209 
0210     // have to go via filesystem for now, not nice
0211     if (!m_bufferFile) {
0212         m_bufferFile = new QTemporaryFile(this);
0213         m_bufferFile->open();
0214     } else {
0215         // reset position
0216         m_bufferFile->seek(0);
0217     }
0218     const QUrl tempFileUrl(QUrl::fromLocalFile(m_bufferFile->fileName()));
0219     qCDebug(KTEPREVIEW) << "Pushing data via temporary file, url:" << tempFileUrl.url();
0220 
0221     // write current data
0222     m_bufferFile->write(m_document->text().toUtf8());
0223     // truncate at end of new content
0224     m_bufferFile->resize(m_bufferFile->pos());
0225     m_bufferFile->flush();
0226 
0227     // TODO: find out why we need to send this queued
0228     QMetaObject::invokeMethod(m_part, "openUrl", Qt::QueuedConnection, Q_ARG(QUrl, tempFileUrl));
0229 
0230     m_previewDirty = false;
0231 }
0232 
0233 void KPartView::handleOpenUrlRequest(const QUrl &url)
0234 {
0235     QDesktopServices::openUrl(url);
0236 }
0237 
0238 bool KPartView::eventFilter(QObject *object, QEvent *event)
0239 {
0240     if (object == m_part->widget() && event->type() == QEvent::Show) {
0241         if (m_document && m_autoUpdating && m_previewDirty) {
0242             updatePreview();
0243         }
0244         return true;
0245     } else if (event->type() == QEvent::ShortcutOverride) {
0246         const auto keyEvent = static_cast<const QKeyEvent *>(event);
0247         auto *const action = m_shortcuts.value(QKeySequence(keyEvent->modifiers() | keyEvent->key()));
0248         if (action) {
0249             action->trigger();
0250             event->accept();
0251             return true;
0252         }
0253     }
0254 
0255     return QObject::eventFilter(object, event);
0256 }
0257 
0258 #include "moc_kpartview.cpp"