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"