123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417 |
- #include "markdownviewer.h"
- #include <QWebChannel>
- #include <QContextMenuEvent>
- #include <QMenu>
- #include <QApplication>
- #include <QMimeData>
- #include <QScopedPointer>
- #include "markdownvieweradapter.h"
- #include "previewhelper.h"
- #include <utils/clipboardutils.h>
- #include <utils/fileutils.h>
- #include <utils/utils.h>
- #include <utils/widgetutils.h>
- #include <core/configmgr.h>
- #include <core/editorconfig.h>
- #include "../widgetsfactory.h"
- using namespace vnotex;
- // We set the property of the clipboard to mark that the URL copied in the
- // clipboard has been altered.
- static const char *c_propertyImageUrlAltered = "CopiedImageUrlAltered";
- // Indicate whether this clipboard change is triggered by cross copy.
- static const char *c_propertyCrossCopy = "CrossCopy";
- MarkdownViewer::MarkdownViewer(MarkdownViewerAdapter *p_adapter,
- const QColor &p_background,
- qreal p_zoomFactor,
- QWidget *p_parent)
- : WebViewer(p_background, p_zoomFactor, p_parent),
- m_adapter(p_adapter)
- {
- m_adapter->setParent(this);
- auto channel = new QWebChannel(this);
- channel->registerObject(QStringLiteral("vxAdapter"), m_adapter);
- page()->setWebChannel(channel);
- connect(QApplication::clipboard(), &QClipboard::changed,
- this, &MarkdownViewer::handleClipboardChanged);
- connect(m_adapter, &MarkdownViewerAdapter::keyPressed,
- this, &MarkdownViewer::handleWebKeyPress);
- connect(m_adapter, &MarkdownViewerAdapter::zoomed,
- this, [this](bool p_zoomIn) {
- p_zoomIn ? zoomIn() : zoomOut();
- });
- connect(m_adapter, &MarkdownViewerAdapter::crossCopyReady,
- this, [](quint64 p_id, quint64 p_timeStamp, const QString &p_html) {
- Q_UNUSED(p_id);
- Q_UNUSED(p_timeStamp);
- std::unique_ptr<QMimeData> mimeData(new QMimeData());
- mimeData->setHtml(p_html);
- ClipboardUtils::setMimeDataToClipboard(QApplication::clipboard(), mimeData.release());
- });
- }
- MarkdownViewerAdapter *MarkdownViewer::adapter() const
- {
- return m_adapter;
- }
- void MarkdownViewer::setPreviewHelper(PreviewHelper *p_previewHelper)
- {
- connect(p_previewHelper, &PreviewHelper::graphPreviewRequested,
- this, [this, p_previewHelper](quint64 p_id,
- TimeStamp p_timeStamp,
- const QString &p_lang,
- const QString &p_text) {
- if (m_adapter->isViewerReady()) {
- m_adapter->graphPreviewRequested(p_id, p_timeStamp, p_lang, p_text);
- } else {
- p_previewHelper->handleGraphPreviewData(MarkdownViewerAdapter::PreviewData());
- }
- });
- connect(p_previewHelper, &PreviewHelper::mathPreviewRequested,
- this, [this, p_previewHelper](quint64 p_id,
- TimeStamp p_timeStamp,
- const QString &p_text) {
- if (m_adapter->isViewerReady()) {
- m_adapter->mathPreviewRequested(p_id, p_timeStamp, p_text);
- } else {
- p_previewHelper->handleMathPreviewData(MarkdownViewerAdapter::PreviewData());
- }
- });
- connect(m_adapter, &MarkdownViewerAdapter::graphPreviewDataReady,
- p_previewHelper, &PreviewHelper::handleGraphPreviewData);
- connect(m_adapter, &MarkdownViewerAdapter::mathPreviewDataReady,
- p_previewHelper, &PreviewHelper::handleMathPreviewData);
- }
- void MarkdownViewer::contextMenuEvent(QContextMenuEvent *p_event)
- {
- QScopedPointer<QMenu> menu(page()->createStandardContextMenu());
- const QList<QAction *> actions = menu->actions();
- #if defined(Q_OS_WIN)
- if (!m_copyImageUrlActionHooked) {
- // "Copy Image URL" action will put the encoded URL to the clipboard as text
- // and the URL as URLs. If the URL contains Chinese, OneNote or Word could not
- // recognize it.
- // We need to change it to only-space-encoded text.
- QAction *copyImageUrlAct = pageAction(QWebEnginePage::CopyImageUrlToClipboard);
- if (actions.contains(copyImageUrlAct)) {
- connect(copyImageUrlAct, &QAction::triggered,
- this, &MarkdownViewer::handleCopyImageUrlAction);
- m_copyImageUrlActionHooked = true;
- }
- }
- #endif
- if (!hasSelection()) {
- auto firstAct = actions.isEmpty() ? nullptr : actions[0];
- auto editAct = new QAction(tr("&Edit"), menu.data());
- WidgetUtils::addActionShortcutText(editAct,
- ConfigMgr::getInst().getEditorConfig().getShortcut(EditorConfig::Shortcut::EditRead));
- connect(editAct, &QAction::triggered,
- this, &MarkdownViewer::editRequested);
- menu->insertAction(firstAct, editAct);
- if (firstAct) {
- menu->insertSeparator(firstAct);
- }
- }
- // We need to replace the "Copy Image" action:
- // - the default one use the fully-encoded URL to fetch the image while
- // Windows seems to not recognize it.
- // - We need to remove the html to let it be recognized by some web pages.
- {
- auto defaultCopyImageAct = pageAction(QWebEnginePage::CopyImageToClipboard);
- if (actions.contains(defaultCopyImageAct)) {
- QAction *copyImageAct = new QAction(defaultCopyImageAct->text(), menu.data());
- copyImageAct->setToolTip(defaultCopyImageAct->toolTip());
- connect(copyImageAct, &QAction::triggered,
- this, &MarkdownViewer::copyImage);
- menu->insertAction(defaultCopyImageAct, copyImageAct);
- defaultCopyImageAct->setVisible(false);
- }
- }
- {
- auto copyAct = pageAction(QWebEnginePage::Copy);
- if (actions.contains(copyAct)) {
- setupCrossCopyMenu(menu.data(), copyAct);
- }
- }
- hideUnusedActions(menu.data());
- p_event->accept();
- bool valid = false;
- for (auto act : menu->actions()) {
- // There may be one action visible with text being empty.
- if (act->isVisible() && !act->text().isEmpty()) {
- valid = true;
- break;
- }
- }
- if (valid) {
- menu->exec(p_event->globalPos());
- }
- }
- void MarkdownViewer::handleCopyImageUrlAction()
- {
- // To avoid failure of setting clipboard mime data.
- QCoreApplication::processEvents();
- QClipboard *clipboard = QApplication::clipboard();
- const QMimeData *mimeData = clipboard->mimeData();
- clipboard->setProperty(c_propertyImageUrlAltered, false);
- if (clipboard->ownsClipboard()
- && mimeData->hasText()
- && mimeData->hasUrls()) {
- QString text = mimeData->text();
- QList<QUrl> urls = mimeData->urls();
- if (urls.size() == 1
- && urls[0].isLocalFile()
- && urls[0].toEncoded() == text) {
- QString spaceOnlyText = urls[0].toString(QUrl::EncodeSpaces);
- if (spaceOnlyText != text) {
- // Set new mime data.
- QMimeData *data = new QMimeData();
- data->setUrls(urls);
- data->setText(spaceOnlyText);
- ClipboardUtils::setMimeDataToClipboard(clipboard, data, QClipboard::Clipboard);
- clipboard->setProperty(c_propertyImageUrlAltered, true);
- qDebug() << "clipboard copy image URL altered" << spaceOnlyText;
- }
- }
- }
- }
- void MarkdownViewer::copyImage()
- {
- #if defined(Q_OS_WIN)
- Q_ASSERT(m_copyImageUrlActionHooked);
- // triggerPageAction(QWebEnginePage::CopyImageUrlToClipboard) will not really
- // trigger the corresponding action. It just do the stuff directly.
- QAction *copyImageUrlAct = pageAction(QWebEnginePage::CopyImageUrlToClipboard);
- copyImageUrlAct->trigger();
- QCoreApplication::processEvents();
- QClipboard *clipboard = QApplication::clipboard();
- if (clipboard->property(c_propertyImageUrlAltered).toBool()) {
- const QMimeData *mimeData = clipboard->mimeData();
- QString imgPath;
- if (mimeData->hasUrls()) {
- QList<QUrl> urls = mimeData->urls();
- if (!urls.isEmpty() && urls[0].isLocalFile()) {
- imgPath = urls[0].toLocalFile();
- }
- }
- if (!imgPath.isEmpty()) {
- QImage img = FileUtils::imageFromFile(imgPath);
- if (!img.isNull()) {
- m_copyImageTriggered = false;
- ClipboardUtils::setImageToClipboard(clipboard, img, QClipboard::Clipboard);
- return;
- }
- }
- }
- #endif
- m_copyImageTriggered = true;
- // Fall back.
- triggerPageAction(QWebEnginePage::CopyImageToClipboard);
- }
- void MarkdownViewer::handleClipboardChanged(QClipboard::Mode p_mode)
- {
- if (!hasFocus() || p_mode != QClipboard::Clipboard) {
- return;
- }
- QClipboard *clipboard = QApplication::clipboard();
- if (!clipboard->ownsClipboard()) {
- return;
- }
- const QMimeData *mimeData = clipboard->mimeData();
- if (m_copyImageTriggered) {
- m_copyImageTriggered = false;
- removeHtmlFromImageData(clipboard, mimeData);
- return;
- }
- if (clipboard->property(c_propertyCrossCopy).toBool()) {
- clipboard->setProperty(c_propertyCrossCopy, false);
- if (mimeData->hasHtml() && !mimeData->hasImage() && !m_crossCopyTarget.isEmpty()) {
- crossCopy(m_crossCopyTarget, url().toString(), mimeData->html());
- }
- }
- }
- void MarkdownViewer::removeHtmlFromImageData(QClipboard *p_clipboard,
- const QMimeData *p_mimeData)
- {
- if (!p_mimeData->hasImage()) {
- return;
- }
- if (p_mimeData->hasHtml()) {
- qDebug() << "remove HTML from image QMimeData" << p_mimeData->html();
- QMimeData *data = new QMimeData();
- data->setImageData(p_mimeData->imageData());
- ClipboardUtils::setMimeDataToClipboard(p_clipboard, data, QClipboard::Clipboard);
- }
- }
- void MarkdownViewer::hideUnusedActions(QMenu *p_menu)
- {
- QList<QAction *> unusedActions;
- // QWebEnginePage uses different actions of Back/Forward/Reload.
- // [Woboq](https://code.woboq.org/qt5/qtwebengine/src/webenginewidgets/api/qwebenginepage.cpp.html#1652)
- // We tell these three actions by name.
- const QStringList actionNames({QWebEnginePage::tr("&Back"),
- QWebEnginePage::tr("&Forward"),
- QWebEnginePage::tr("&Reload")});
- const QList<QAction *> actions = p_menu->actions();
- for (auto it : actions) {
- if (actionNames.contains(it->text())) {
- unusedActions.append(it);
- }
- }
- QVector<QWebEnginePage::WebAction> pageActions = { QWebEnginePage::SavePage,
- QWebEnginePage::ViewSource,
- QWebEnginePage::DownloadImageToDisk,
- QWebEnginePage::DownloadLinkToDisk,
- QWebEnginePage::OpenLinkInThisWindow,
- QWebEnginePage::OpenLinkInNewBackgroundTab,
- QWebEnginePage::OpenLinkInNewTab,
- QWebEnginePage::OpenLinkInNewWindow
- };
- for (auto pageAct : pageActions) {
- auto act = pageAction(pageAct);
- unusedActions.append(act);
- }
- for (auto it : unusedActions) {
- if (it) {
- it->setVisible(false);
- }
- }
- }
- void MarkdownViewer::handleWebKeyPress(int p_key, bool p_ctrl, bool p_shift, bool p_meta)
- {
- Q_UNUSED(p_shift);
- Q_UNUSED(p_meta);
- switch (p_key) {
- // Esc
- case 27:
- break;
- // Dash
- case 189:
- if (p_ctrl) {
- // Zoom out.
- zoomOut();
- }
- break;
- // Equal
- case 187:
- if (p_ctrl) {
- // Zoom in.
- zoomIn();
- }
- break;
- // 0
- case 48:
- if (p_ctrl) {
- // Recover zoom.
- restoreZoom();
- }
- break;
- default:
- break;
- }
- }
- void MarkdownViewer::zoomOut()
- {
- qreal factor = zoomFactor();
- if (factor > 0.25) {
- factor -= 0.25;
- setZoomFactor(factor);
- emit zoomFactorChanged(factor);
- }
- }
- void MarkdownViewer::zoomIn()
- {
- qreal factor = zoomFactor();
- factor += 0.25;
- setZoomFactor(factor);
- emit zoomFactorChanged(factor);
- }
- void MarkdownViewer::restoreZoom()
- {
- setZoomFactor(1);
- emit zoomFactorChanged(1);
- }
- void MarkdownViewer::setupCrossCopyMenu(QMenu *p_menu, QAction *p_copyAct)
- {
- const auto &targets = m_adapter->getCrossCopyTargets();
- if (targets.isEmpty()) {
- return;
- }
- auto subMenu = WidgetsFactory::createMenu(tr("Cross Copy"), p_menu);
- for (const auto &target : targets) {
- auto act = subMenu->addAction(m_adapter->getCrossCopyTargetDisplayName(target));
- act->setData(target);
- }
- connect(subMenu, &QMenu::triggered,
- this, [this](QAction *p_act) {
- // selectedText() will return a plain text, so we trigger the Copy action here.
- m_crossCopyTarget = p_act->data().toString();
- QClipboard *clipboard = QApplication::clipboard();
- clipboard->setProperty(c_propertyCrossCopy, true);
- // Will handle the remaining logics in handleClipboardChanged().
- triggerPageAction(QWebEnginePage::Copy);
- });
- auto menuAct = p_menu->insertMenu(p_copyAct, subMenu);
- p_menu->removeAction(p_copyAct);
- p_menu->insertAction(menuAct, p_copyAct);
- }
- void MarkdownViewer::crossCopy(const QString &p_target, const QString &p_baseUrl, const QString &p_html)
- {
- emit m_adapter->crossCopyRequested(0, 0, p_target, p_baseUrl, p_html);
- }
|