diff --git a/addons/CMakeLists.txt b/addons/CMakeLists.txt index c0b8f2692..d41379ae7 100644 --- a/addons/CMakeLists.txt +++ b/addons/CMakeLists.txt @@ -1,86 +1,89 @@ # detect additional frameworks find_package(KF5 "${KF5_DEP_VERSION}" OPTIONAL_COMPONENTS Wallet Plasma Service ItemModels ThreadWeaver NewStuff IconThemes GuiAddons) set_package_properties(KF5Wallet PROPERTIES PURPOSE "Required to build the katesql addon") set_package_properties(KF5Plasma PROPERTIES PURPOSE "Required to build the sessionapplet addon") set_package_properties(KF5Service PROPERTIES PURPOSE "Required to build the sessionapplet addon") set_package_properties(KF5ItemModels PROPERTIES PURPOSE "Required to build the project, konsole addon") set_package_properties(KF5ThreadWeaver PROPERTIES PURPOSE "Required to build the project addon") set_package_properties(KF5NewStuff PROPERTIES PURPOSE "Required to build the snippets and project addons") # document switcher ecm_optional_add_subdirectory (filetree) # search in open documents and files ecm_optional_add_subdirectory (search) # ALT+Tab like tab switcher ecm_optional_add_subdirectory (tabswitcher) # ctags ecm_optional_add_subdirectory (kate-ctags) # backtrace ecm_optional_add_subdirectory (backtracebrowser) # file browser ecm_optional_add_subdirectory (filebrowser) # xml completion ecm_optional_add_subdirectory (xmltools) # XML Validation plugin ecm_optional_add_subdirectory (xmlcheck) # open header matching to current file ecm_optional_add_subdirectory (openheader) # debugger plugin, needs windows love, guarded until ported to win32 if (NOT WIN32) ecm_optional_add_subdirectory (gdbplugin) endif () # list symbols and functions in a file ecm_optional_add_subdirectory (symbolviewer) # replicode integration ecm_optional_add_subdirectory (replicode) # pipe text through some external command ecm_optional_add_subdirectory (textfilter) # Rust complection plugin ecm_optional_add_subdirectory (rustcompletion) # D completion plugin ecm_optional_add_subdirectory (lumen) # build plugin ecm_optional_add_subdirectory (katebuild-plugin) # close document except this one (or similar) ecm_optional_add_subdirectory (close-except-like) if(KF5Wallet_FOUND) # kate sql ecm_optional_add_subdirectory (katesql) endif() if(KF5NewStuff_FOUND) # snippets 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) endif() if(KF5ItemModels_FOUND AND KF5ThreadWeaver_FOUND AND KF5NewStuff_FOUND) # small & smart project manager ecm_optional_add_subdirectory (project) endif() if (KF5Plasma_FOUND AND KF5Service_FOUND) ecm_optional_add_subdirectory (sessionapplet) endif() diff --git a/addons/preview/CMakeLists.txt b/addons/preview/CMakeLists.txt new file mode 100644 index 000000000..7d9fd888b --- /dev/null +++ b/addons/preview/CMakeLists.txt @@ -0,0 +1,25 @@ +add_definitions(-DTRANSLATION_DOMAIN=\"ktexteditorpreviewplugin\") + +include(ECMQtDeclareLoggingCategory) + +set(ktexteditorpreviewplugin_SRCS + ktexteditorpreviewplugin.cpp + ktexteditorpreviewview.cpp + previewwidget.cpp + kpartview.cpp +) + +ecm_qt_declare_logging_category(ktexteditorpreviewplugin_SRCS + HEADER ktexteditorpreview_debug.h + IDENTIFIER KTEPREVIEW + CATEGORY_NAME "ktexteditorpreviewplugin" +) + +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/Messages.sh b/addons/preview/Messages.sh new file mode 100644 index 000000000..4998768ad --- /dev/null +++ b/addons/preview/Messages.sh @@ -0,0 +1,2 @@ +#! /usr/bin/env bash +$XGETTEXT *.cpp -o $podir/ktexteditorpreviewplugin.pot diff --git a/addons/preview/kpartview.cpp b/addons/preview/kpartview.cpp new file mode 100644 index 000000000..5bb23ca85 --- /dev/null +++ b/addons/preview/kpartview.cpp @@ -0,0 +1,230 @@ +/* + * 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" + +#include + +// KF +#include + +#include +#include +#include +#include +#include + +// Qt +#include +#include +#include + + +using namespace KTextEditorPreview; + +// 300 ms as initial proposal, was found to be delay which worked for the +// author with the use-case of quickly peeking over to the preview while +// typing to see if things are working out as intended, without getting a +// having-to-wait feeling. +// Surely could get some more serious research what is a proper (default) value. +// Perhaps the whole update logic cpuld also be overhauled, to only do an +// update once there was at least xxx ms idle time to meet the use-case of +// quickly-peeking-over. And otherwise update in bigger intervals of +// 500-2000(?) ms, to cover the use-case of seeing from the corner of one's +// eye that something is changing while one is editing the sources. +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 if (!m_part->widget()) { + // should not happen, but just be safe + delete m_part; + m_errorLabel = new QLabel(QStringLiteral("KPart provides no widget.")); + } 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); + } + m_part->widget()->installEventFilter(this); + } +} + +KPartView::~KPartView() +{ + delete m_errorLabel; +} + +QWidget* KPartView::widget() const +{ + return m_part ? m_part->widget() : m_errorLabel; +} + +KParts::ReadOnlyPart* KPartView::kPart() const +{ + return m_part; +} + +KTextEditor::Document* KPartView::document() const +{ + return m_document; +} + +bool KPartView::isAutoUpdating() const +{ + return m_autoUpdating; +} + +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; + + // delete any temporary file, to trigger creation of a new if needed + // for some unique url/path of the temporary file for the new document (or use a counter ourselves?) + // but see comment for stream url + delete m_bufferFile; + m_bufferFile = nullptr; + + if (m_document) { + m_previewDirty = true; + updatePreview(); + connect(m_document, &KTextEditor::Document::textChanged, this, &KPartView::triggerUpdatePreview); + } else { + m_part->closeUrl(); + } +} + +void KPartView::setAutoUpdating(bool autoUpdating) +{ + if (m_autoUpdating == autoUpdating) { + return; + } + + m_autoUpdating = autoUpdating; + + if (m_autoUpdating) { + if (m_document && m_part && m_previewDirty) { + updatePreview(); + } + } else { + m_updateSquashingTimer.stop(); + } +} + +void KPartView::triggerUpdatePreview() +{ + m_previewDirty = true; + + if (m_part->widget()->isVisible() && m_autoUpdating && !m_updateSquashingTimer.isActive()) { + m_updateSquashingTimer.start(); + } +} + +void KPartView::updatePreview() +{ + if (!m_part->widget()->isVisible()) { + return; + } + + // TODO: some kparts seem to steal the focus after they have loaded a file, sometimes also async + // that possibly needs fixing in the respective kparts, as that could be considered non-cooperative + + // TODO: investigate if pushing of the data to the kpart could be done in a non-gui-thread, + // so their loading of the file (e.g. ReadOnlyPart::openFile() is sync design) does not block + + const auto mimeType = m_document->mimeType(); + KParts::OpenUrlArguments arguments; + arguments.setMimeType(mimeType); + m_part->setArguments(arguments); + + // try to stream the data to avoid filesystem I/O + // create url unique for this document + // TODO: encode existing url instead, and for yet-to-be-stored docs some other unique id + const QUrl streamUrl(QStringLiteral("ktexteditorpreview:/object/%1") + .arg(reinterpret_cast(m_document), 0, 16)); + if (m_part->openStream(mimeType, streamUrl)) { + qCDebug(KTEPREVIEW) << "Pushing data via streaming API, url:" << streamUrl.url(); + m_part->writeStream(m_document->text().toUtf8()); + m_part->closeStream(); + + m_previewDirty = false; + return; + } + + // have to go via filesystem for now, not nice + if (!m_bufferFile) { + m_bufferFile = new QTemporaryFile(this); + m_bufferFile->open(); + } else { + // reset position + m_bufferFile->seek(0); + } + const QUrl tempFileUrl(QUrl::fromLocalFile(m_bufferFile->fileName())); + qCDebug(KTEPREVIEW) << "Pushing data via temporary file, url:" << tempFileUrl.url(); + + // write current data + 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, tempFileUrl)); + + m_previewDirty = false; +} + +void KPartView::handleOpenUrlRequest(const QUrl& url) +{ + QDesktopServices::openUrl(url); +} + +bool KPartView::eventFilter(QObject* object, QEvent* event) +{ + if (object == m_part->widget() && event->type() == QEvent::Show) { + if (m_document && m_autoUpdating && m_previewDirty) { + updatePreview(); + } + return true; + } + + return QObject::eventFilter(object, event); +} diff --git a/addons/preview/kpartview.h b/addons/preview/kpartview.h new file mode 100644 index 000000000..e2a273b59 --- /dev/null +++ b/addons/preview/kpartview.h @@ -0,0 +1,125 @@ +/* + * 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 KTEXTEDITORPREVIEW_KPARTVIEW_H +#define KTEXTEDITORPREVIEW_KPARTVIEW_H + +// KF +#include + +// Qt +#include +#include + +namespace KTextEditor { +class Document; +} +namespace KParts { +class ReadOnlyPart; +} +class QLabel; +class QTemporaryFile; + +namespace KTextEditorPreview { + +/** + * 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; + + KParts::ReadOnlyPart* kPart() 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; + + /** + * Sets whether the preview should be updating automatically on document changes or not. + * + * @param autoUpdating whether the preview should be updating automatically on document changes or not + */ + void setAutoUpdating(bool autoUpdating); + + /** + * Returns @c true if the preview is updating automatically on document changes, @c false otherwise. + */ + bool isAutoUpdating() const; + + /** + * Update preview to current document content. + */ + void updatePreview(); + +protected: + bool eventFilter(QObject* object, QEvent* event) override; + +private: + void triggerUpdatePreview(); + void handleOpenUrlRequest(const QUrl& url); + +private: + QLabel* m_errorLabel = nullptr; + KParts::ReadOnlyPart* m_part = nullptr; + KTextEditor::Document* m_document = nullptr; + + bool m_autoUpdating = true; + bool m_previewDirty = true; + QTimer m_updateSquashingTimer; + QTemporaryFile* m_bufferFile = nullptr; +}; + +} + +#endif diff --git a/addons/preview/ktexteditorpreview.json b/addons/preview/ktexteditorpreview.json new file mode 100644 index 000000000..7b65e07d0 --- /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", + "Id": "ktexteditorpreview", + "Name": "Document Preview", + "ServiceTypes": [ + "KTextEditor/Plugin", + "KDevelop/Plugin" + ] + } +} diff --git a/addons/preview/ktexteditorpreviewplugin.cpp b/addons/preview/ktexteditorpreviewplugin.cpp new file mode 100644 index 000000000..8521eac79 --- /dev/null +++ b/addons/preview/ktexteditorpreviewplugin.cpp @@ -0,0 +1,47 @@ +/* + * 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" +#include + +// KF +#include +#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/ktexteditorpreviewplugin.h b/addons/preview/ktexteditorpreviewplugin.h new file mode 100644 index 000000000..73597c8f1 --- /dev/null +++ b/addons/preview/ktexteditorpreviewplugin.h @@ -0,0 +1,42 @@ +/* + * 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 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/ktexteditorpreviewview.cpp b/addons/preview/ktexteditorpreviewview.cpp new file mode 100644 index 000000000..affc3b122 --- /dev/null +++ b/addons/preview/ktexteditorpreviewview.cpp @@ -0,0 +1,62 @@ +/* + * 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 + +// Qt +#include +#include + +using namespace KTextEditorPreview; + +KTextEditorPreviewView::KTextEditorPreviewView(KTextEditorPreviewPlugin* plugin, KTextEditor::MainWindow* mainWindow) + : QObject(mainWindow) +{ + Q_UNUSED(plugin); + + 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, mainWindow, m_toolView.data()); + m_toolView->layout()->setMargin(0); + m_toolView->layout()->addWidget(m_previewView); + m_toolView->addActions(m_previewView->actions()); +} + +KTextEditorPreviewView::~KTextEditorPreviewView() = default; + +void KTextEditorPreviewView::readSessionConfig(const KConfigGroup& config) +{ + m_previewView->readSessionConfig(config); +} + +void KTextEditorPreviewView::writeSessionConfig(KConfigGroup& config) +{ + m_previewView->writeSessionConfig(config); +} diff --git a/addons/preview/ktexteditorpreviewview.h b/addons/preview/ktexteditorpreviewview.h new file mode 100644 index 000000000..8c2413a74 --- /dev/null +++ b/addons/preview/ktexteditorpreviewview.h @@ -0,0 +1,59 @@ +/* + * 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 + +// KF +#include +// Qt +#include +#include + +namespace KTextEditorPreview { +class PreviewWidget; +} + +namespace KTextEditor { +class MainWindow; +class View; +} + +class KTextEditorPreviewPlugin; + +class QWidget; + +class KTextEditorPreviewView: public QObject, public KTextEditor::SessionConfigInterface +{ + Q_OBJECT + Q_INTERFACES(KTextEditor::SessionConfigInterface) + +public: + KTextEditorPreviewView(KTextEditorPreviewPlugin* plugin, KTextEditor::MainWindow* mainWindow); + ~KTextEditorPreviewView() override; + + void readSessionConfig(const KConfigGroup& config) override; + void writeSessionConfig(KConfigGroup& config) override; + +private: + QPointer m_toolView; + KTextEditorPreview::PreviewWidget* m_previewView; +}; + +#endif diff --git a/addons/preview/previewwidget.cpp b/addons/preview/previewwidget.cpp new file mode 100644 index 000000000..69e4e8371 --- /dev/null +++ b/addons/preview/previewwidget.cpp @@ -0,0 +1,340 @@ +/* + * 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" +#include + +// KF +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Qt +#include +#include +#include +#include +#include +#include +#include + + +using namespace KTextEditorPreview; + +PreviewWidget::PreviewWidget(KTextEditorPreviewPlugin* core, KTextEditor::MainWindow* mainWindow, + QWidget* parent) + : QStackedWidget(parent) + , KXMLGUIBuilder(this) + , m_core(core) + , m_mainWindow(mainWindow) + , m_xmlGuiFactory(new KXMLGUIFactory(this, this)) +{ + 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); + + // TODO: better icon(s) + const QIcon autoUpdateIcon = QIcon::fromTheme(QStringLiteral("media-playback-start")); + m_autoUpdateAction = new KToggleAction(autoUpdateIcon, i18n("Automatically Update Preview"), this); + m_autoUpdateAction->setToolTip(i18n("Enable automatic updates of the preview to the current document content")); + m_autoUpdateAction->setCheckedState(KGuiItem(i18n("Manually Update Preview"), autoUpdateIcon, i18n("Disable automatic updates of the preview to the current document content"))); + m_autoUpdateAction->setChecked(false); + connect(m_autoUpdateAction, &QAction::triggered, this, &PreviewWidget::toggleAutoUpdating); + addAction(m_autoUpdateAction); + + m_updateAction = new QAction(QIcon::fromTheme(QStringLiteral("view-refresh")), i18n("Update Preview"), this); + m_updateAction->setToolTip(i18n("Update the preview to the current document content")); + connect(m_updateAction, &QAction::triggered, this, &PreviewWidget::updatePreview); + m_updateAction->setEnabled(false); + addAction(m_updateAction); + + // manually prepare a proper dropdown menu button, because Qt itself does not do what one would expect + // when adding a default menu->menuAction() to a QToolbar + const auto kPartMenuIcon = QIcon::fromTheme(QStringLiteral("application-menu")); + const auto kPartMenuText = i18n("View"); + m_kPartMenu = new QMenu(this); + QToolButton* toolButton = new QToolButton(); + toolButton->setMenu(m_kPartMenu); + toolButton->setIcon(kPartMenuIcon); + toolButton->setText(kPartMenuText); + toolButton->setPopupMode(QToolButton::InstantPopup); + + m_kPartMenuAction = new QWidgetAction(this); + m_kPartMenuAction->setIcon(kPartMenuIcon); + m_kPartMenuAction->setText(kPartMenuText); + m_kPartMenuAction->setMenu(m_kPartMenu); + m_kPartMenuAction->setDefaultWidget(toolButton); + m_kPartMenuAction->setEnabled(false); + addAction(m_kPartMenuAction); + + m_aboutKPartAction = new QAction(this); + connect(m_aboutKPartAction, &QAction::triggered, this, &PreviewWidget::showAboutKPartPlugin); + m_aboutKPartAction->setEnabled(false); + + auto label = new QLabel(i18n("No preview available."), this); + label->setAlignment(Qt::AlignHCenter); + addWidget(label); + + connect(m_mainWindow, SIGNAL(viewChanged(KTextEditor::View*)), + this, SLOT(setTextEditorView(KTextEditor::View*))); + + setTextEditorView(m_mainWindow->activeView()); +} + +PreviewWidget::~PreviewWidget() = default; + +void PreviewWidget::readSessionConfig(const KConfigGroup& configGroup) +{ + // TODO: also store document id/url and see to catch the same document on restoring config + m_lockAction->setChecked(configGroup.readEntry("documentLocked", false)); + m_autoUpdateAction->setChecked(configGroup.readEntry("automaticUpdate", false)); +} + +void PreviewWidget::writeSessionConfig(KConfigGroup& configGroup) const +{ + configGroup.writeEntry("documentLocked", m_lockAction->isChecked()); + configGroup.writeEntry("automaticUpdate", m_autoUpdateAction->isChecked()); +} + +void PreviewWidget::setTextEditorView(KTextEditor::View* view) +{ + if ((m_previewedTextEditorView == view) || + !isVisible() || + m_lockAction->isChecked()) { + return; + } + + m_previewedTextEditorView = view; + + KService::Ptr service; + if (m_previewedTextEditorView) { + // TODO: mimetype is not set for new document which have not been saved yet. + // needs another way to get this info, or perhaps some proper fix in Kate/Kdevelop + // to guess the mimetype based on current content, selected mode/highlighting etc. + // which then also would needs a signal mimetypeChanged and handling here + const auto mimeType = m_previewedTextEditorView->document()->mimeType(); + service = KMimeTypeTrader::self()->preferredService(mimeType, QStringLiteral("KParts/ReadOnlyPart")); + if (service) { + qCDebug(KTEPREVIEW) << "Found preferred kpart service named" << service->name() + << "with library" <library() + << "for mimetype" << mimeType; + + if (service->library().isEmpty()) { + qCWarning(KTEPREVIEW) << "Discarding preferred kpart service due to empty library name:" << service->name(); + service.reset(); + } + + // 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? + // or making a distinction between source editors/viewers and final editors/viewers? + // latter would also help other source editors/viewers like a hexeditor, which "supports" any mimetype + if (service && service->mimeTypes().contains(QStringLiteral("text/plain"))) { + qCDebug(KTEPREVIEW) << "Blindly discarding preferred service as it also supports text/plain, to avoid useless plain/text preview."; + service.reset(); + } + } else { + qCDebug(KTEPREVIEW) << "Found no preferred kpart service for mimetype" << mimeType; + } + } + + // 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) { + // clear kpart menu + m_xmlGuiFactory->removeClient(m_partView->kPart()); + m_kPartMenu->clear(); + + removeWidget(m_partView->widget()); + delete m_partView; + } + + m_currentServiceId = serviceId; + + if (service) { + qCDebug(KTEPREVIEW) << "Creating new kpart service instance."; + m_partView = new KPartView(service, this); + m_partView->setAutoUpdating(m_autoUpdateAction->isChecked()); + int index = addWidget(m_partView->widget()); + setCurrentIndex(index); + + // update kpart menu + const auto kPart = m_partView->kPart(); + if (kPart) { + const auto kPartDisplayName = kPart->componentData().displayName(); + m_aboutKPartAction->setText(i18n("About %1", kPartDisplayName)); + m_xmlGuiFactory->addClient(kPart); + m_kPartMenu->addSeparator(); + m_kPartMenu->addAction(m_aboutKPartAction); + } + } else { + m_partView = nullptr; + } + } else { + if (m_partView) { + qCDebug(KTEPREVIEW) << "Reusing active kpart service instance."; + } + } + + if (m_partView) { + m_partView->setDocument(m_previewedTextEditorView->document()); + } + + m_updateAction->setEnabled(m_partView && !m_autoUpdateAction->isChecked()); + const bool hasKPart = (m_partView && m_partView->kPart()); + m_kPartMenuAction->setEnabled(hasKPart); + m_aboutKPartAction->setEnabled(hasKPart); +} + +void PreviewWidget::showEvent(QShowEvent* event) +{ + Q_UNUSED(event); + + m_updateAction->setEnabled(m_partView && !m_autoUpdateAction->isChecked()); + + setTextEditorView(m_mainWindow->activeView()); +} + +void PreviewWidget::hideEvent(QHideEvent* event) +{ + Q_UNUSED(event); + + // keep active part for reuse, but close preview document + if (m_partView) { + // TODO: we also get hide event in kdevelop when the view is changed, + // need to find out how to filter this out or how to fix kdevelop + // so currently keep the preview document +// m_partView->setDocument(nullptr); + } + + m_updateAction->setEnabled(false); +} + +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_mainWindow->activeView()); + } +} + +void PreviewWidget::toggleAutoUpdating(bool autoRefreshing) +{ + if (!m_partView) { + // nothing to do + return; + } + + m_updateAction->setEnabled(!autoRefreshing && isVisible()); + m_partView->setAutoUpdating(autoRefreshing); +} + +void PreviewWidget::updatePreview() +{ + m_partView->updatePreview(); +} + +void PreviewWidget::handleLockedDocumentClosing() +{ + // remove any current partview + if (m_partView) { + removeWidget(m_partView->widget()); + delete m_partView; + m_partView = nullptr; + } + + m_currentServiceId.clear(); +} + +QWidget* PreviewWidget::createContainer(QWidget* parent, int index, const QDomElement& element, QAction*& containerAction) +{ + containerAction = nullptr; + + if (element.attribute(QStringLiteral("deleted")).toLower() == QLatin1String("true")) { + return nullptr; + } + + const QString tagName = element.tagName().toLower(); + // filter out things we do not support + // TODO: consider integrating the toolbars + if (tagName == QLatin1String("mainwindow") || + tagName == QLatin1String("toolbar") || + tagName == QLatin1String("statusbar")) { + return nullptr; + } + + if (tagName == QLatin1String("menubar")) { + return m_kPartMenu; + } + + return KXMLGUIBuilder::createContainer(parent, index, element, containerAction); +} + +void PreviewWidget::removeContainer(QWidget* container, QWidget* parent, + QDomElement& element, QAction* containerAction) +{ + if (container == m_kPartMenu) { + return; + } + + KXMLGUIBuilder::removeContainer(container, parent, element, containerAction); +} + +void PreviewWidget::showAboutKPartPlugin() +{ + if (m_partView && m_partView->kPart()) { + QPointer aboutDialog = new KAboutApplicationDialog(m_partView->kPart()->componentData(), this); + aboutDialog->exec(); + delete aboutDialog; + } +} diff --git a/addons/preview/previewwidget.h b/addons/preview/previewwidget.h new file mode 100644 index 000000000..674e8f934 --- /dev/null +++ b/addons/preview/previewwidget.h @@ -0,0 +1,124 @@ +/* + * 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 KTEXTEDITORPREVIEW_PREVIEWWIDGET_H +#define KTEXTEDITORPREVIEW_PREVIEWWIDGET_H + +// KF +#include +// Qt +#include + +class KTextEditorPreviewPlugin; + +namespace KTextEditor { +class MainWindow; +class View; +} +class KXMLGUIFactory; +class KToggleAction; +class KConfigGroup; + +class QWidgetAction; +class QMenu; + +namespace KTextEditorPreview { +class KPartView; + +/** + * 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, public KXMLGUIBuilder +{ + Q_OBJECT + +public: + /** + * Constructor + * + * @param core the plugin object + * @param mainWindow the main window with all the texteditor views + * @param parent widget object taking the ownership + */ + PreviewWidget(KTextEditorPreviewPlugin* core, KTextEditor::MainWindow* mainWindow, QWidget* parent); + ~PreviewWidget() override; + + void readSessionConfig(const KConfigGroup& configGroup); + void writeSessionConfig(KConfigGroup& configGroup) const; + +public: // KXMLGUIBuilder API + QWidget* createContainer(QWidget* parent, int index, + const QDomElement& element, QAction*& containerAction) override; + void removeContainer(QWidget* container, QWidget* parent, + QDomElement& element, QAction* containerAction) override; + +protected: + void showEvent(QShowEvent* event) override; + void hideEvent(QHideEvent* event) override; + +private Q_SLOTS: + /** + * 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(); + void toggleAutoUpdating(bool autoRefreshing); + void updatePreview(); + void showAboutKPartPlugin(); + +private: + KToggleAction* m_lockAction; + KToggleAction* m_autoUpdateAction; + QAction* m_updateAction; + QWidgetAction* m_kPartMenuAction; + QMenu* m_kPartMenu; + QAction* m_aboutKPartAction; + + KTextEditorPreviewPlugin* const m_core; + KTextEditor::MainWindow* const m_mainWindow; + + KTextEditor::View* m_previewedTextEditorView = nullptr; + QString m_currentServiceId; + KPartView* m_partView = nullptr; + KXMLGUIFactory* m_xmlGuiFactory; +}; + +} + +#endif