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"