diff --git a/addons/CMakeLists.txt b/addons/CMakeLists.txt --- a/addons/CMakeLists.txt +++ b/addons/CMakeLists.txt @@ -71,6 +71,9 @@ ecm_optional_add_subdirectory (snippets) endif() +# live preview of sources in target format +ecm_optional_add_subdirectory (preview) + # terminal tool view if(KF5Service_FOUND AND NOT WIN32) ecm_optional_add_subdirectory (konsole) diff --git a/addons/preview/CMakeLists.txt b/addons/preview/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/addons/preview/CMakeLists.txt @@ -0,0 +1,17 @@ +add_definitions(-DTRANSLATION_DOMAIN=\"ktexteditorpreviewplugin\") + +set(ktexteditorpreviewplugin_SRCS + ktexteditorpreviewplugin.cpp + ktexteditorpreviewview.cpp + previewwidget.cpp + kpartview.cpp +) + +add_library(ktexteditorpreviewplugin MODULE ${ktexteditorpreviewplugin_SRCS}) + +target_link_libraries(ktexteditorpreviewplugin + KF5::TextEditor + KF5::I18n +) + +install(TARGETS ktexteditorpreviewplugin DESTINATION ${KDE_INSTALL_PLUGINDIR}/ktexteditor) diff --git a/addons/preview/Message.sh b/addons/preview/Message.sh new file mode 100644 --- /dev/null +++ b/addons/preview/Message.sh @@ -0,0 +1,2 @@ +#! /usr/bin/env bash +$XGETTEXT *.cpp -o $podir/ktexteditorpreviewplugin.pot diff --git a/addons/preview/kpartview.h b/addons/preview/kpartview.h new file mode 100644 --- /dev/null +++ b/addons/preview/kpartview.h @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2017 by Friedrich W. H. Kossebau + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#ifndef KPARTVIEW_H +#define KPARTVIEW_H + +#include + +#include +#include + +namespace KTextEditor { +class Document; +} +namespace KParts { +class ReadOnlyPart; +} +class QLabel; +class QTemporaryFile; + +/** + * Wrapper around a KPart which handles feeding it the content of a text document + * + * The class creates a KPart from the service description passed in the constructor + * and then takes care for feeding the content of the currently set text document + * to the KPart for preview in the target format. both on changes in the document + * or when a new document is set. + * + * The content is pushed via the KParts stream API, if the KPart instance + * supports it, or as fallback via the filesystem, using a QTemporaryFile instance. + * Updates are squashed via a timer, to reduce load. + */ +class KPartView : public QObject +{ + Q_OBJECT + +public: + /** + * Constructor + * + * @param service the description of the KPart which should be used, may not be a nullptr + * @param parent the object taking ownership, can be a nullptr + */ + KPartView(const KService::Ptr& service, QObject* parent); + ~KPartView() override; + + /** + * Returns the widget object, Ownership is not transferred. + */ + QWidget* widget() const; + + /** + * Sets the current document whose content should be previewed by the KPart. + * + * @param document the document or, if there is none to preview, a nullptr + */ + void setDocument(KTextEditor::Document* document); + + /** + * Returns the current document whose content is previewed by the KPart. + * + * @return current document or, if there is none, a nullptr + */ + KTextEditor::Document* document() const; + +private: + void triggerUpdatePreview(); + void updatePreview(); + void handleOpenUrlRequest(const QUrl& url); + +private: + QLabel* m_errorLabel = nullptr; + KParts::ReadOnlyPart* m_part = nullptr; + KTextEditor::Document* m_document = nullptr; + + QTimer m_updateSquashingTimer; + QTemporaryFile* m_bufferFile = nullptr; +}; + +#endif diff --git a/addons/preview/kpartview.cpp b/addons/preview/kpartview.cpp new file mode 100644 --- /dev/null +++ b/addons/preview/kpartview.cpp @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2017 by Friedrich W. H. Kossebau + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#include "kpartview.h" + +// KF +#include + +#include +#include +#include +#include +#include + +// Qt +#include +#include +#include +#include + + +static const int updateDelay = 300; // ms + +KPartView::KPartView(const KService::Ptr& service, QObject* parent) + : QObject(parent) +{ + QString errorString; + m_part = service->createInstance(nullptr, this, QVariantList(), &errorString); + + if (!m_part) { + m_errorLabel = new QLabel(errorString); + } else { + m_updateSquashingTimer.setSingleShot(true); + m_updateSquashingTimer.setInterval(updateDelay); + connect(&m_updateSquashingTimer, &QTimer::timeout, this, &KPartView::updatePreview); + + auto browserExtension = m_part->browserExtension(); + if (browserExtension) { + connect(browserExtension, &KParts::BrowserExtension::openUrlRequestDelayed, + this, &KPartView::handleOpenUrlRequest); + } + } +} + +KPartView::~KPartView() +{ + delete m_errorLabel; +} + +QWidget* KPartView::widget() const +{ + return m_part ? m_part->widget() : m_errorLabel; +} + +KTextEditor::Document* KPartView::document() const +{ + return m_document; +} + +void KPartView::setDocument(KTextEditor::Document* document) +{ + if (m_document == document) { + return; + } + if (!m_part) { + return; + } + + if (m_document) { + disconnect(m_document, &KTextEditor::Document::textChanged, this, &KPartView::triggerUpdatePreview); + m_updateSquashingTimer.stop(); + } + + m_document = document; + + if (m_document) { + updatePreview(); + connect(m_document, &KTextEditor::Document::textChanged, this, &KPartView::triggerUpdatePreview); + } else { + m_part->closeUrl(); + } +} + +void KPartView::triggerUpdatePreview() +{ + if (!m_updateSquashingTimer.isActive()) { + m_updateSquashingTimer.start(); + } +} + +void KPartView::updatePreview() +{ + // try to stream the data to avoid filesystem I/O + if (m_part->openStream(m_document->mimeType(), QUrl(QStringLiteral("ktexteditorpreview:/data")))) { + m_part->writeStream(m_document->text().toUtf8()); + m_part->closeStream(); + return; + } + + // have to go via filesystem for now, not nice + if (!m_bufferFile) { + m_bufferFile = new QTemporaryFile(this); + } else { + // drop any old data + m_bufferFile->close(); + } + + // write current data + m_bufferFile->open(); + m_bufferFile->write(m_document->text().toUtf8()); + // truncate at end of new content + m_bufferFile->resize(m_bufferFile->pos()); + m_bufferFile->flush(); + + // TODO: find out why we need to send this queued + QMetaObject::invokeMethod(m_part, "openUrl", Qt::QueuedConnection, Q_ARG(QUrl, QUrl::fromLocalFile(m_bufferFile->fileName()))); +} + +void KPartView::handleOpenUrlRequest(const QUrl& url) +{ + QDesktopServices::openUrl(url); +} diff --git a/addons/preview/ktexteditorpreview.json b/addons/preview/ktexteditorpreview.json new file mode 100644 --- /dev/null +++ b/addons/preview/ktexteditorpreview.json @@ -0,0 +1,18 @@ +{ + "KPlugin": { + "Authors": [ + { + "Email": "kossebau@kde.org", + "Name": "Friedrich W. H. Kossebau" + } + ], + "Description": "Preview the document in the target format", + "Icon": "document-preview", + "Name": "Document Preview", + "Id": "ktexteditorpreview", + "ServiceTypes": [ + "KTextEditor/Plugin", + "KDevelop/Plugin" + ] + } +} diff --git a/addons/preview/ktexteditorpreviewplugin.h b/addons/preview/ktexteditorpreviewplugin.h new file mode 100644 --- /dev/null +++ b/addons/preview/ktexteditorpreviewplugin.h @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2017 by Friedrich W. H. Kossebau + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#ifndef KTEXTEDITORPREVIEWPLUGIN_H +#define KTEXTEDITORPREVIEWPLUGIN_H + +// KF +#include + +class KPartPreviewer; + +class KTextEditorPreviewPlugin : public KTextEditor::Plugin +{ + Q_OBJECT + +public: + /** + * Default constructor, with arguments as expected by KPluginFactory + */ + KTextEditorPreviewPlugin(QObject* parent, const QVariantList& args); + + ~KTextEditorPreviewPlugin() override; + +public: // KTextEditor::Plugin API + QObject* createView(KTextEditor::MainWindow* mainWindow) override; +}; + +#endif diff --git a/addons/preview/ktexteditorpreviewplugin.cpp b/addons/preview/ktexteditorpreviewplugin.cpp new file mode 100644 --- /dev/null +++ b/addons/preview/ktexteditorpreviewplugin.cpp @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2017 by Friedrich W. H. Kossebau + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#include "ktexteditorpreviewplugin.h" + +#include "ktexteditorpreviewview.h" + +// KF +#include +#include + +// Qt +#include + + +K_PLUGIN_FACTORY_WITH_JSON(KTextEditorPreviewPluginFactory, "ktexteditorpreview.json", registerPlugin();) + + +KTextEditorPreviewPlugin::KTextEditorPreviewPlugin(QObject* parent, const QVariantList& /*args*/) + : KTextEditor::Plugin(parent) +{ +} + +KTextEditorPreviewPlugin::~KTextEditorPreviewPlugin() = default; + +QObject* KTextEditorPreviewPlugin::createView(KTextEditor::MainWindow* mainwindow) +{ + return new KTextEditorPreviewView(this, mainwindow); +} + + +// needed for K_PLUGIN_FACTORY_WITH_JSON +#include diff --git a/addons/preview/ktexteditorpreviewview.h b/addons/preview/ktexteditorpreviewview.h new file mode 100644 --- /dev/null +++ b/addons/preview/ktexteditorpreviewview.h @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2017 by Friedrich W. H. Kossebau + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#ifndef KTEXTEDITORPREVIEWVIEW_H +#define KTEXTEDITORPREVIEWVIEW_H + +// Qt headers +#include +#include + +class PreviewWidget; + +namespace KTextEditor { +class MainWindow; +class View; +} + +class KTextEditorPreviewPlugin; + +class QWidget; + +class KTextEditorPreviewView: public QObject +{ + Q_OBJECT + +public: + KTextEditorPreviewView(KTextEditorPreviewPlugin* plugin, KTextEditor::MainWindow* mainWindow); + ~KTextEditorPreviewView() override; + +public Q_SLOTS: + void handleViewChanged(KTextEditor::View* view); + +private: + QPointer m_toolView; + PreviewWidget* m_previewView; +}; + +#endif diff --git a/addons/preview/ktexteditorpreviewview.cpp b/addons/preview/ktexteditorpreviewview.cpp new file mode 100644 --- /dev/null +++ b/addons/preview/ktexteditorpreviewview.cpp @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2017 by Friedrich W. H. Kossebau + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#include "ktexteditorpreviewview.h" + +#include "ktexteditorpreviewplugin.h" +#include "previewwidget.h" + +// KF +#include +#include +#include + +#include + +// Qt +#include +#include + +KTextEditorPreviewView::KTextEditorPreviewView(KTextEditorPreviewPlugin* plugin, KTextEditor::MainWindow* mainWindow) + : QObject(mainWindow) +{ + Q_UNUSED(plugin); + + // Toolview for snippets + m_toolView = mainWindow->createToolView(plugin, QStringLiteral("ktexteditorpreviewplugin"), + KTextEditor::MainWindow::Right, + QIcon::fromTheme(QStringLiteral("document-preview")), + i18n("Preview")); + + // add preview widget + m_previewView = new PreviewWidget(plugin, m_toolView.data()); + m_toolView->layout()->setMargin(0); + m_toolView->layout()->addWidget(m_previewView); + m_toolView->addActions(m_previewView->actions()); + + connect(mainWindow, SIGNAL(viewChanged(KTextEditor::View*)), + this, SLOT(handleViewChanged(KTextEditor::View*))); + + m_previewView->setTextEditorView(mainWindow->activeView()); +} + +KTextEditorPreviewView::~KTextEditorPreviewView() +{ +} + +void KTextEditorPreviewView::handleViewChanged(KTextEditor::View* view) +{ + m_previewView->setTextEditorView(view); +} diff --git a/addons/preview/previewwidget.h b/addons/preview/previewwidget.h new file mode 100644 --- /dev/null +++ b/addons/preview/previewwidget.h @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2017 by Friedrich W. H. Kossebau + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#ifndef PREVIEWWIDGET_H +#define PREVIEWWIDGET_H + +// Qt +#include + +class KTextEditorPreviewPlugin; +class KPartView; + +namespace KTextEditor { +class View; +} +class KToggleAction; + +/** + * The actual widget shown in the toolview. + * + * It either shows a label "No preview available." + * or the widget of the KPart currently used to preview + * the selected document in its target format. + * + * The preview can be locked to a document by checking off the + * lock action. If locked the currently shown document will not + * be changed if another view is activated, unless the document + * itself is closed, where then the label is shown instead. + */ +class PreviewWidget: public QStackedWidget +{ + Q_OBJECT + +public: + /** + * Constructor + * + * @param core the plugin object + * @param parent widget object taking the ownership + */ + PreviewWidget(KTextEditorPreviewPlugin* core, QWidget* parent); + ~PreviewWidget() override; + +public: + /** + * Update the widget to the currently active view. + * + * If the document lock is set, this will not change the preview. + * Otherwise the preview will switch to the document of the now active view + * and show a preview for that, unless there is no matching kpart found. + * In that case, or if there is no active view, a label will be shown with + * "No preview available". + * + * @param view the view or, if there is none, a nullptr + */ + void setTextEditorView(KTextEditor::View* view); + +private: + void toggleDocumentLocking(bool locked); + void handleLockedDocumentClosing(); + +private: + KToggleAction* m_lockAction; + + KTextEditorPreviewPlugin* const m_core; + + KTextEditor::View* m_currentTextEditorView = nullptr; + QString m_currentServiceId; + KPartView* m_partView = nullptr; +}; + +#endif diff --git a/addons/preview/previewwidget.cpp b/addons/preview/previewwidget.cpp new file mode 100644 --- /dev/null +++ b/addons/preview/previewwidget.cpp @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2017 by Friedrich W. H. Kossebau + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#include "previewwidget.h" + +#include "ktexteditorpreviewplugin.h" +#include "kpartview.h" + +// KF +#include +#include + +#include +#include + +#include +#include +#include + +// Qt +#include +#include +#include +#include + + +PreviewWidget::PreviewWidget(KTextEditorPreviewPlugin* core, QWidget* parent) + : QStackedWidget(parent) + , m_core(core) +{ + m_lockAction = new KToggleAction(QIcon::fromTheme(QStringLiteral("object-unlocked")), i18n("Lock Current Document"), this); + m_lockAction->setToolTip(i18n("Lock preview to current document")); + m_lockAction->setCheckedState(KGuiItem(i18n("Unlock Current View"), QIcon::fromTheme(QStringLiteral("object-locked")), i18n("Unlock current view"))); + m_lockAction->setChecked(false); + connect(m_lockAction, &QAction::triggered, this, &PreviewWidget::toggleDocumentLocking); + addAction(m_lockAction); + + auto label = new QLabel(i18n("No preview available."), this); + label->setAlignment(Qt::AlignHCenter); + addWidget(label); +} + +PreviewWidget::~PreviewWidget() = default; + +void PreviewWidget::setTextEditorView(KTextEditor::View* view) +{ + m_currentTextEditorView = view; + + if (m_lockAction->isChecked()) + return; + + KService::Ptr service; + if (view) { + service = KMimeTypeTrader::self()->preferredService(view->document()->mimeType(), + QStringLiteral("KParts/ReadOnlyPart")); + // no interest in kparts which also just display the text (like katepart itself) + // TODO: what about parts which also support importing plain text and turning into richer format + // and thus have it in their mimetypes list? + // could that perhaps be solved by introducing the concept of "native" and "imported" mimetypes? + if (service && service->mimeTypes().contains(QStringLiteral("text/plain"))) { + service.reset(); + } + } + + // change of preview type? + // TODO: find a better id than library? + const QString serviceId = service ? service->library() : QString(); + + if (serviceId != m_currentServiceId) { + if (m_partView) { + removeWidget(m_partView->widget()); + delete m_partView; + } + + m_currentServiceId = serviceId; + + if (service) { + m_partView = new KPartView(service, this); + int index = addWidget(m_partView->widget()); + setCurrentIndex(index); + } else { + m_partView = nullptr; + } + } + + if (m_partView) { + m_partView->setDocument(view ? view->document() : nullptr); + } +} + +void PreviewWidget::toggleDocumentLocking(bool locked) +{ + if (locked) { + if (!m_partView) { + // nothing to do + return; + } + auto document = m_partView->document(); + connect(document, &KTextEditor::Document::aboutToClose, + this, &PreviewWidget::handleLockedDocumentClosing); + } else { + if (m_partView) { + auto document = m_partView->document(); + disconnect(document, &KTextEditor::Document::aboutToClose, + this, &PreviewWidget::handleLockedDocumentClosing); + } + // jump tp current view + setTextEditorView(m_currentTextEditorView); + } +} + +void PreviewWidget::handleLockedDocumentClosing() +{ + // remove any current partview + if (m_partView) { + removeWidget(m_partView->widget()); + delete m_partView; + m_partView = nullptr; + } + + m_currentServiceId.clear(); +}