markdownviewer.cpp 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417
  1. #include "markdownviewer.h"
  2. #include <QWebChannel>
  3. #include <QContextMenuEvent>
  4. #include <QMenu>
  5. #include <QApplication>
  6. #include <QMimeData>
  7. #include <QScopedPointer>
  8. #include "markdownvieweradapter.h"
  9. #include "previewhelper.h"
  10. #include <utils/clipboardutils.h>
  11. #include <utils/fileutils.h>
  12. #include <utils/utils.h>
  13. #include <utils/widgetutils.h>
  14. #include <core/configmgr.h>
  15. #include <core/editorconfig.h>
  16. #include "../widgetsfactory.h"
  17. using namespace vnotex;
  18. // We set the property of the clipboard to mark that the URL copied in the
  19. // clipboard has been altered.
  20. static const char *c_propertyImageUrlAltered = "CopiedImageUrlAltered";
  21. // Indicate whether this clipboard change is triggered by cross copy.
  22. static const char *c_propertyCrossCopy = "CrossCopy";
  23. MarkdownViewer::MarkdownViewer(MarkdownViewerAdapter *p_adapter,
  24. const QColor &p_background,
  25. qreal p_zoomFactor,
  26. QWidget *p_parent)
  27. : WebViewer(p_background, p_zoomFactor, p_parent),
  28. m_adapter(p_adapter)
  29. {
  30. m_adapter->setParent(this);
  31. auto channel = new QWebChannel(this);
  32. channel->registerObject(QStringLiteral("vxAdapter"), m_adapter);
  33. page()->setWebChannel(channel);
  34. connect(QApplication::clipboard(), &QClipboard::changed,
  35. this, &MarkdownViewer::handleClipboardChanged);
  36. connect(m_adapter, &MarkdownViewerAdapter::keyPressed,
  37. this, &MarkdownViewer::handleWebKeyPress);
  38. connect(m_adapter, &MarkdownViewerAdapter::zoomed,
  39. this, [this](bool p_zoomIn) {
  40. p_zoomIn ? zoomIn() : zoomOut();
  41. });
  42. connect(m_adapter, &MarkdownViewerAdapter::crossCopyReady,
  43. this, [](quint64 p_id, quint64 p_timeStamp, const QString &p_html) {
  44. Q_UNUSED(p_id);
  45. Q_UNUSED(p_timeStamp);
  46. std::unique_ptr<QMimeData> mimeData(new QMimeData());
  47. mimeData->setHtml(p_html);
  48. ClipboardUtils::setMimeDataToClipboard(QApplication::clipboard(), mimeData.release());
  49. });
  50. }
  51. MarkdownViewerAdapter *MarkdownViewer::adapter() const
  52. {
  53. return m_adapter;
  54. }
  55. void MarkdownViewer::setPreviewHelper(PreviewHelper *p_previewHelper)
  56. {
  57. connect(p_previewHelper, &PreviewHelper::graphPreviewRequested,
  58. this, [this, p_previewHelper](quint64 p_id,
  59. TimeStamp p_timeStamp,
  60. const QString &p_lang,
  61. const QString &p_text) {
  62. if (m_adapter->isViewerReady()) {
  63. m_adapter->graphPreviewRequested(p_id, p_timeStamp, p_lang, p_text);
  64. } else {
  65. p_previewHelper->handleGraphPreviewData(MarkdownViewerAdapter::PreviewData());
  66. }
  67. });
  68. connect(p_previewHelper, &PreviewHelper::mathPreviewRequested,
  69. this, [this, p_previewHelper](quint64 p_id,
  70. TimeStamp p_timeStamp,
  71. const QString &p_text) {
  72. if (m_adapter->isViewerReady()) {
  73. m_adapter->mathPreviewRequested(p_id, p_timeStamp, p_text);
  74. } else {
  75. p_previewHelper->handleMathPreviewData(MarkdownViewerAdapter::PreviewData());
  76. }
  77. });
  78. connect(m_adapter, &MarkdownViewerAdapter::graphPreviewDataReady,
  79. p_previewHelper, &PreviewHelper::handleGraphPreviewData);
  80. connect(m_adapter, &MarkdownViewerAdapter::mathPreviewDataReady,
  81. p_previewHelper, &PreviewHelper::handleMathPreviewData);
  82. }
  83. void MarkdownViewer::contextMenuEvent(QContextMenuEvent *p_event)
  84. {
  85. QScopedPointer<QMenu> menu(page()->createStandardContextMenu());
  86. const QList<QAction *> actions = menu->actions();
  87. #if defined(Q_OS_WIN)
  88. if (!m_copyImageUrlActionHooked) {
  89. // "Copy Image URL" action will put the encoded URL to the clipboard as text
  90. // and the URL as URLs. If the URL contains Chinese, OneNote or Word could not
  91. // recognize it.
  92. // We need to change it to only-space-encoded text.
  93. QAction *copyImageUrlAct = pageAction(QWebEnginePage::CopyImageUrlToClipboard);
  94. if (actions.contains(copyImageUrlAct)) {
  95. connect(copyImageUrlAct, &QAction::triggered,
  96. this, &MarkdownViewer::handleCopyImageUrlAction);
  97. m_copyImageUrlActionHooked = true;
  98. }
  99. }
  100. #endif
  101. if (!hasSelection()) {
  102. auto firstAct = actions.isEmpty() ? nullptr : actions[0];
  103. auto editAct = new QAction(tr("&Edit"), menu.data());
  104. WidgetUtils::addActionShortcutText(editAct,
  105. ConfigMgr::getInst().getEditorConfig().getShortcut(EditorConfig::Shortcut::EditRead));
  106. connect(editAct, &QAction::triggered,
  107. this, &MarkdownViewer::editRequested);
  108. menu->insertAction(firstAct, editAct);
  109. if (firstAct) {
  110. menu->insertSeparator(firstAct);
  111. }
  112. }
  113. // We need to replace the "Copy Image" action:
  114. // - the default one use the fully-encoded URL to fetch the image while
  115. // Windows seems to not recognize it.
  116. // - We need to remove the html to let it be recognized by some web pages.
  117. {
  118. auto defaultCopyImageAct = pageAction(QWebEnginePage::CopyImageToClipboard);
  119. if (actions.contains(defaultCopyImageAct)) {
  120. QAction *copyImageAct = new QAction(defaultCopyImageAct->text(), menu.data());
  121. copyImageAct->setToolTip(defaultCopyImageAct->toolTip());
  122. connect(copyImageAct, &QAction::triggered,
  123. this, &MarkdownViewer::copyImage);
  124. menu->insertAction(defaultCopyImageAct, copyImageAct);
  125. defaultCopyImageAct->setVisible(false);
  126. }
  127. }
  128. {
  129. auto copyAct = pageAction(QWebEnginePage::Copy);
  130. if (actions.contains(copyAct)) {
  131. setupCrossCopyMenu(menu.data(), copyAct);
  132. }
  133. }
  134. hideUnusedActions(menu.data());
  135. p_event->accept();
  136. bool valid = false;
  137. for (auto act : menu->actions()) {
  138. // There may be one action visible with text being empty.
  139. if (act->isVisible() && !act->text().isEmpty()) {
  140. valid = true;
  141. break;
  142. }
  143. }
  144. if (valid) {
  145. menu->exec(p_event->globalPos());
  146. }
  147. }
  148. void MarkdownViewer::handleCopyImageUrlAction()
  149. {
  150. // To avoid failure of setting clipboard mime data.
  151. QCoreApplication::processEvents();
  152. QClipboard *clipboard = QApplication::clipboard();
  153. const QMimeData *mimeData = clipboard->mimeData();
  154. clipboard->setProperty(c_propertyImageUrlAltered, false);
  155. if (clipboard->ownsClipboard()
  156. && mimeData->hasText()
  157. && mimeData->hasUrls()) {
  158. QString text = mimeData->text();
  159. QList<QUrl> urls = mimeData->urls();
  160. if (urls.size() == 1
  161. && urls[0].isLocalFile()
  162. && urls[0].toEncoded() == text) {
  163. QString spaceOnlyText = urls[0].toString(QUrl::EncodeSpaces);
  164. if (spaceOnlyText != text) {
  165. // Set new mime data.
  166. QMimeData *data = new QMimeData();
  167. data->setUrls(urls);
  168. data->setText(spaceOnlyText);
  169. ClipboardUtils::setMimeDataToClipboard(clipboard, data, QClipboard::Clipboard);
  170. clipboard->setProperty(c_propertyImageUrlAltered, true);
  171. qDebug() << "clipboard copy image URL altered" << spaceOnlyText;
  172. }
  173. }
  174. }
  175. }
  176. void MarkdownViewer::copyImage()
  177. {
  178. #if defined(Q_OS_WIN)
  179. Q_ASSERT(m_copyImageUrlActionHooked);
  180. // triggerPageAction(QWebEnginePage::CopyImageUrlToClipboard) will not really
  181. // trigger the corresponding action. It just do the stuff directly.
  182. QAction *copyImageUrlAct = pageAction(QWebEnginePage::CopyImageUrlToClipboard);
  183. copyImageUrlAct->trigger();
  184. QCoreApplication::processEvents();
  185. QClipboard *clipboard = QApplication::clipboard();
  186. if (clipboard->property(c_propertyImageUrlAltered).toBool()) {
  187. const QMimeData *mimeData = clipboard->mimeData();
  188. QString imgPath;
  189. if (mimeData->hasUrls()) {
  190. QList<QUrl> urls = mimeData->urls();
  191. if (!urls.isEmpty() && urls[0].isLocalFile()) {
  192. imgPath = urls[0].toLocalFile();
  193. }
  194. }
  195. if (!imgPath.isEmpty()) {
  196. QImage img = FileUtils::imageFromFile(imgPath);
  197. if (!img.isNull()) {
  198. m_copyImageTriggered = false;
  199. ClipboardUtils::setImageToClipboard(clipboard, img, QClipboard::Clipboard);
  200. return;
  201. }
  202. }
  203. }
  204. #endif
  205. m_copyImageTriggered = true;
  206. // Fall back.
  207. triggerPageAction(QWebEnginePage::CopyImageToClipboard);
  208. }
  209. void MarkdownViewer::handleClipboardChanged(QClipboard::Mode p_mode)
  210. {
  211. if (!hasFocus() || p_mode != QClipboard::Clipboard) {
  212. return;
  213. }
  214. QClipboard *clipboard = QApplication::clipboard();
  215. if (!clipboard->ownsClipboard()) {
  216. return;
  217. }
  218. const QMimeData *mimeData = clipboard->mimeData();
  219. if (m_copyImageTriggered) {
  220. m_copyImageTriggered = false;
  221. removeHtmlFromImageData(clipboard, mimeData);
  222. return;
  223. }
  224. if (clipboard->property(c_propertyCrossCopy).toBool()) {
  225. clipboard->setProperty(c_propertyCrossCopy, false);
  226. if (mimeData->hasHtml() && !mimeData->hasImage() && !m_crossCopyTarget.isEmpty()) {
  227. crossCopy(m_crossCopyTarget, url().toString(), mimeData->html());
  228. }
  229. }
  230. }
  231. void MarkdownViewer::removeHtmlFromImageData(QClipboard *p_clipboard,
  232. const QMimeData *p_mimeData)
  233. {
  234. if (!p_mimeData->hasImage()) {
  235. return;
  236. }
  237. if (p_mimeData->hasHtml()) {
  238. qDebug() << "remove HTML from image QMimeData" << p_mimeData->html();
  239. QMimeData *data = new QMimeData();
  240. data->setImageData(p_mimeData->imageData());
  241. ClipboardUtils::setMimeDataToClipboard(p_clipboard, data, QClipboard::Clipboard);
  242. }
  243. }
  244. void MarkdownViewer::hideUnusedActions(QMenu *p_menu)
  245. {
  246. QList<QAction *> unusedActions;
  247. // QWebEnginePage uses different actions of Back/Forward/Reload.
  248. // [Woboq](https://code.woboq.org/qt5/qtwebengine/src/webenginewidgets/api/qwebenginepage.cpp.html#1652)
  249. // We tell these three actions by name.
  250. const QStringList actionNames({QWebEnginePage::tr("&Back"),
  251. QWebEnginePage::tr("&Forward"),
  252. QWebEnginePage::tr("&Reload")});
  253. const QList<QAction *> actions = p_menu->actions();
  254. for (auto it : actions) {
  255. if (actionNames.contains(it->text())) {
  256. unusedActions.append(it);
  257. }
  258. }
  259. QVector<QWebEnginePage::WebAction> pageActions = { QWebEnginePage::SavePage,
  260. QWebEnginePage::ViewSource,
  261. QWebEnginePage::DownloadImageToDisk,
  262. QWebEnginePage::DownloadLinkToDisk,
  263. QWebEnginePage::OpenLinkInThisWindow,
  264. QWebEnginePage::OpenLinkInNewBackgroundTab,
  265. QWebEnginePage::OpenLinkInNewTab,
  266. QWebEnginePage::OpenLinkInNewWindow
  267. };
  268. for (auto pageAct : pageActions) {
  269. auto act = pageAction(pageAct);
  270. unusedActions.append(act);
  271. }
  272. for (auto it : unusedActions) {
  273. if (it) {
  274. it->setVisible(false);
  275. }
  276. }
  277. }
  278. void MarkdownViewer::handleWebKeyPress(int p_key, bool p_ctrl, bool p_shift, bool p_meta)
  279. {
  280. Q_UNUSED(p_shift);
  281. Q_UNUSED(p_meta);
  282. switch (p_key) {
  283. // Esc
  284. case 27:
  285. break;
  286. // Dash
  287. case 189:
  288. if (p_ctrl) {
  289. // Zoom out.
  290. zoomOut();
  291. }
  292. break;
  293. // Equal
  294. case 187:
  295. if (p_ctrl) {
  296. // Zoom in.
  297. zoomIn();
  298. }
  299. break;
  300. // 0
  301. case 48:
  302. if (p_ctrl) {
  303. // Recover zoom.
  304. restoreZoom();
  305. }
  306. break;
  307. default:
  308. break;
  309. }
  310. }
  311. void MarkdownViewer::zoomOut()
  312. {
  313. qreal factor = zoomFactor();
  314. if (factor > 0.25) {
  315. factor -= 0.25;
  316. setZoomFactor(factor);
  317. emit zoomFactorChanged(factor);
  318. }
  319. }
  320. void MarkdownViewer::zoomIn()
  321. {
  322. qreal factor = zoomFactor();
  323. factor += 0.25;
  324. setZoomFactor(factor);
  325. emit zoomFactorChanged(factor);
  326. }
  327. void MarkdownViewer::restoreZoom()
  328. {
  329. setZoomFactor(1);
  330. emit zoomFactorChanged(1);
  331. }
  332. void MarkdownViewer::setupCrossCopyMenu(QMenu *p_menu, QAction *p_copyAct)
  333. {
  334. const auto &targets = m_adapter->getCrossCopyTargets();
  335. if (targets.isEmpty()) {
  336. return;
  337. }
  338. auto subMenu = WidgetsFactory::createMenu(tr("Cross Copy"), p_menu);
  339. for (const auto &target : targets) {
  340. auto act = subMenu->addAction(m_adapter->getCrossCopyTargetDisplayName(target));
  341. act->setData(target);
  342. }
  343. connect(subMenu, &QMenu::triggered,
  344. this, [this](QAction *p_act) {
  345. // selectedText() will return a plain text, so we trigger the Copy action here.
  346. m_crossCopyTarget = p_act->data().toString();
  347. QClipboard *clipboard = QApplication::clipboard();
  348. clipboard->setProperty(c_propertyCrossCopy, true);
  349. // Will handle the remaining logics in handleClipboardChanged().
  350. triggerPageAction(QWebEnginePage::Copy);
  351. });
  352. auto menuAct = p_menu->insertMenu(p_copyAct, subMenu);
  353. p_menu->removeAction(p_copyAct);
  354. p_menu->insertAction(menuAct, p_copyAct);
  355. }
  356. void MarkdownViewer::crossCopy(const QString &p_target, const QString &p_baseUrl, const QString &p_html)
  357. {
  358. emit m_adapter->crossCopyRequested(0, 0, p_target, p_baseUrl, p_html);
  359. }