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"