1 /*
2 * SPDX-FileCopyrightText: 2017 Friedrich W. H. Kossebau <kossebau@kde.org>
3 *
4 * SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6
7 #include "previewwidget.h"
8
9 #include "kpartview.h"
10 #include "ktexteditorpreviewplugin.h"
11 #include <ktexteditorpreview_debug.h>
12
13 // KF
14 #include <KAboutPluginDialog>
15 #include <KConfigGroup>
16 #include <KGuiItem>
17 #include <KLocalizedString>
18 #include <KParts/PartLoader>
19 #include <KParts/ReadOnlyPart>
20 #include <KPluginMetaData>
21 #include <KService>
22 #include <KSharedConfig>
23 #include <KTextEditor/Document>
24 #include <KTextEditor/MainWindow>
25 #include <KTextEditor/View>
26 #include <KToggleAction>
27 #include <KXMLGUIFactory>
28
29 // Qt
30 #include <QAction>
31 #include <QDomElement>
32 #include <QIcon>
33 #include <QLabel>
34 #include <QMenu>
35 #include <QToolButton>
36 #include <QWidgetAction>
37
38 using namespace KTextEditorPreview;
39
PreviewWidget(KTextEditorPreviewPlugin * core,KTextEditor::MainWindow * mainWindow,QWidget * parent)40 PreviewWidget::PreviewWidget(KTextEditorPreviewPlugin *core, KTextEditor::MainWindow *mainWindow, QWidget *parent)
41 : QStackedWidget(parent)
42 , KXMLGUIBuilder(this)
43 , m_core(core)
44 , m_mainWindow(mainWindow)
45 , m_xmlGuiFactory(new KXMLGUIFactory(this, this))
46 {
47 m_lockAction = new KToggleAction(QIcon::fromTheme(QStringLiteral("object-unlocked")), i18n("Lock Current Document"), this);
48 m_lockAction->setToolTip(i18n("Lock preview to current document"));
49 m_lockAction->setCheckedState(KGuiItem(i18n("Unlock Current View"), QIcon::fromTheme(QStringLiteral("object-locked")), i18n("Unlock current view")));
50 m_lockAction->setChecked(false);
51 connect(m_lockAction, &QAction::triggered, this, &PreviewWidget::toggleDocumentLocking);
52 addAction(m_lockAction);
53
54 // TODO: better icon(s)
55 const QIcon autoUpdateIcon = QIcon::fromTheme(QStringLiteral("media-playback-start"));
56 m_autoUpdateAction = new KToggleAction(autoUpdateIcon, i18n("Automatically Update Preview"), this);
57 m_autoUpdateAction->setToolTip(i18n("Enable automatic updates of the preview to the current document content"));
58 m_autoUpdateAction->setCheckedState(KGuiItem(i18n("Manually Update Preview"), //
59 autoUpdateIcon,
60 i18n("Disable automatic updates of the preview to the current document content")));
61 m_autoUpdateAction->setChecked(false);
62 connect(m_autoUpdateAction, &QAction::triggered, this, &PreviewWidget::toggleAutoUpdating);
63 addAction(m_autoUpdateAction);
64
65 m_updateAction = new QAction(QIcon::fromTheme(QStringLiteral("view-refresh")), i18n("Update Preview"), this);
66 m_updateAction->setToolTip(i18n("Update the preview to the current document content"));
67 connect(m_updateAction, &QAction::triggered, this, &PreviewWidget::updatePreview);
68 m_updateAction->setEnabled(false);
69 addAction(m_updateAction);
70
71 // manually prepare a proper dropdown menu button, because Qt itself does not do what one would expect
72 // when adding a default menu->menuAction() to a QToolbar
73 const auto kPartMenuIcon = QIcon::fromTheme(QStringLiteral("application-menu"));
74 const auto kPartMenuText = i18n("View");
75
76 // m_kPartMenu may not be a child of this, because otherwise its XMLGUI-menu is deleted when switching views
77 // and therefore closing the tool view, which is a QMainWindow in KDevelop (IdealController::addView).
78 // see KXMLGUIBuilder::createContainer => tagName == d->tagMenu
79 m_kPartMenu = new QMenu;
80
81 QToolButton *toolButton = new QToolButton();
82 toolButton->setMenu(m_kPartMenu);
83 toolButton->setIcon(kPartMenuIcon);
84 toolButton->setText(kPartMenuText);
85 toolButton->setPopupMode(QToolButton::InstantPopup);
86
87 m_kPartMenuAction = new QWidgetAction(this);
88 m_kPartMenuAction->setIcon(kPartMenuIcon);
89 m_kPartMenuAction->setText(kPartMenuText);
90 m_kPartMenuAction->setMenu(m_kPartMenu);
91 m_kPartMenuAction->setDefaultWidget(toolButton);
92 m_kPartMenuAction->setEnabled(false);
93 addAction(m_kPartMenuAction);
94
95 m_aboutKPartAction = new QAction(this);
96 connect(m_aboutKPartAction, &QAction::triggered, this, &PreviewWidget::showAboutKPartPlugin);
97 m_aboutKPartAction->setEnabled(false);
98
99 auto label = new QLabel(i18n("No preview available."), this);
100 label->setAlignment(Qt::AlignHCenter);
101 addWidget(label);
102
103 connect(m_mainWindow, &KTextEditor::MainWindow::viewChanged, this, &PreviewWidget::setTextEditorView);
104
105 setTextEditorView(m_mainWindow->activeView());
106 }
107
~PreviewWidget()108 PreviewWidget::~PreviewWidget()
109 {
110 delete m_kPartMenu;
111 }
112
readSessionConfig(const KConfigGroup & configGroup)113 void PreviewWidget::readSessionConfig(const KConfigGroup &configGroup)
114 {
115 // TODO: also store document id/url and see to catch the same document on restoring config
116 m_lockAction->setChecked(configGroup.readEntry("documentLocked", false));
117 m_autoUpdateAction->setChecked(configGroup.readEntry("automaticUpdate", false));
118 }
119
writeSessionConfig(KConfigGroup & configGroup) const120 void PreviewWidget::writeSessionConfig(KConfigGroup &configGroup) const
121 {
122 configGroup.writeEntry("documentLocked", m_lockAction->isChecked());
123 configGroup.writeEntry("automaticUpdate", m_autoUpdateAction->isChecked());
124 }
125
setTextEditorView(KTextEditor::View * view)126 void PreviewWidget::setTextEditorView(KTextEditor::View *view)
127 {
128 if ((view && view == m_previewedTextEditorView && view->document() == m_previewedTextEditorDocument
129 && (!m_previewedTextEditorDocument || m_previewedTextEditorDocument->mode() == m_currentMode))
130 || !view || !isVisible() || m_lockAction->isChecked()) {
131 return;
132 }
133
134 m_previewedTextEditorView = view;
135 m_previewedTextEditorDocument = view ? view->document() : nullptr;
136
137 resetTextEditorView(m_previewedTextEditorDocument);
138 }
139
findPreviewPart(const QStringList mimeTypes)140 std::optional<KPluginMetaData> KTextEditorPreview::PreviewWidget::findPreviewPart(const QStringList mimeTypes)
141 {
142 for (const auto &mimeType : qAsConst(mimeTypes)) {
143 const auto offers = KParts::PartLoader::partsForMimeType(mimeType);
144
145 if (offers.isEmpty()) {
146 continue;
147 }
148
149 const KPluginMetaData service = offers.first();
150 qCDebug(KTEPREVIEW) << "Found preferred kpart named" << service.name() << "with library" << service.fileName() << "for mimetype" << mimeType;
151
152 // no interest in kparts which also just display the text (like katepart itself)
153 // TODO: what about parts which also support importing plain text and turning into richer format
154 // and thus have it in their mimetypes list?
155 // could that perhaps be solved by introducing the concept of "native" and "imported" mimetypes?
156 // or making a distinction between source editors/viewers and final editors/viewers?
157 // latter would also help other source editors/viewers like a hexeditor, which "supports" any mimetype
158 if (service.mimeTypes().contains(QLatin1String("text/plain"))) {
159 qCDebug(KTEPREVIEW) << "Blindly discarding preferred kpart as it also supports text/plain, to avoid useless plain/text preview.";
160 continue;
161 }
162
163 return service;
164 }
165 return {};
166 }
167
resetTextEditorView(KTextEditor::Document * document)168 void PreviewWidget::resetTextEditorView(KTextEditor::Document *document)
169 {
170 if (!isVisible() || m_previewedTextEditorDocument != document) {
171 return;
172 }
173
174 std::optional<KPluginMetaData> service;
175
176 if (m_previewedTextEditorDocument) {
177 // TODO: mimetype is not set for new documents which have not been saved yet.
178 // Maybe retry to guess as soon as content is inserted.
179 m_currentMode = m_previewedTextEditorDocument->mode();
180
181 // Get mimetypes assigned to the currently set mode.
182 auto mimeTypes = KConfigGroup(KSharedConfig::openConfig(QStringLiteral("katemoderc")), m_currentMode).readXdgListEntry("Mimetypes");
183 // Also try to guess from the content, if the above fails.
184 mimeTypes << m_previewedTextEditorDocument->mimeType();
185
186 service = findPreviewPart(mimeTypes);
187
188 if (!service) {
189 qCDebug(KTEPREVIEW) << "Found no preferred kpart service for mimetypes" << mimeTypes;
190 }
191
192 // Update if the mode is changed. The signal may also be emitted, when a new
193 // url is loaded, therefore wait (QueuedConnection) for the document to load.
194 connect(m_previewedTextEditorDocument,
195 &KTextEditor::Document::modeChanged,
196 this,
197 &PreviewWidget::resetTextEditorView,
198 static_cast<Qt::ConnectionType>(Qt::QueuedConnection | Qt::UniqueConnection));
199 // Explicitly clear the old document, which otherwise might be accessed in
200 // m_partView->setDocument.
201 connect(m_previewedTextEditorDocument, &KTextEditor::Document::aboutToClose, this, &PreviewWidget::unsetDocument, Qt::UniqueConnection);
202 } else {
203 m_currentMode.clear();
204 }
205
206 // change of preview type?
207 // TODO: find a better id than library?
208 const QString serviceId = service ? service->pluginId() : QString();
209
210 if (serviceId != m_currentServiceId) {
211 if (m_partView) {
212 clearMenu();
213 }
214
215 m_currentServiceId = serviceId;
216
217 if (service) {
218 qCDebug(KTEPREVIEW) << "Creating new kpart service instance.";
219 m_partView = new KPartView(*service, this);
220 const bool autoupdate = m_autoUpdateAction->isChecked();
221 m_partView->setAutoUpdating(autoupdate);
222 int index = addWidget(m_partView->widget());
223 setCurrentIndex(index);
224
225 // update kpart menu
226 const auto kPart = m_partView->kPart();
227 if (kPart) {
228 m_xmlGuiFactory->addClient(kPart);
229
230 m_aboutKPartAction->setText(i18n("About %1", kPart->metaData().name()));
231 m_aboutKPartAction->setEnabled(true);
232 m_kPartMenu->addSeparator();
233 m_kPartMenu->addAction(m_aboutKPartAction);
234 m_kPartMenuAction->setEnabled(true);
235 }
236
237 m_updateAction->setEnabled(!autoupdate);
238 } else {
239 m_partView = nullptr;
240 }
241 } else if (m_partView) {
242 qCDebug(KTEPREVIEW) << "Reusing active kpart service instance.";
243 }
244
245 if (m_partView) {
246 m_partView->setDocument(m_previewedTextEditorDocument);
247 }
248 }
249
unsetDocument(KTextEditor::Document * document)250 void PreviewWidget::unsetDocument(KTextEditor::Document *document)
251 {
252 if (!m_partView || m_previewedTextEditorDocument != document) {
253 return;
254 }
255
256 m_partView->setDocument(nullptr);
257 m_previewedTextEditorDocument = nullptr;
258
259 // remove any current partview
260 clearMenu();
261 m_partView = nullptr;
262
263 m_currentServiceId.clear();
264 }
265
showEvent(QShowEvent * event)266 void PreviewWidget::showEvent(QShowEvent *event)
267 {
268 Q_UNUSED(event);
269
270 m_updateAction->setEnabled(m_partView && !m_autoUpdateAction->isChecked());
271
272 if (m_lockAction->isChecked()) {
273 resetTextEditorView(m_previewedTextEditorDocument);
274 } else {
275 setTextEditorView(m_mainWindow->activeView());
276 }
277 }
278
hideEvent(QHideEvent * event)279 void PreviewWidget::hideEvent(QHideEvent *event)
280 {
281 Q_UNUSED(event);
282
283 // keep active part for reuse, but close preview document
284 // TODO: we also get hide event in kdevelop when the view is changed,
285 // need to find out how to filter this out or how to fix kdevelop
286 // so currently keep the preview document
287 // unsetDocument(m_previewedTextEditorDocument);
288
289 m_updateAction->setEnabled(false);
290 }
291
toggleDocumentLocking(bool locked)292 void PreviewWidget::toggleDocumentLocking(bool locked)
293 {
294 if (!locked) {
295 setTextEditorView(m_mainWindow->activeView());
296 }
297 }
298
toggleAutoUpdating(bool autoRefreshing)299 void PreviewWidget::toggleAutoUpdating(bool autoRefreshing)
300 {
301 if (!m_partView) {
302 // nothing to do
303 return;
304 }
305
306 m_updateAction->setEnabled(!autoRefreshing && isVisible());
307 m_partView->setAutoUpdating(autoRefreshing);
308 }
309
updatePreview()310 void PreviewWidget::updatePreview()
311 {
312 if (m_partView && m_partView->document()) {
313 m_partView->updatePreview();
314 }
315 }
316
createContainer(QWidget * parent,int index,const QDomElement & element,QAction * & containerAction)317 QWidget *PreviewWidget::createContainer(QWidget *parent, int index, const QDomElement &element, QAction *&containerAction)
318 {
319 containerAction = nullptr;
320
321 if (element.attribute(QStringLiteral("deleted")).toLower() == QLatin1String("true")) {
322 return nullptr;
323 }
324
325 const QString tagName = element.tagName().toLower();
326 // filter out things we do not support
327 // TODO: consider integrating the toolbars
328 if (tagName == QLatin1String("mainwindow") || tagName == QLatin1String("toolbar") || tagName == QLatin1String("statusbar")) {
329 return nullptr;
330 }
331
332 if (tagName == QLatin1String("menubar")) {
333 return m_kPartMenu;
334 }
335
336 return KXMLGUIBuilder::createContainer(parent, index, element, containerAction);
337 }
338
removeContainer(QWidget * container,QWidget * parent,QDomElement & element,QAction * containerAction)339 void PreviewWidget::removeContainer(QWidget *container, QWidget *parent, QDomElement &element, QAction *containerAction)
340 {
341 if (container == m_kPartMenu) {
342 return;
343 }
344
345 KXMLGUIBuilder::removeContainer(container, parent, element, containerAction);
346 }
347
showAboutKPartPlugin()348 void PreviewWidget::showAboutKPartPlugin()
349 {
350 if (m_partView && m_partView->kPart()) {
351 QPointer<KAboutPluginDialog> aboutDialog = new KAboutPluginDialog(m_partView->kPart()->metaData(), this);
352 aboutDialog->exec();
353 delete aboutDialog;
354 }
355 }
356
clearMenu()357 void PreviewWidget::clearMenu()
358 {
359 // clear kpart menu
360 m_xmlGuiFactory->removeClient(m_partView->kPart());
361 m_kPartMenu->clear();
362
363 removeWidget(m_partView->widget());
364 delete m_partView;
365
366 m_updateAction->setEnabled(false);
367 m_kPartMenuAction->setEnabled(false);
368 m_aboutKPartAction->setEnabled(false);
369 }
370