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"