File indexing completed on 2024-04-14 05:45:37

0001 /*
0002     SPDX-FileCopyrightText: 2017, 2020 Friedrich W. H. Kossebau <kossebau@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.1-or-later
0005 */
0006 
0007 #include "markdownpart.hpp"
0008 
0009 // part
0010 #include "markdownview.hpp"
0011 #include "markdownbrowserextension.hpp"
0012 #include "searchtoolbar.hpp"
0013 // KF
0014 #include <KPluginMetaData>
0015 #include <KActionCollection>
0016 #include <KStandardAction>
0017 #include <KLocalizedString>
0018 #include <KFileItem>
0019 // Qt
0020 #include <QTextDocument>
0021 #include <QFile>
0022 #include <QTextStream>
0023 #include <QMimeDatabase>
0024 #include <QBuffer>
0025 #include <QShortcut>
0026 #include <QDesktopServices>
0027 #include <QMimeData>
0028 #include <QClipboard>
0029 #include <QApplication>
0030 #include <QMenu>
0031 #include <QVBoxLayout>
0032 
0033 
0034 MarkdownPart::MarkdownPart(QWidget* parentWidget, QObject* parent, const KPluginMetaData& metaData, Modus modus)
0035     : KParts::ReadOnlyPart(parent, metaData)
0036     , m_sourceDocument(new QTextDocument(this))
0037     , m_widget(new MarkdownView(m_sourceDocument, parentWidget))
0038     , m_searchToolBar(new SearchToolBar(m_widget, parentWidget))
0039     , m_browserExtension(new MarkdownBrowserExtension(this))
0040 {
0041     // set internal UI
0042     auto* mainLayout = new QVBoxLayout;
0043     mainLayout->setContentsMargins(0, 0, 0, 0);
0044     mainLayout->setSpacing(0);
0045 
0046     mainLayout->addWidget(m_widget);
0047 
0048     m_searchToolBar->hide();
0049     mainLayout->addWidget(m_searchToolBar);
0050 
0051     auto* mainWidget = new QWidget(parentWidget);
0052     mainWidget->setLayout(mainLayout);
0053     setWidget(mainWidget);
0054 
0055     // set KXMLUI resource file
0056     setXMLFile(QStringLiteral("markdownpartui.rc"));
0057 
0058     if (modus == BrowserViewModus) {
0059         connect(m_widget, &MarkdownView::anchorClicked,
0060                 m_browserExtension, &MarkdownBrowserExtension::requestOpenUrl);
0061         connect(m_widget, &MarkdownView::copyAvailable,
0062                 m_browserExtension, &MarkdownBrowserExtension::updateCopyAction);
0063         connect(m_widget, &MarkdownView::contextMenuRequested,
0064                 m_browserExtension, &MarkdownBrowserExtension::requestContextMenu);
0065     } else {
0066         connect(m_widget, &MarkdownView::anchorClicked,
0067                 this, &MarkdownPart::handleOpenUrlRequest);
0068         connect(m_widget, &MarkdownView::contextMenuRequested,
0069                 this, &MarkdownPart::handleContextMenuRequest);
0070     }
0071     connect(m_widget, QOverload<const QUrl &>::of(&MarkdownView::highlighted),
0072             this, &MarkdownPart::showHoveredLink);
0073 
0074     setupActions(modus);
0075 }
0076 
0077 MarkdownPart::~MarkdownPart() = default;
0078 
0079 
0080 void MarkdownPart::setupActions(Modus modus)
0081 {
0082     // only register to xmlgui if not in browser mode
0083     QObject* copySelectionActionParent = (modus == BrowserViewModus) ? static_cast<QObject*>(this) : static_cast<QObject*>(actionCollection());
0084     m_copySelectionAction = KStandardAction::copy(copySelectionActionParent);
0085     m_copySelectionAction->setText(i18nc("@action", "&Copy Text"));
0086     m_copySelectionAction->setEnabled(m_widget->hasSelection());
0087     connect(m_widget, &MarkdownView::copyAvailable,
0088             m_copySelectionAction, &QAction::setEnabled);
0089     connect(m_copySelectionAction, &QAction::triggered, this, &MarkdownPart::copySelection);
0090 
0091     m_selectAllAction = KStandardAction::selectAll(this, &MarkdownPart::selectAll, actionCollection());
0092     m_selectAllAction->setShortcutContext(Qt::WidgetShortcut);
0093     m_widget->addAction(m_selectAllAction);
0094 
0095     m_searchAction = KStandardAction::find(m_searchToolBar, &SearchToolBar::startSearch, actionCollection());
0096     m_searchAction->setEnabled(false);
0097     m_widget->addAction(m_searchAction);
0098 
0099     m_searchNextAction = KStandardAction::findNext(m_searchToolBar, &SearchToolBar::searchNext, actionCollection());
0100     m_searchNextAction->setEnabled(false);
0101     m_widget->addAction(m_searchNextAction);
0102 
0103     m_searchPreviousAction = KStandardAction::findPrev(m_searchToolBar, &SearchToolBar::searchPrevious, actionCollection());
0104     m_searchPreviousAction->setEnabled(false);
0105     m_widget->addAction(m_searchPreviousAction);
0106 
0107     auto* closeFindBarShortcut = new QShortcut(QKeySequence(Qt::Key_Escape), widget());
0108     closeFindBarShortcut->setContext(Qt::WidgetWithChildrenShortcut);
0109     connect(closeFindBarShortcut, &QShortcut::activated, m_searchToolBar, &SearchToolBar::hide);
0110 }
0111 
0112 bool MarkdownPart::openFile()
0113 {
0114     QFile file(localFilePath());
0115     if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
0116         return false;
0117     }
0118 
0119     prepareViewStateRestoringOnReload();
0120 
0121     QTextStream stream(&file);
0122     QString text = stream.readAll();
0123 
0124     file.close();
0125 
0126     m_sourceDocument->setMarkdown(text);
0127     const QUrl b = QUrl::fromLocalFile(localFilePath()).adjusted(QUrl::RemoveFilename);
0128     m_sourceDocument->setBaseUrl(b);
0129 
0130     restoreScrollPosition();
0131 
0132     m_searchAction->setEnabled(true);
0133     m_searchNextAction->setEnabled(true);
0134     m_searchPreviousAction->setEnabled(true);
0135 
0136     return true;
0137 }
0138 
0139 bool MarkdownPart::doOpenStream(const QString& mimeType)
0140 {
0141     const QMimeType mime = QMimeDatabase().mimeTypeForName(mimeType);
0142     if (!mime.inherits(QStringLiteral("text/markdown"))) {
0143         return false;
0144     }
0145 
0146     m_streamedData.clear();
0147     m_sourceDocument->setMarkdown(QString());
0148     return true;
0149 }
0150 
0151 bool MarkdownPart::doWriteStream(const QByteArray& data)
0152 {
0153     m_streamedData.append(data);
0154     return true;
0155 }
0156 
0157 bool MarkdownPart::doCloseStream()
0158 {
0159     QBuffer buffer(&m_streamedData);
0160 
0161     if (!buffer.open(QIODevice::ReadOnly | QIODevice::Text)) {
0162         m_streamedData.clear();
0163         return false;
0164     }
0165 
0166     prepareViewStateRestoringOnReload();
0167 
0168     QTextStream stream(&buffer);
0169     QString text = stream.readAll();
0170 
0171     m_sourceDocument->setMarkdown(text);
0172     m_sourceDocument->setBaseUrl(QUrl());
0173 
0174     restoreScrollPosition();
0175 
0176     m_searchAction->setEnabled(true);
0177     m_searchNextAction->setEnabled(true);
0178     m_searchPreviousAction->setEnabled(true);
0179 
0180     m_streamedData.clear();
0181     return true;
0182 }
0183 
0184 bool MarkdownPart::closeUrl()
0185 {
0186     // protect against repeated call if already closed
0187     const QUrl currentUrl = url();
0188     if (currentUrl.isValid()) {
0189         m_previousScrollPosition = m_widget->scrollPosition();
0190         m_previousUrl = currentUrl;
0191     }
0192 
0193     m_sourceDocument->setMarkdown(QString());
0194     m_sourceDocument->setBaseUrl(QUrl());
0195     m_searchAction->setEnabled(false);
0196     m_searchNextAction->setEnabled(false);
0197     m_searchPreviousAction->setEnabled(false);
0198     m_streamedData.clear();
0199 
0200     return ReadOnlyPart::closeUrl();
0201 }
0202 
0203 void MarkdownPart::prepareViewStateRestoringOnReload()
0204 {
0205     if (url() == m_previousUrl) {
0206         KParts::OpenUrlArguments args(arguments());
0207         args.setXOffset(m_previousScrollPosition.x());
0208         args.setYOffset(m_previousScrollPosition.y());
0209         setArguments(args);
0210     }
0211 }
0212 
0213 void MarkdownPart::restoreScrollPosition()
0214 {
0215     const KParts::OpenUrlArguments args(arguments());
0216     m_widget->setScrollPosition({args.xOffset(), args.yOffset()});
0217 }
0218 
0219 void MarkdownPart::handleOpenUrlRequest(const QUrl& url)
0220 {
0221     QDesktopServices::openUrl(url);
0222 }
0223 
0224 void MarkdownPart::handleContextMenuRequest(QPoint globalPos,
0225                                             const QUrl& linkUrl,
0226                                             bool hasSelection)
0227 {
0228     QMenu menu(m_widget);
0229 
0230     if (!linkUrl.isValid()) {
0231         if (hasSelection) {
0232             menu.addAction(m_copySelectionAction);
0233         } else {
0234             menu.addAction(m_selectAllAction);
0235             if (m_searchToolBar->isHidden()) {
0236                 menu.addAction(m_searchAction);
0237             }
0238         }
0239     } else {
0240         QAction* action = menu.addAction(i18nc("@action", "Open Link"));
0241         connect(action, &QAction::triggered, this, [&] {
0242             handleOpenUrlRequest(linkUrl);
0243         });
0244         menu.addSeparator();
0245 
0246         if (linkUrl.scheme() == QLatin1String("mailto")) {
0247             menu.addAction(createCopyEmailAddressAction(&menu, linkUrl));
0248         } else {
0249             menu.addAction(createCopyLinkUrlAction(&menu, linkUrl));
0250         }
0251     }
0252 
0253     if (!menu.isEmpty()) {
0254         menu.exec(globalPos);
0255     }
0256 }
0257 
0258 void MarkdownPart::showHoveredLink(const QUrl& _linkUrl)
0259 {
0260     QUrl linkUrl = resolvedUrl(_linkUrl);
0261     QString message;
0262     KFileItem fileItem;
0263 
0264     if (linkUrl.isValid()) {
0265 
0266         // Protect the user against URL spoofing!
0267         linkUrl.setUserName(QString());
0268         message = linkUrl.toDisplayString();
0269 
0270         if (linkUrl.scheme() != QLatin1String("mailto")) {
0271             fileItem = KFileItem(linkUrl, QString(), KFileItem::Unknown);
0272         }
0273     }
0274 
0275     Q_EMIT m_browserExtension->mouseOverInfo(fileItem);
0276     Q_EMIT setStatusBarText(message);
0277 }
0278 
0279 QAction* MarkdownPart::copySelectionAction() const
0280 {
0281     return m_copySelectionAction;
0282 }
0283 
0284 QAction* MarkdownPart::createCopyEmailAddressAction(QObject* parent, const QUrl& mailtoUrl)
0285 {
0286     auto* action = new QAction(parent);
0287     action->setText(i18nc("@action", "&Copy Email Address"));
0288     connect(action, &QAction::triggered, parent, [&] {
0289         auto* data = new QMimeData;
0290         data->setText(mailtoUrl.path());
0291         QApplication::clipboard()->setMimeData(data, QClipboard::Clipboard);
0292     });
0293 
0294     return action;
0295 }
0296 
0297 QAction* MarkdownPart::createCopyLinkUrlAction(QObject* parent, const QUrl& linkUrl)
0298 {
0299     auto* action = new QAction(parent);
0300     action->setText(i18nc("@action", "Copy Link &URL"));
0301     connect(action, &QAction::triggered, parent, [&] {
0302         auto* data = new QMimeData;
0303         data->setUrls({linkUrl});
0304         QApplication::clipboard()->setMimeData(data, QClipboard::Clipboard);
0305     });
0306 
0307     return action;
0308 }
0309 
0310 void MarkdownPart::copySelection()
0311 {
0312     m_widget->copy();
0313 }
0314 
0315 void MarkdownPart::selectAll()
0316 {
0317     m_widget->selectAll();
0318 }
0319 
0320 QUrl MarkdownPart::resolvedUrl(const QUrl &url) const
0321 {
0322     QUrl u = url;
0323     if (u.isRelative()) {
0324         const QUrl baseUrl = m_sourceDocument->baseUrl();
0325         u = baseUrl.resolved(u);
0326     }
0327 
0328     return (u.adjusted(QUrl::NormalizePathSegments));
0329 }
0330 
0331 #include "moc_markdownpart.cpp"