vedittab.cpp 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659
  1. #include <QtWidgets>
  2. #include <QWebChannel>
  3. #include <QWebEngineView>
  4. #include <QFileInfo>
  5. #include <QXmlStreamReader>
  6. #include "vedittab.h"
  7. #include "vedit.h"
  8. #include "vdocument.h"
  9. #include "vnote.h"
  10. #include "utils/vutils.h"
  11. #include "vpreviewpage.h"
  12. #include "hgmarkdownhighlighter.h"
  13. #include "vconfigmanager.h"
  14. #include "vmarkdownconverter.h"
  15. #include "vnotebook.h"
  16. #include "vtoc.h"
  17. #include "vmdedit.h"
  18. #include "dialog/vfindreplacedialog.h"
  19. #include "veditarea.h"
  20. #include "vconstants.h"
  21. extern VConfigManager vconfig;
  22. VEditTab::VEditTab(VFile *p_file, OpenFileMode p_mode, QWidget *p_parent)
  23. : QStackedWidget(p_parent), m_file(p_file), isEditMode(false), document(p_file, this),
  24. mdConverterType(vconfig.getMdConverterType()), m_fileModified(false),
  25. m_editArea(NULL)
  26. {
  27. tableOfContent.filePath = p_file->retrivePath();
  28. curHeader.filePath = p_file->retrivePath();
  29. Q_ASSERT(!m_file->isOpened());
  30. m_file->open();
  31. setupUI();
  32. if (p_mode == OpenFileMode::Edit) {
  33. showFileEditMode();
  34. } else {
  35. showFileReadMode();
  36. }
  37. connect(qApp, &QApplication::focusChanged,
  38. this, &VEditTab::handleFocusChanged);
  39. }
  40. VEditTab::~VEditTab()
  41. {
  42. if (m_file) {
  43. m_file->close();
  44. }
  45. }
  46. void VEditTab::init(VEditArea *p_editArea)
  47. {
  48. m_editArea = p_editArea;
  49. }
  50. void VEditTab::setupUI()
  51. {
  52. switch (m_file->getDocType()) {
  53. case DocType::Markdown:
  54. m_textEditor = new VMdEdit(m_file, this);
  55. connect(dynamic_cast<VMdEdit *>(m_textEditor), &VMdEdit::headersChanged,
  56. this, &VEditTab::updateTocFromHeaders);
  57. connect(dynamic_cast<VMdEdit *>(m_textEditor), &VMdEdit::statusChanged,
  58. this, &VEditTab::noticeStatusChanged);
  59. connect(m_textEditor, SIGNAL(curHeaderChanged(int, int)),
  60. this, SLOT(updateCurHeader(int, int)));
  61. connect(m_textEditor, &VEdit::textChanged,
  62. this, &VEditTab::handleTextChanged);
  63. m_textEditor->reloadFile();
  64. addWidget(m_textEditor);
  65. setupMarkdownPreview();
  66. break;
  67. case DocType::Html:
  68. m_textEditor = new VEdit(m_file, this);
  69. connect(m_textEditor, &VEdit::textChanged,
  70. this, &VEditTab::handleTextChanged);
  71. m_textEditor->reloadFile();
  72. addWidget(m_textEditor);
  73. webPreviewer = NULL;
  74. break;
  75. default:
  76. qWarning() << "unknown doc type" << int(m_file->getDocType());
  77. Q_ASSERT(false);
  78. }
  79. }
  80. void VEditTab::handleTextChanged()
  81. {
  82. if (m_fileModified) {
  83. return;
  84. }
  85. noticeStatusChanged();
  86. }
  87. void VEditTab::noticeStatusChanged()
  88. {
  89. m_fileModified = m_file->isModified();
  90. emit statusChanged();
  91. }
  92. void VEditTab::showFileReadMode()
  93. {
  94. qDebug() << "read" << m_file->getName();
  95. isEditMode = false;
  96. int outlineIndex = curHeader.m_outlineIndex;
  97. switch (m_file->getDocType()) {
  98. case DocType::Html:
  99. m_textEditor->setReadOnly(true);
  100. break;
  101. case DocType::Markdown:
  102. if (mdConverterType == MarkdownConverterType::Hoedown) {
  103. previewByConverter();
  104. } else {
  105. document.updateText();
  106. updateTocFromHtml(document.getToc());
  107. }
  108. setCurrentWidget(webPreviewer);
  109. clearSearchedWordHighlight();
  110. scrollPreviewToHeader(outlineIndex);
  111. break;
  112. default:
  113. qWarning() << "unknown doc type" << int(m_file->getDocType());
  114. Q_ASSERT(false);
  115. }
  116. noticeStatusChanged();
  117. }
  118. void VEditTab::scrollPreviewToHeader(int p_outlineIndex)
  119. {
  120. Q_ASSERT(p_outlineIndex >= 0);
  121. if (p_outlineIndex < tableOfContent.headers.size()) {
  122. QString anchor = tableOfContent.headers[p_outlineIndex].anchor;
  123. qDebug() << "scroll preview to" << p_outlineIndex << anchor;
  124. if (!anchor.isEmpty()) {
  125. document.scrollToAnchor(anchor.mid(1));
  126. }
  127. }
  128. }
  129. void VEditTab::previewByConverter()
  130. {
  131. VMarkdownConverter mdConverter;
  132. const QString &content = m_file->getContent();
  133. QString html = mdConverter.generateHtml(content, vconfig.getMarkdownExtensions());
  134. QRegularExpression tocExp("<p>\\[TOC\\]<\\/p>", QRegularExpression::CaseInsensitiveOption);
  135. QString toc = mdConverter.generateToc(content, vconfig.getMarkdownExtensions());
  136. processHoedownToc(toc);
  137. html.replace(tocExp, toc);
  138. document.setHtml(html);
  139. // Hoedown will add '\n' while Marked does not
  140. updateTocFromHtml(toc);
  141. }
  142. void VEditTab::processHoedownToc(QString &p_toc)
  143. {
  144. // Hoedown will add '\n'.
  145. p_toc.replace("\n", "");
  146. // Hoedown will translate `_` in title to `<em>`.
  147. p_toc.replace("<em>", "_");
  148. p_toc.replace("</em>", "_");
  149. }
  150. void VEditTab::showFileEditMode()
  151. {
  152. isEditMode = true;
  153. // beginEdit() may change curHeader.
  154. int outlineIndex = curHeader.m_outlineIndex;
  155. m_textEditor->beginEdit();
  156. setCurrentWidget(m_textEditor);
  157. if (m_file->getDocType() == DocType::Markdown) {
  158. dynamic_cast<VMdEdit *>(m_textEditor)->scrollToHeader(outlineIndex);
  159. }
  160. m_textEditor->setFocus();
  161. noticeStatusChanged();
  162. }
  163. bool VEditTab::closeFile(bool p_forced)
  164. {
  165. if (p_forced && isEditMode) {
  166. // Discard buffer content
  167. m_textEditor->reloadFile();
  168. m_textEditor->endEdit();
  169. showFileReadMode();
  170. } else {
  171. readFile();
  172. }
  173. return !isEditMode;
  174. }
  175. void VEditTab::editFile()
  176. {
  177. if (isEditMode) {
  178. return;
  179. }
  180. showFileEditMode();
  181. }
  182. void VEditTab::readFile()
  183. {
  184. if (!isEditMode) {
  185. return;
  186. }
  187. if (m_textEditor->isModified()) {
  188. // Prompt to save the changes
  189. int ret = VUtils::showMessage(QMessageBox::Information, tr("Information"),
  190. tr("Note %1 has been modified.").arg(m_file->getName()),
  191. tr("Do you want to save your changes?"),
  192. QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel,
  193. QMessageBox::Save, this);
  194. switch (ret) {
  195. case QMessageBox::Save:
  196. saveFile();
  197. // Fall through
  198. case QMessageBox::Discard:
  199. m_textEditor->reloadFile();
  200. break;
  201. case QMessageBox::Cancel:
  202. // Nothing to do if user cancel this action
  203. return;
  204. default:
  205. qWarning() << "wrong return value from QMessageBox:" << ret;
  206. return;
  207. }
  208. }
  209. m_textEditor->endEdit();
  210. showFileReadMode();
  211. }
  212. bool VEditTab::saveFile()
  213. {
  214. bool ret;
  215. if (!isEditMode || !m_textEditor->isModified()) {
  216. return true;
  217. }
  218. // Make sure the file already exists. Temporary deal with cases when user delete or move
  219. // a file.
  220. QString filePath = m_file->retrivePath();
  221. if (!QFile(filePath).exists()) {
  222. qWarning() << filePath << "being written has been removed";
  223. VUtils::showMessage(QMessageBox::Warning, tr("Warning"), tr("Fail to save note."),
  224. tr("%1 being written has been removed.").arg(filePath),
  225. QMessageBox::Ok, QMessageBox::Ok, this);
  226. return false;
  227. }
  228. m_textEditor->saveFile();
  229. ret = m_file->save();
  230. if (!ret) {
  231. VUtils::showMessage(QMessageBox::Warning, tr("Warning"), tr("Fail to save note."),
  232. tr("Fail to write to disk when saving a note. Please try it again."),
  233. QMessageBox::Ok, QMessageBox::Ok, this);
  234. m_textEditor->setModified(true);
  235. }
  236. noticeStatusChanged();
  237. return ret;
  238. }
  239. void VEditTab::setupMarkdownPreview()
  240. {
  241. const QString jsHolder("JS_PLACE_HOLDER");
  242. const QString extraHolder("<!-- EXTRA_PLACE_HOLDER -->");
  243. webPreviewer = new QWebEngineView(this);
  244. VPreviewPage *page = new VPreviewPage(this);
  245. webPreviewer->setPage(page);
  246. webPreviewer->setZoomFactor(vconfig.getWebZoomFactor());
  247. QWebChannel *channel = new QWebChannel(this);
  248. channel->registerObject(QStringLiteral("content"), &document);
  249. connect(&document, &VDocument::tocChanged,
  250. this, &VEditTab::updateTocFromHtml);
  251. connect(&document, SIGNAL(headerChanged(const QString&)),
  252. this, SLOT(updateCurHeader(const QString &)));
  253. connect(&document, &VDocument::keyPressed,
  254. this, &VEditTab::handleWebKeyPressed);
  255. page->setWebChannel(channel);
  256. QString jsFile, extraFile;
  257. switch (mdConverterType) {
  258. case MarkdownConverterType::Marked:
  259. jsFile = "qrc" + VNote::c_markedJsFile;
  260. extraFile = "<script src=\"qrc" + VNote::c_markedExtraFile + "\"></script>\n";
  261. break;
  262. case MarkdownConverterType::Hoedown:
  263. jsFile = "qrc" + VNote::c_hoedownJsFile;
  264. break;
  265. case MarkdownConverterType::MarkdownIt:
  266. jsFile = "qrc" + VNote::c_markdownitJsFile;
  267. extraFile = "<script src=\"qrc" + VNote::c_markdownitExtraFile + "\"></script>\n" +
  268. "<script src=\"qrc" + VNote::c_markdownitAnchorExtraFile + "\"></script>\n" +
  269. "<script src=\"qrc" + VNote::c_markdownitTaskListExtraFile + "\"></script>\n";
  270. break;
  271. default:
  272. Q_ASSERT(false);
  273. }
  274. if (vconfig.getEnableMermaid()) {
  275. extraFile += "<link rel=\"stylesheet\" type=\"text/css\" href=\"qrc" + VNote::c_mermaidCssFile +
  276. "\"/>\n" + "<script src=\"qrc" + VNote::c_mermaidApiJsFile + "\"></script>\n" +
  277. "<script>var VEnableMermaid = true;</script>\n";
  278. }
  279. if (vconfig.getEnableMathjax()) {
  280. extraFile += "<script type=\"text/x-mathjax-config\">"
  281. "MathJax.Hub.Config({\n"
  282. " tex2jax: {inlineMath: [['$','$'], ['\\\\(','\\\\)']]},\n"
  283. " showProcessingMessages: false,\n"
  284. " messageStyle: \"none\"});\n"
  285. "</script>\n"
  286. "<script type=\"text/javascript\" async src=\"" + VNote::c_mathjaxJsFile + "\"></script>\n" +
  287. "<script>var VEnableMathjax = true;</script>\n";
  288. }
  289. QString htmlTemplate = VNote::s_markdownTemplate;
  290. htmlTemplate.replace(jsHolder, jsFile);
  291. if (!extraFile.isEmpty()) {
  292. htmlTemplate.replace(extraHolder, extraFile);
  293. }
  294. webPreviewer->setHtml(htmlTemplate, QUrl::fromLocalFile(m_file->retriveBasePath() + QDir::separator()));
  295. addWidget(webPreviewer);
  296. }
  297. void VEditTab::focusTab()
  298. {
  299. currentWidget()->setFocus();
  300. emit getFocused();
  301. }
  302. void VEditTab::handleFocusChanged(QWidget * /* old */, QWidget *now)
  303. {
  304. if (isChild(now)) {
  305. if (now == this) {
  306. // When VEditTab get focus, it should focus to current widget.
  307. currentWidget()->setFocus();
  308. }
  309. emit getFocused();
  310. }
  311. }
  312. void VEditTab::updateTocFromHtml(const QString &tocHtml)
  313. {
  314. if (isEditMode) {
  315. return;
  316. }
  317. tableOfContent.type = VHeaderType::Anchor;
  318. QVector<VHeader> &headers = tableOfContent.headers;
  319. headers.clear();
  320. if (!tocHtml.isEmpty()) {
  321. QXmlStreamReader xml(tocHtml);
  322. if (xml.readNextStartElement()) {
  323. if (xml.name() == "ul") {
  324. parseTocUl(xml, headers, 1);
  325. } else {
  326. qWarning() << "TOC HTML does not start with <ul>";
  327. }
  328. }
  329. if (xml.hasError()) {
  330. qWarning() << "fail to parse TOC in HTML";
  331. return;
  332. }
  333. }
  334. tableOfContent.filePath = m_file->retrivePath();
  335. tableOfContent.valid = true;
  336. emit outlineChanged(tableOfContent);
  337. }
  338. void VEditTab::updateTocFromHeaders(const QVector<VHeader> &headers)
  339. {
  340. if (!isEditMode) {
  341. return;
  342. }
  343. tableOfContent.type = VHeaderType::LineNumber;
  344. tableOfContent.headers = headers;
  345. tableOfContent.filePath = m_file->retrivePath();
  346. tableOfContent.valid = true;
  347. emit outlineChanged(tableOfContent);
  348. }
  349. void VEditTab::parseTocUl(QXmlStreamReader &xml, QVector<VHeader> &headers, int level)
  350. {
  351. Q_ASSERT(xml.isStartElement() && xml.name() == "ul");
  352. while (xml.readNextStartElement()) {
  353. if (xml.name() == "li") {
  354. parseTocLi(xml, headers, level);
  355. } else {
  356. qWarning() << "TOC HTML <ul> should contain <li>" << xml.name();
  357. break;
  358. }
  359. }
  360. }
  361. void VEditTab::parseTocLi(QXmlStreamReader &xml, QVector<VHeader> &headers, int level)
  362. {
  363. Q_ASSERT(xml.isStartElement() && xml.name() == "li");
  364. if (xml.readNextStartElement()) {
  365. if (xml.name() == "a") {
  366. QString anchor = xml.attributes().value("href").toString();
  367. QString name;
  368. if (xml.readNext()) {
  369. if (xml.tokenString() == "Characters") {
  370. name = xml.text().toString();
  371. } else if (!xml.isEndElement()) {
  372. qWarning() << "TOC HTML <a> should be ended by </a>" << xml.name();
  373. return;
  374. }
  375. VHeader header(level, name, anchor, -1);
  376. headers.append(header);
  377. } else {
  378. // Error
  379. return;
  380. }
  381. } else if (xml.name() == "ul") {
  382. // Such as header 3 under header 1 directly
  383. VHeader header(level, "[EMPTY]", "#", -1);
  384. headers.append(header);
  385. parseTocUl(xml, headers, level + 1);
  386. } else {
  387. qWarning() << "TOC HTML <li> should contain <a> or <ul>" << xml.name();
  388. return;
  389. }
  390. }
  391. while (xml.readNext()) {
  392. if (xml.isEndElement()) {
  393. if (xml.name() == "li") {
  394. return;
  395. }
  396. continue;
  397. }
  398. if (xml.name() == "ul") {
  399. // Nested unordered list
  400. parseTocUl(xml, headers, level + 1);
  401. } else {
  402. return;
  403. }
  404. }
  405. }
  406. void VEditTab::requestUpdateCurHeader()
  407. {
  408. emit curHeaderChanged(curHeader);
  409. }
  410. void VEditTab::requestUpdateOutline()
  411. {
  412. checkToc();
  413. emit outlineChanged(tableOfContent);
  414. }
  415. void VEditTab::scrollToAnchor(const VAnchor &anchor)
  416. {
  417. if (anchor == curHeader) {
  418. return;
  419. }
  420. curHeader = anchor;
  421. if (isEditMode) {
  422. if (anchor.lineNumber > -1) {
  423. m_textEditor->scrollToLine(anchor.lineNumber);
  424. }
  425. } else {
  426. if (!anchor.anchor.isEmpty()) {
  427. document.scrollToAnchor(anchor.anchor.mid(1));
  428. }
  429. }
  430. }
  431. void VEditTab::updateCurHeader(const QString &anchor)
  432. {
  433. if (isEditMode || curHeader.anchor.mid(1) == anchor) {
  434. return;
  435. }
  436. curHeader = VAnchor(m_file->retrivePath(), "#" + anchor, -1);
  437. if (!anchor.isEmpty()) {
  438. if (checkToc()) {
  439. emit outlineChanged(tableOfContent);
  440. }
  441. const QVector<VHeader> &headers = tableOfContent.headers;
  442. for (int i = 0; i < headers.size(); ++i) {
  443. if (headers[i].anchor == curHeader.anchor) {
  444. curHeader.m_outlineIndex = i;
  445. break;
  446. }
  447. }
  448. emit curHeaderChanged(curHeader);
  449. }
  450. }
  451. void VEditTab::updateCurHeader(int p_lineNumber, int p_outlineIndex)
  452. {
  453. if (!isEditMode || curHeader.lineNumber == p_lineNumber) {
  454. return;
  455. }
  456. if (checkToc()) {
  457. emit outlineChanged(tableOfContent);
  458. }
  459. curHeader = VAnchor(m_file->retrivePath(), "", p_lineNumber);
  460. curHeader.m_outlineIndex = p_outlineIndex;
  461. if (p_lineNumber > -1) {
  462. emit curHeaderChanged(curHeader);
  463. }
  464. }
  465. void VEditTab::insertImage()
  466. {
  467. qDebug() << "insert image";
  468. if (!isEditMode) {
  469. return;
  470. }
  471. m_textEditor->insertImage();
  472. }
  473. void VEditTab::findText(const QString &p_text, uint p_options, bool p_peek,
  474. bool p_forward)
  475. {
  476. if (isEditMode || !webPreviewer) {
  477. if (p_peek) {
  478. m_textEditor->peekText(p_text, p_options);
  479. } else {
  480. m_textEditor->findText(p_text, p_options, p_forward);
  481. }
  482. } else {
  483. findTextInWebView(p_text, p_options, p_peek, p_forward);
  484. }
  485. }
  486. void VEditTab::replaceText(const QString &p_text, uint p_options,
  487. const QString &p_replaceText, bool p_findNext)
  488. {
  489. if (isEditMode) {
  490. m_textEditor->replaceText(p_text, p_options, p_replaceText, p_findNext);
  491. }
  492. }
  493. void VEditTab::replaceTextAll(const QString &p_text, uint p_options,
  494. const QString &p_replaceText)
  495. {
  496. if (isEditMode) {
  497. m_textEditor->replaceTextAll(p_text, p_options, p_replaceText);
  498. }
  499. }
  500. void VEditTab::findTextInWebView(const QString &p_text, uint p_options,
  501. bool /* p_peek */, bool p_forward)
  502. {
  503. Q_ASSERT(webPreviewer);
  504. QWebEnginePage::FindFlags flags;
  505. if (p_options & FindOption::CaseSensitive) {
  506. flags |= QWebEnginePage::FindCaseSensitively;
  507. }
  508. if (!p_forward) {
  509. flags |= QWebEnginePage::FindBackward;
  510. }
  511. webPreviewer->findText(p_text, flags);
  512. }
  513. QString VEditTab::getSelectedText() const
  514. {
  515. if (isEditMode || !webPreviewer) {
  516. QTextCursor cursor = m_textEditor->textCursor();
  517. return cursor.selectedText();
  518. } else {
  519. return webPreviewer->selectedText();
  520. }
  521. }
  522. void VEditTab::clearSearchedWordHighlight()
  523. {
  524. if (webPreviewer) {
  525. webPreviewer->findText("");
  526. }
  527. m_textEditor->clearSearchedWordHighlight();
  528. }
  529. bool VEditTab::checkToc()
  530. {
  531. bool ret = false;
  532. if (tableOfContent.filePath != m_file->retrivePath()) {
  533. tableOfContent.filePath = m_file->retrivePath();
  534. ret = true;
  535. }
  536. return ret;
  537. }
  538. void VEditTab::handleWebKeyPressed(int p_key, bool p_ctrl, bool /* p_shift */)
  539. {
  540. Q_ASSERT(webPreviewer);
  541. switch (p_key) {
  542. // Esc
  543. case 27:
  544. m_editArea->getFindReplaceDialog()->closeDialog();
  545. break;
  546. // Dash
  547. case 189:
  548. if (p_ctrl) {
  549. // Zoom out.
  550. zoomWebPage(false);
  551. }
  552. break;
  553. // Equal
  554. case 187:
  555. if (p_ctrl) {
  556. // Zoom in.
  557. zoomWebPage(true);
  558. }
  559. break;
  560. // 0
  561. case 48:
  562. if (p_ctrl) {
  563. // Recover zoom.
  564. webPreviewer->setZoomFactor(1);
  565. }
  566. break;
  567. default:
  568. break;
  569. }
  570. }
  571. void VEditTab::wheelEvent(QWheelEvent *p_event)
  572. {
  573. if (!isEditMode && webPreviewer) {
  574. QPoint angle = p_event->angleDelta();
  575. Qt::KeyboardModifiers modifiers = p_event->modifiers();
  576. if (!angle.isNull() && (angle.y() != 0) && (modifiers & Qt::ControlModifier)) {
  577. zoomWebPage(angle.y() > 0);
  578. p_event->accept();
  579. return;
  580. }
  581. }
  582. p_event->ignore();
  583. }
  584. void VEditTab::zoomWebPage(bool p_zoomIn, qreal p_step)
  585. {
  586. Q_ASSERT(webPreviewer);
  587. qreal curFactor = webPreviewer->zoomFactor();
  588. qreal newFactor = p_zoomIn ? curFactor + p_step : curFactor - p_step;
  589. if (newFactor < c_webZoomFactorMin) {
  590. newFactor = c_webZoomFactorMin;
  591. } else if (newFactor > c_webZoomFactorMax) {
  592. newFactor = c_webZoomFactorMax;
  593. }
  594. webPreviewer->setZoomFactor(newFactor);
  595. }