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 "previewwidget.h"
0008 
0009 #include "kpartview.h"
0010 #include "ktexteditorpreviewplugin.h"
0011 #include <ktexteditorpreview_debug.h>
0012 
0013 // KF
0014 #include <KAboutPluginDialog>
0015 #include <KConfigGroup>
0016 #include <KGuiItem>
0017 #include <KLocalizedString>
0018 #include <KParts/PartLoader>
0019 #include <KPluginMetaData>
0020 #include <KSharedConfig>
0021 #include <KTextEditor/Document>
0022 #include <KTextEditor/MainWindow>
0023 #include <KTextEditor/View>
0024 #include <KToggleAction>
0025 #include <KXMLGUIFactory>
0026 
0027 // Qt
0028 #include <QAction>
0029 #include <QDomElement>
0030 #include <QIcon>
0031 #include <QLabel>
0032 #include <QMenu>
0033 #include <QToolButton>
0034 #include <QWidgetAction>
0035 
0036 using namespace KTextEditorPreview;
0037 
0038 PreviewWidget::PreviewWidget(KTextEditor::MainWindow *mainWindow, QWidget *parent)
0039     : QStackedWidget(parent)
0040     , KXMLGUIBuilder(this)
0041     , m_mainWindow(mainWindow)
0042     , m_xmlGuiFactory(new KXMLGUIFactory(this, this))
0043 {
0044     m_lockAction = new KToggleAction(QIcon::fromTheme(QStringLiteral("object-unlocked")), i18n("Lock Current Document"), this);
0045     m_lockAction->setToolTip(i18n("Lock preview to current document"));
0046     m_lockAction->setCheckedState(KGuiItem(i18n("Unlock Current View"), QIcon::fromTheme(QStringLiteral("object-locked")), i18n("Unlock current view")));
0047     m_lockAction->setChecked(false);
0048     connect(m_lockAction, &QAction::triggered, this, &PreviewWidget::toggleDocumentLocking);
0049     addAction(m_lockAction);
0050 
0051     // TODO: better icon(s)
0052     const QIcon autoUpdateIcon = QIcon::fromTheme(QStringLiteral("media-playback-start"));
0053     m_autoUpdateAction = new KToggleAction(autoUpdateIcon, i18n("Automatically Update Preview"), this);
0054     m_autoUpdateAction->setToolTip(i18n("Enable automatic updates of the preview to the current document content"));
0055     m_autoUpdateAction->setCheckedState(KGuiItem(i18n("Manually Update Preview"), //
0056                                                  autoUpdateIcon,
0057                                                  i18n("Disable automatic updates of the preview to the current document content")));
0058     m_autoUpdateAction->setChecked(false);
0059     connect(m_autoUpdateAction, &QAction::triggered, this, &PreviewWidget::toggleAutoUpdating);
0060     addAction(m_autoUpdateAction);
0061 
0062     m_updateAction = new QAction(QIcon::fromTheme(QStringLiteral("view-refresh")), i18n("Update Preview"), this);
0063     m_updateAction->setToolTip(i18n("Update the preview to the current document content"));
0064     connect(m_updateAction, &QAction::triggered, this, &PreviewWidget::updatePreview);
0065     m_updateAction->setEnabled(false);
0066     addAction(m_updateAction);
0067 
0068     // manually prepare a proper dropdown menu button, because Qt itself does not do what one would expect
0069     // when adding a default menu->menuAction() to a QToolbar
0070     const auto kPartMenuIcon = QIcon::fromTheme(QStringLiteral("application-menu"));
0071     const auto kPartMenuText = i18n("View");
0072 
0073     // m_kPartMenu may not be a child of this, because otherwise its XMLGUI-menu is deleted when switching views
0074     // and therefore closing the tool view, which is a QMainWindow in KDevelop (IdealController::addView).
0075     // see KXMLGUIBuilder::createContainer => tagName == d->tagMenu
0076     m_kPartMenu = new QMenu;
0077 
0078     QToolButton *toolButton = new QToolButton();
0079     toolButton->setMenu(m_kPartMenu);
0080     toolButton->setIcon(kPartMenuIcon);
0081     toolButton->setText(kPartMenuText);
0082     toolButton->setPopupMode(QToolButton::InstantPopup);
0083 
0084     m_kPartMenuAction = new QWidgetAction(this);
0085     m_kPartMenuAction->setIcon(kPartMenuIcon);
0086     m_kPartMenuAction->setText(kPartMenuText);
0087     m_kPartMenuAction->setMenu(m_kPartMenu);
0088     m_kPartMenuAction->setDefaultWidget(toolButton);
0089     m_kPartMenuAction->setEnabled(false);
0090     addAction(m_kPartMenuAction);
0091 
0092     m_aboutKPartAction = new QAction(this);
0093     connect(m_aboutKPartAction, &QAction::triggered, this, &PreviewWidget::showAboutKPartPlugin);
0094     m_aboutKPartAction->setEnabled(false);
0095 
0096     auto label = new QLabel(i18n("No preview available."), this);
0097     label->setAlignment(Qt::AlignHCenter);
0098     addWidget(label);
0099 
0100     connect(m_mainWindow, &KTextEditor::MainWindow::viewChanged, this, &PreviewWidget::setTextEditorView);
0101 
0102     setTextEditorView(m_mainWindow->activeView());
0103 }
0104 
0105 PreviewWidget::~PreviewWidget()
0106 {
0107     delete m_kPartMenu;
0108 }
0109 
0110 void PreviewWidget::readSessionConfig(const KConfigGroup &configGroup)
0111 {
0112     // TODO: also store document id/url and see to catch the same document on restoring config
0113     m_lockAction->setChecked(configGroup.readEntry("documentLocked", false));
0114     m_autoUpdateAction->setChecked(configGroup.readEntry("automaticUpdate", false));
0115 }
0116 
0117 void PreviewWidget::writeSessionConfig(KConfigGroup &configGroup) const
0118 {
0119     configGroup.writeEntry("documentLocked", m_lockAction->isChecked());
0120     configGroup.writeEntry("automaticUpdate", m_autoUpdateAction->isChecked());
0121 }
0122 
0123 void PreviewWidget::setTextEditorView(KTextEditor::View *view)
0124 {
0125     if ((view && view == m_previewedTextEditorView && view->document() == m_previewedTextEditorDocument
0126          && (!m_previewedTextEditorDocument || m_previewedTextEditorDocument->mode() == m_currentMode))
0127         || !view || !isVisible() || m_lockAction->isChecked()) {
0128         return;
0129     }
0130 
0131     m_previewedTextEditorView = view;
0132     m_previewedTextEditorDocument = view ? view->document() : nullptr;
0133 
0134     resetTextEditorView(m_previewedTextEditorDocument);
0135 }
0136 
0137 std::optional<KPluginMetaData> KTextEditorPreview::PreviewWidget::findPreviewPart(const QStringList mimeTypes)
0138 {
0139     for (const auto &mimeType : qAsConst(mimeTypes)) {
0140         const auto offers = KParts::PartLoader::partsForMimeType(mimeType);
0141 
0142         if (offers.isEmpty()) {
0143             continue;
0144         }
0145 
0146         const KPluginMetaData service = offers.first();
0147         qCDebug(KTEPREVIEW) << "Found preferred kpart named" << service.name() << "with library" << service.fileName() << "for mimetype" << mimeType;
0148 
0149         // no interest in kparts which also just display the text (like katepart itself)
0150         // TODO: what about parts which also support importing plain text and turning into richer format
0151         // and thus have it in their mimetypes list?
0152         // could that perhaps be solved by introducing the concept of "native" and "imported" mimetypes?
0153         // or making a distinction between source editors/viewers and final editors/viewers?
0154         // latter would also help other source editors/viewers like a hexeditor, which "supports" any mimetype
0155         if (service.mimeTypes().contains(QLatin1String("text/plain"))) {
0156             qCDebug(KTEPREVIEW) << "Blindly discarding preferred kpart as it also supports text/plain, to avoid useless plain/text preview.";
0157             continue;
0158         }
0159 
0160         return service;
0161     }
0162     return {};
0163 }
0164 
0165 void PreviewWidget::resetTextEditorView(KTextEditor::Document *document)
0166 {
0167     if (!isVisible() || m_previewedTextEditorDocument != document) {
0168         return;
0169     }
0170 
0171     std::optional<KPluginMetaData> service;
0172 
0173     if (m_previewedTextEditorDocument) {
0174         // TODO: mimetype is not set for new documents which have not been saved yet.
0175         // Maybe retry to guess as soon as content is inserted.
0176         m_currentMode = m_previewedTextEditorDocument->mode();
0177 
0178         // Get mimetypes assigned to the currently set mode.
0179         auto mimeTypes = KConfigGroup(KSharedConfig::openConfig(QStringLiteral("katemoderc")), m_currentMode).readXdgListEntry("Mimetypes");
0180         // For markdown manually add text/markdown if above fails e.g., if the file is untitled
0181         if (mimeTypes.isEmpty() && m_currentMode == QStringLiteral("Markdown")) {
0182             mimeTypes << QStringLiteral("text/markdown");
0183         }
0184 
0185         // Also try to guess from the content, if the above fails.
0186         mimeTypes << m_previewedTextEditorDocument->mimeType();
0187 
0188         service = findPreviewPart(mimeTypes);
0189 
0190         if (!service) {
0191             qCDebug(KTEPREVIEW) << "Found no preferred kpart service for mimetypes" << mimeTypes;
0192         }
0193 
0194         // Update if the mode is changed. The signal may also be emitted, when a new
0195         // url is loaded, therefore wait (QueuedConnection) for the document to load.
0196         connect(m_previewedTextEditorDocument,
0197                 &KTextEditor::Document::modeChanged,
0198                 this,
0199                 &PreviewWidget::resetTextEditorView,
0200                 static_cast<Qt::ConnectionType>(Qt::QueuedConnection | Qt::UniqueConnection));
0201         // Explicitly clear the old document, which otherwise might be accessed in
0202         // m_partView->setDocument.
0203         connect(m_previewedTextEditorDocument, &KTextEditor::Document::aboutToClose, this, &PreviewWidget::unsetDocument, Qt::UniqueConnection);
0204     } else {
0205         m_currentMode.clear();
0206     }
0207 
0208     // change of preview type?
0209     // TODO: find a better id than library?
0210     const QString serviceId = service ? service->pluginId() : QString();
0211 
0212     if (serviceId != m_currentServiceId) {
0213         if (m_partView) {
0214             clearMenu();
0215         }
0216 
0217         m_currentServiceId = serviceId;
0218 
0219         if (service) {
0220             qCDebug(KTEPREVIEW) << "Creating new kpart service instance.";
0221             m_partView = new KPartView(*service, this);
0222             const bool autoupdate = m_autoUpdateAction->isChecked();
0223             m_partView->setAutoUpdating(autoupdate);
0224             int index = addWidget(m_partView->widget());
0225             setCurrentIndex(index);
0226 
0227             // update kpart menu
0228             const auto kPart = m_partView->kPart();
0229             if (kPart) {
0230                 m_xmlGuiFactory->addClient(kPart);
0231 
0232                 m_aboutKPartAction->setText(i18n("About %1", kPart->metaData().name()));
0233                 m_aboutKPartAction->setEnabled(true);
0234                 m_kPartMenu->addSeparator();
0235                 m_kPartMenu->addAction(m_aboutKPartAction);
0236                 m_kPartMenuAction->setEnabled(true);
0237             }
0238 
0239             m_updateAction->setEnabled(!autoupdate);
0240         } else {
0241             m_partView = nullptr;
0242         }
0243     } else if (m_partView) {
0244         qCDebug(KTEPREVIEW) << "Reusing active kpart service instance.";
0245     }
0246 
0247     if (m_partView) {
0248         m_partView->setDocument(m_previewedTextEditorDocument);
0249     }
0250 }
0251 
0252 void PreviewWidget::unsetDocument(KTextEditor::Document *document)
0253 {
0254     if (!m_partView || m_previewedTextEditorDocument != document) {
0255         return;
0256     }
0257 
0258     m_partView->setDocument(nullptr);
0259     m_previewedTextEditorDocument = nullptr;
0260 
0261     // remove any current partview
0262     clearMenu();
0263     m_partView = nullptr;
0264 
0265     m_currentServiceId.clear();
0266 }
0267 
0268 void PreviewWidget::showEvent(QShowEvent *event)
0269 {
0270     Q_UNUSED(event);
0271 
0272     m_updateAction->setEnabled(m_partView && !m_autoUpdateAction->isChecked());
0273 
0274     if (m_lockAction->isChecked()) {
0275         resetTextEditorView(m_previewedTextEditorDocument);
0276     } else {
0277         setTextEditorView(m_mainWindow->activeView());
0278     }
0279 }
0280 
0281 void PreviewWidget::hideEvent(QHideEvent *event)
0282 {
0283     Q_UNUSED(event);
0284 
0285     // keep active part for reuse, but close preview document
0286     // TODO: we also get hide event in kdevelop when the view is changed,
0287     // need to find out how to filter this out or how to fix kdevelop
0288     // so currently keep the preview document
0289     //     unsetDocument(m_previewedTextEditorDocument);
0290 
0291     m_updateAction->setEnabled(false);
0292 }
0293 
0294 void PreviewWidget::toggleDocumentLocking(bool locked)
0295 {
0296     if (!locked) {
0297         setTextEditorView(m_mainWindow->activeView());
0298     }
0299 }
0300 
0301 void PreviewWidget::toggleAutoUpdating(bool autoRefreshing)
0302 {
0303     if (!m_partView) {
0304         // nothing to do
0305         return;
0306     }
0307 
0308     m_updateAction->setEnabled(!autoRefreshing && isVisible());
0309     m_partView->setAutoUpdating(autoRefreshing);
0310 }
0311 
0312 void PreviewWidget::updatePreview()
0313 {
0314     if (m_partView && m_partView->document()) {
0315         m_partView->updatePreview();
0316     }
0317 }
0318 
0319 QWidget *PreviewWidget::createContainer(QWidget *parent, int index, const QDomElement &element, QAction *&containerAction)
0320 {
0321     containerAction = nullptr;
0322 
0323     if (element.attribute(QStringLiteral("deleted")).toLower() == QLatin1String("true")) {
0324         return nullptr;
0325     }
0326 
0327     const QString tagName = element.tagName().toLower();
0328     // filter out things we do not support
0329     // TODO: consider integrating the toolbars
0330     if (tagName == QLatin1String("mainwindow") || tagName == QLatin1String("toolbar") || tagName == QLatin1String("statusbar")) {
0331         return nullptr;
0332     }
0333 
0334     if (tagName == QLatin1String("menubar")) {
0335         return m_kPartMenu;
0336     }
0337 
0338     return KXMLGUIBuilder::createContainer(parent, index, element, containerAction);
0339 }
0340 
0341 void PreviewWidget::removeContainer(QWidget *container, QWidget *parent, QDomElement &element, QAction *containerAction)
0342 {
0343     if (container == m_kPartMenu) {
0344         return;
0345     }
0346 
0347     KXMLGUIBuilder::removeContainer(container, parent, element, containerAction);
0348 }
0349 
0350 void PreviewWidget::showAboutKPartPlugin()
0351 {
0352     if (m_partView && m_partView->kPart()) {
0353         QPointer<KAboutPluginDialog> aboutDialog = new KAboutPluginDialog(m_partView->kPart()->metaData(), this);
0354         aboutDialog->exec();
0355         delete aboutDialog;
0356     }
0357 }
0358 
0359 void PreviewWidget::clearMenu()
0360 {
0361     // clear kpart menu
0362     m_xmlGuiFactory->removeClient(m_partView->kPart());
0363     m_kPartMenu->clear();
0364 
0365     removeWidget(m_partView->widget());
0366     delete m_partView;
0367 
0368     m_updateAction->setEnabled(false);
0369     m_kPartMenuAction->setEnabled(false);
0370     m_aboutKPartAction->setEnabled(false);
0371 }
0372 
0373 #include "moc_previewwidget.cpp"