File indexing completed on 2023-09-24 09:54:56

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