vmdtab.cpp 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685
  1. #include <QtWidgets>
  2. #include <QWebChannel>
  3. #include <QFileInfo>
  4. #include <QXmlStreamReader>
  5. #include "vmdtab.h"
  6. #include "vdocument.h"
  7. #include "vnote.h"
  8. #include "utils/vutils.h"
  9. #include "vpreviewpage.h"
  10. #include "hgmarkdownhighlighter.h"
  11. #include "vconfigmanager.h"
  12. #include "vmarkdownconverter.h"
  13. #include "vnotebook.h"
  14. #include "vtableofcontent.h"
  15. #include "vmdedit.h"
  16. #include "dialog/vfindreplacedialog.h"
  17. #include "veditarea.h"
  18. #include "vconstants.h"
  19. #include "vwebview.h"
  20. extern VConfigManager *g_config;
  21. VMdTab::VMdTab(VFile *p_file, VEditArea *p_editArea,
  22. OpenFileMode p_mode, QWidget *p_parent)
  23. : VEditTab(p_file, p_editArea, p_parent),
  24. m_editor(NULL),
  25. m_webViewer(NULL),
  26. m_document(NULL),
  27. m_mdConType(g_config->getMdConverterType()),
  28. m_enableHeadingSequence(false)
  29. {
  30. V_ASSERT(m_file->getDocType() == DocType::Markdown);
  31. m_file->open();
  32. HeadingSequenceType headingSequenceType = g_config->getHeadingSequenceType();
  33. if (headingSequenceType == HeadingSequenceType::Enabled) {
  34. m_enableHeadingSequence = true;
  35. } else if (headingSequenceType == HeadingSequenceType::EnabledNoteOnly
  36. && m_file->getType() == FileType::Note) {
  37. m_enableHeadingSequence = true;
  38. }
  39. setupUI();
  40. if (p_mode == OpenFileMode::Edit) {
  41. showFileEditMode();
  42. } else {
  43. showFileReadMode();
  44. }
  45. }
  46. void VMdTab::setupUI()
  47. {
  48. m_stacks = new QStackedLayout(this);
  49. setupMarkdownViewer();
  50. // Setup editor when we really need it.
  51. m_editor = NULL;
  52. setLayout(m_stacks);
  53. }
  54. void VMdTab::showFileReadMode()
  55. {
  56. m_isEditMode = false;
  57. VHeaderPointer header(m_currentHeader);
  58. if (m_mdConType == MarkdownConverterType::Hoedown) {
  59. viewWebByConverter();
  60. } else {
  61. m_document->updateText();
  62. updateOutlineFromHtml(m_document->getToc());
  63. }
  64. m_stacks->setCurrentWidget(m_webViewer);
  65. clearSearchedWordHighlight();
  66. scrollWebViewToHeader(header);
  67. updateStatus();
  68. }
  69. bool VMdTab::scrollWebViewToHeader(const VHeaderPointer &p_header)
  70. {
  71. if (!m_outline.isMatched(p_header)
  72. || m_outline.getType() != VTableOfContentType::Anchor) {
  73. return false;
  74. }
  75. if (p_header.isValid()) {
  76. const VTableOfContentItem *item = m_outline.getItem(p_header);
  77. if (item) {
  78. if (item->m_anchor.isEmpty()) {
  79. return false;
  80. }
  81. m_currentHeader = p_header;
  82. m_document->scrollToAnchor(item->m_anchor);
  83. } else {
  84. return false;
  85. }
  86. } else {
  87. if (m_outline.isEmpty()) {
  88. // Let it be.
  89. m_currentHeader = p_header;
  90. } else {
  91. // Scroll to top.
  92. m_currentHeader = p_header;
  93. m_document->scrollToAnchor("");
  94. }
  95. }
  96. emit currentHeaderChanged(m_currentHeader);
  97. return true;
  98. }
  99. bool VMdTab::scrollEditorToHeader(const VHeaderPointer &p_header)
  100. {
  101. if (!m_outline.isMatched(p_header)
  102. || m_outline.getType() != VTableOfContentType::BlockNumber) {
  103. return false;
  104. }
  105. VMdEdit *mdEdit = dynamic_cast<VMdEdit *>(getEditor());
  106. int blockNumber = -1;
  107. if (p_header.isValid()) {
  108. const VTableOfContentItem *item = m_outline.getItem(p_header);
  109. if (item) {
  110. blockNumber = item->m_blockNumber;
  111. if (blockNumber == -1) {
  112. // Empty item.
  113. return false;
  114. }
  115. } else {
  116. return false;
  117. }
  118. } else {
  119. if (m_outline.isEmpty()) {
  120. // No outline and scroll to -1 index.
  121. // Just let it be.
  122. m_currentHeader = p_header;
  123. return true;
  124. } else {
  125. // Has outline and scroll to -1 index.
  126. // Scroll to top.
  127. blockNumber = 0;
  128. }
  129. }
  130. if (mdEdit->scrollToHeader(blockNumber)) {
  131. m_currentHeader = p_header;
  132. return true;
  133. } else {
  134. return false;
  135. }
  136. }
  137. bool VMdTab::scrollToHeaderInternal(const VHeaderPointer &p_header)
  138. {
  139. if (m_isEditMode) {
  140. return scrollEditorToHeader(p_header);
  141. } else {
  142. return scrollWebViewToHeader(p_header);
  143. }
  144. }
  145. void VMdTab::viewWebByConverter()
  146. {
  147. VMarkdownConverter mdConverter;
  148. QString toc;
  149. QString html = mdConverter.generateHtml(m_file->getContent(),
  150. g_config->getMarkdownExtensions(),
  151. toc);
  152. m_document->setHtml(html);
  153. updateOutlineFromHtml(toc);
  154. }
  155. void VMdTab::showFileEditMode()
  156. {
  157. if (!m_file->isModifiable()) {
  158. return;
  159. }
  160. VHeaderPointer header(m_currentHeader);
  161. m_isEditMode = true;
  162. VMdEdit *mdEdit = dynamic_cast<VMdEdit *>(getEditor());
  163. V_ASSERT(mdEdit);
  164. mdEdit->beginEdit();
  165. m_stacks->setCurrentWidget(mdEdit);
  166. // If editor is not init, we need to wait for it to init headers.
  167. // Generally, beginEdit() will generate the headers. Wait is needed when
  168. // highlight completion is going to re-generate the headers.
  169. int nrRetry = 5;
  170. while (header.m_index > -1 && m_outline.isEmpty() && nrRetry-- > 0) {
  171. qDebug() << "wait another 100 ms for editor's headers ready";
  172. VUtils::sleepWait(100);
  173. }
  174. scrollEditorToHeader(header);
  175. mdEdit->setFocus();
  176. }
  177. bool VMdTab::closeFile(bool p_forced)
  178. {
  179. if (p_forced && m_isEditMode) {
  180. // Discard buffer content
  181. Q_ASSERT(m_editor);
  182. m_editor->reloadFile();
  183. m_editor->endEdit();
  184. showFileReadMode();
  185. } else {
  186. readFile();
  187. }
  188. return !m_isEditMode;
  189. }
  190. void VMdTab::editFile()
  191. {
  192. if (m_isEditMode || !m_file->isModifiable()) {
  193. return;
  194. }
  195. showFileEditMode();
  196. }
  197. void VMdTab::readFile()
  198. {
  199. if (!m_isEditMode) {
  200. return;
  201. }
  202. if (m_editor && m_editor->isModified()) {
  203. // Prompt to save the changes.
  204. int ret = VUtils::showMessage(QMessageBox::Information, tr("Information"),
  205. tr("Note <span style=\"%1\">%2</span> has been modified.")
  206. .arg(g_config->c_dataTextStyle).arg(m_file->getName()),
  207. tr("Do you want to save your changes?"),
  208. QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel,
  209. QMessageBox::Save, this);
  210. switch (ret) {
  211. case QMessageBox::Save:
  212. saveFile();
  213. // Fall through
  214. case QMessageBox::Discard:
  215. m_editor->reloadFile();
  216. break;
  217. case QMessageBox::Cancel:
  218. // Nothing to do if user cancel this action
  219. return;
  220. default:
  221. qWarning() << "wrong return value from QMessageBox:" << ret;
  222. return;
  223. }
  224. }
  225. if (m_editor) {
  226. m_editor->endEdit();
  227. }
  228. showFileReadMode();
  229. }
  230. bool VMdTab::saveFile()
  231. {
  232. if (!m_isEditMode) {
  233. return true;
  234. }
  235. Q_ASSERT(m_editor);
  236. if (!m_editor->isModified()) {
  237. return true;
  238. }
  239. bool ret;
  240. // Make sure the file already exists. Temporary deal with cases when user delete or move
  241. // a file.
  242. QString filePath = m_file->fetchPath();
  243. if (!QFileInfo::exists(filePath)) {
  244. qWarning() << filePath << "being written has been removed";
  245. VUtils::showMessage(QMessageBox::Warning, tr("Warning"), tr("Fail to save note."),
  246. tr("File <span style=\"%1\">%2</span> being written has been removed.")
  247. .arg(g_config->c_dataTextStyle).arg(filePath),
  248. QMessageBox::Ok, QMessageBox::Ok, this);
  249. return false;
  250. }
  251. m_editor->saveFile();
  252. ret = m_file->save();
  253. if (!ret) {
  254. VUtils::showMessage(QMessageBox::Warning, tr("Warning"), tr("Fail to save note."),
  255. tr("Fail to write to disk when saving a note. Please try it again."),
  256. QMessageBox::Ok, QMessageBox::Ok, this);
  257. m_editor->setModified(true);
  258. }
  259. return ret;
  260. }
  261. void VMdTab::saveAndRead()
  262. {
  263. saveFile();
  264. readFile();
  265. }
  266. void VMdTab::discardAndRead()
  267. {
  268. readFile();
  269. }
  270. void VMdTab::setupMarkdownViewer()
  271. {
  272. m_webViewer = new VWebView(m_file, this);
  273. connect(m_webViewer, &VWebView::editNote,
  274. this, &VMdTab::editFile);
  275. VPreviewPage *page = new VPreviewPage(m_webViewer);
  276. m_webViewer->setPage(page);
  277. m_webViewer->setZoomFactor(g_config->getWebZoomFactor());
  278. m_document = new VDocument(m_file, m_webViewer);
  279. QWebChannel *channel = new QWebChannel(m_webViewer);
  280. channel->registerObject(QStringLiteral("content"), m_document);
  281. connect(m_document, &VDocument::tocChanged,
  282. this, &VMdTab::updateOutlineFromHtml);
  283. connect(m_document, SIGNAL(headerChanged(const QString &)),
  284. this, SLOT(updateCurrentHeader(const QString &)));
  285. connect(m_document, &VDocument::keyPressed,
  286. this, &VMdTab::handleWebKeyPressed);
  287. connect(m_document, SIGNAL(logicsFinished(void)),
  288. this, SLOT(restoreFromTabInfo(void)));
  289. page->setWebChannel(channel);
  290. m_webViewer->setHtml(VUtils::generateHtmlTemplate(m_mdConType, false),
  291. m_file->getBaseUrl());
  292. m_stacks->addWidget(m_webViewer);
  293. }
  294. void VMdTab::setupMarkdownEditor()
  295. {
  296. Q_ASSERT(m_file->isModifiable() && !m_editor);
  297. qDebug() << "create Markdown editor";
  298. m_editor = new VMdEdit(m_file, m_document, m_mdConType, this);
  299. connect(dynamic_cast<VMdEdit *>(m_editor), &VMdEdit::headersChanged,
  300. this, &VMdTab::updateOutlineFromHeaders);
  301. connect(dynamic_cast<VMdEdit *>(m_editor), SIGNAL(currentHeaderChanged(int)),
  302. this, SLOT(updateCurrentHeader(int)));
  303. connect(dynamic_cast<VMdEdit *>(m_editor), &VMdEdit::statusChanged,
  304. this, &VMdTab::updateStatus);
  305. connect(m_editor, &VEdit::textChanged,
  306. this, &VMdTab::updateStatus);
  307. connect(m_editor, &VEdit::cursorPositionChanged,
  308. this, &VMdTab::updateStatus);
  309. connect(m_editor, &VEdit::saveAndRead,
  310. this, &VMdTab::saveAndRead);
  311. connect(m_editor, &VEdit::discardAndRead,
  312. this, &VMdTab::discardAndRead);
  313. connect(m_editor, &VEdit::saveNote,
  314. this, &VMdTab::saveFile);
  315. connect(m_editor, &VEdit::statusMessage,
  316. this, &VEditTab::statusMessage);
  317. connect(m_editor, &VEdit::vimStatusUpdated,
  318. this, &VEditTab::vimStatusUpdated);
  319. connect(m_editor, &VEdit::requestCloseFindReplaceDialog,
  320. this, [this]() {
  321. this->m_editArea->getFindReplaceDialog()->closeDialog();
  322. });
  323. connect(m_editor, SIGNAL(ready(void)),
  324. this, SLOT(restoreFromTabInfo(void)));
  325. enableHeadingSequence(m_enableHeadingSequence);
  326. m_editor->reloadFile();
  327. m_stacks->addWidget(m_editor);
  328. }
  329. void VMdTab::updateOutlineFromHtml(const QString &p_tocHtml)
  330. {
  331. if (m_isEditMode) {
  332. return;
  333. }
  334. m_outline.clear();
  335. if (m_outline.parseTableFromHtml(p_tocHtml)) {
  336. m_outline.setFile(m_file);
  337. m_outline.setType(VTableOfContentType::Anchor);
  338. }
  339. m_currentHeader.reset();
  340. emit outlineChanged(m_outline);
  341. }
  342. void VMdTab::updateOutlineFromHeaders(const QVector<VTableOfContentItem> &p_headers)
  343. {
  344. if (!m_isEditMode) {
  345. return;
  346. }
  347. m_outline.update(m_file,
  348. p_headers,
  349. VTableOfContentType::BlockNumber);
  350. m_currentHeader.reset();
  351. emit outlineChanged(m_outline);
  352. }
  353. void VMdTab::scrollToHeader(const VHeaderPointer &p_header)
  354. {
  355. if (m_outline.isMatched(p_header)) {
  356. // Scroll only when @p_header is valid.
  357. scrollToHeaderInternal(p_header);
  358. }
  359. }
  360. void VMdTab::updateCurrentHeader(const QString &p_anchor)
  361. {
  362. if (m_isEditMode) {
  363. return;
  364. }
  365. // Find the index of the anchor in outline.
  366. int idx = m_outline.indexOfItemByAnchor(p_anchor);
  367. m_currentHeader.update(m_file, idx);
  368. emit currentHeaderChanged(m_currentHeader);
  369. }
  370. void VMdTab::updateCurrentHeader(int p_blockNumber)
  371. {
  372. if (!m_isEditMode) {
  373. return;
  374. }
  375. // Find the index of the block number in outline.
  376. int idx = m_outline.indexOfItemByBlockNumber(p_blockNumber);
  377. m_currentHeader.update(m_file, idx);
  378. emit currentHeaderChanged(m_currentHeader);
  379. }
  380. void VMdTab::insertImage()
  381. {
  382. if (!m_isEditMode) {
  383. return;
  384. }
  385. Q_ASSERT(m_editor);
  386. m_editor->insertImage();
  387. }
  388. void VMdTab::findText(const QString &p_text, uint p_options, bool p_peek,
  389. bool p_forward)
  390. {
  391. if (m_isEditMode) {
  392. Q_ASSERT(m_editor);
  393. if (p_peek) {
  394. m_editor->peekText(p_text, p_options);
  395. } else {
  396. m_editor->findText(p_text, p_options, p_forward);
  397. }
  398. } else {
  399. findTextInWebView(p_text, p_options, p_peek, p_forward);
  400. }
  401. }
  402. void VMdTab::replaceText(const QString &p_text, uint p_options,
  403. const QString &p_replaceText, bool p_findNext)
  404. {
  405. if (m_isEditMode) {
  406. Q_ASSERT(m_editor);
  407. m_editor->replaceText(p_text, p_options, p_replaceText, p_findNext);
  408. }
  409. }
  410. void VMdTab::replaceTextAll(const QString &p_text, uint p_options,
  411. const QString &p_replaceText)
  412. {
  413. if (m_isEditMode) {
  414. Q_ASSERT(m_editor);
  415. m_editor->replaceTextAll(p_text, p_options, p_replaceText);
  416. }
  417. }
  418. void VMdTab::findTextInWebView(const QString &p_text, uint p_options,
  419. bool /* p_peek */, bool p_forward)
  420. {
  421. V_ASSERT(m_webViewer);
  422. QWebEnginePage::FindFlags flags;
  423. if (p_options & FindOption::CaseSensitive) {
  424. flags |= QWebEnginePage::FindCaseSensitively;
  425. }
  426. if (!p_forward) {
  427. flags |= QWebEnginePage::FindBackward;
  428. }
  429. m_webViewer->findText(p_text, flags);
  430. }
  431. QString VMdTab::getSelectedText() const
  432. {
  433. if (m_isEditMode) {
  434. Q_ASSERT(m_editor);
  435. QTextCursor cursor = m_editor->textCursor();
  436. return cursor.selectedText();
  437. } else {
  438. return m_webViewer->selectedText();
  439. }
  440. }
  441. void VMdTab::clearSearchedWordHighlight()
  442. {
  443. if (m_webViewer) {
  444. m_webViewer->findText("");
  445. }
  446. if (m_editor) {
  447. m_editor->clearSearchedWordHighlight();
  448. }
  449. }
  450. void VMdTab::handleWebKeyPressed(int p_key, bool p_ctrl, bool /* p_shift */)
  451. {
  452. V_ASSERT(m_webViewer);
  453. switch (p_key) {
  454. // Esc
  455. case 27:
  456. m_editArea->getFindReplaceDialog()->closeDialog();
  457. break;
  458. // Dash
  459. case 189:
  460. if (p_ctrl) {
  461. // Zoom out.
  462. zoomWebPage(false);
  463. }
  464. break;
  465. // Equal
  466. case 187:
  467. if (p_ctrl) {
  468. // Zoom in.
  469. zoomWebPage(true);
  470. }
  471. break;
  472. // 0
  473. case 48:
  474. if (p_ctrl) {
  475. // Recover zoom.
  476. m_webViewer->setZoomFactor(1);
  477. }
  478. break;
  479. default:
  480. break;
  481. }
  482. }
  483. void VMdTab::zoom(bool p_zoomIn, qreal p_step)
  484. {
  485. if (m_isEditMode) {
  486. // TODO
  487. } else {
  488. zoomWebPage(p_zoomIn, p_step);
  489. }
  490. }
  491. void VMdTab::zoomWebPage(bool p_zoomIn, qreal p_step)
  492. {
  493. V_ASSERT(m_webViewer);
  494. qreal curFactor = m_webViewer->zoomFactor();
  495. qreal newFactor = p_zoomIn ? curFactor + p_step : curFactor - p_step;
  496. if (newFactor < c_webZoomFactorMin) {
  497. newFactor = c_webZoomFactorMin;
  498. } else if (newFactor > c_webZoomFactorMax) {
  499. newFactor = c_webZoomFactorMax;
  500. }
  501. m_webViewer->setZoomFactor(newFactor);
  502. }
  503. VWebView *VMdTab::getWebViewer() const
  504. {
  505. return m_webViewer;
  506. }
  507. MarkdownConverterType VMdTab::getMarkdownConverterType() const
  508. {
  509. return m_mdConType;
  510. }
  511. void VMdTab::focusChild()
  512. {
  513. m_stacks->currentWidget()->setFocus();
  514. }
  515. void VMdTab::requestUpdateVimStatus()
  516. {
  517. if (m_editor) {
  518. m_editor->requestUpdateVimStatus();
  519. } else {
  520. emit vimStatusUpdated(NULL);
  521. }
  522. }
  523. VEditTabInfo VMdTab::fetchTabInfo() const
  524. {
  525. VEditTabInfo info = VEditTab::fetchTabInfo();
  526. if (m_editor) {
  527. QTextCursor cursor = m_editor->textCursor();
  528. info.m_cursorBlockNumber = cursor.block().blockNumber();
  529. info.m_cursorPositionInBlock = cursor.positionInBlock();
  530. info.m_blockCount = m_editor->document()->blockCount();
  531. }
  532. info.m_headerIndex = m_currentHeader.m_index;
  533. return info;
  534. }
  535. void VMdTab::decorateText(TextDecoration p_decoration)
  536. {
  537. if (m_editor) {
  538. m_editor->decorateText(p_decoration);
  539. }
  540. }
  541. bool VMdTab::restoreFromTabInfo(const VEditTabInfo &p_info)
  542. {
  543. if (p_info.m_editTab != this) {
  544. return false;
  545. }
  546. // Restore header.
  547. VHeaderPointer header(m_file, p_info.m_headerIndex);
  548. bool ret = scrollToHeaderInternal(header);
  549. return ret;
  550. }
  551. void VMdTab::restoreFromTabInfo()
  552. {
  553. restoreFromTabInfo(m_infoToRestore);
  554. // Clear it anyway.
  555. m_infoToRestore.clear();
  556. }
  557. void VMdTab::enableHeadingSequence(bool p_enabled)
  558. {
  559. m_enableHeadingSequence = p_enabled;
  560. if (m_editor) {
  561. VEditConfig &config = m_editor->getConfig();
  562. config.m_enableHeadingSequence = m_enableHeadingSequence;
  563. }
  564. }
  565. bool VMdTab::isHeadingSequenceEnabled() const
  566. {
  567. return m_enableHeadingSequence;
  568. }