vedittab.cpp 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517
  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. extern VConfigManager vconfig;
  20. VEditTab::VEditTab(VFile *p_file, OpenFileMode p_mode, QWidget *p_parent)
  21. : QStackedWidget(p_parent), m_file(p_file), isEditMode(false),
  22. mdConverterType(vconfig.getMdConverterType()), m_fileModified(false)
  23. {
  24. tableOfContent.filePath = p_file->retrivePath();
  25. curHeader.filePath = p_file->retrivePath();
  26. Q_ASSERT(!m_file->isOpened());
  27. m_file->open();
  28. setupUI();
  29. if (p_mode == OpenFileMode::Edit) {
  30. showFileEditMode();
  31. } else {
  32. showFileReadMode();
  33. }
  34. connect(qApp, &QApplication::focusChanged,
  35. this, &VEditTab::handleFocusChanged);
  36. }
  37. VEditTab::~VEditTab()
  38. {
  39. if (m_file) {
  40. m_file->close();
  41. }
  42. }
  43. void VEditTab::setupUI()
  44. {
  45. switch (m_file->getDocType()) {
  46. case DocType::Markdown:
  47. m_textEditor = new VMdEdit(m_file, this);
  48. connect(dynamic_cast<VMdEdit *>(m_textEditor), &VMdEdit::headersChanged,
  49. this, &VEditTab::updateTocFromHeaders);
  50. connect(dynamic_cast<VMdEdit *>(m_textEditor), &VMdEdit::statusChanged,
  51. this, &VEditTab::noticeStatusChanged);
  52. connect(m_textEditor, SIGNAL(curHeaderChanged(int, int)),
  53. this, SLOT(updateCurHeader(int, int)));
  54. connect(m_textEditor, &VEdit::textChanged,
  55. this, &VEditTab::handleTextChanged);
  56. m_textEditor->reloadFile();
  57. addWidget(m_textEditor);
  58. setupMarkdownPreview();
  59. break;
  60. case DocType::Html:
  61. m_textEditor = new VEdit(m_file, this);
  62. connect(m_textEditor, &VEdit::textChanged,
  63. this, &VEditTab::handleTextChanged);
  64. m_textEditor->reloadFile();
  65. addWidget(m_textEditor);
  66. webPreviewer = NULL;
  67. break;
  68. default:
  69. qWarning() << "error: unknown doc type" << int(m_file->getDocType());
  70. Q_ASSERT(false);
  71. }
  72. }
  73. void VEditTab::handleTextChanged()
  74. {
  75. if (m_fileModified) {
  76. return;
  77. }
  78. noticeStatusChanged();
  79. }
  80. void VEditTab::noticeStatusChanged()
  81. {
  82. m_fileModified = m_file->isModified();
  83. emit statusChanged();
  84. }
  85. void VEditTab::showFileReadMode()
  86. {
  87. qDebug() << "read" << m_file->getName();
  88. isEditMode = false;
  89. int outlineIndex = curHeader.m_outlineIndex;
  90. switch (m_file->getDocType()) {
  91. case DocType::Html:
  92. m_textEditor->setReadOnly(true);
  93. break;
  94. case DocType::Markdown:
  95. if (mdConverterType == MarkdownConverterType::Marked) {
  96. document.setText(m_file->getContent());
  97. updateTocFromHtml(document.getToc());
  98. } else {
  99. previewByConverter();
  100. }
  101. setCurrentWidget(webPreviewer);
  102. clearFindSelectionInWebView();
  103. scrollPreviewToHeader(outlineIndex);
  104. break;
  105. default:
  106. qWarning() << "error: unknown doc type" << int(m_file->getDocType());
  107. Q_ASSERT(false);
  108. }
  109. noticeStatusChanged();
  110. }
  111. void VEditTab::scrollPreviewToHeader(int p_outlineIndex)
  112. {
  113. Q_ASSERT(p_outlineIndex >= 0);
  114. if (p_outlineIndex < tableOfContent.headers.size()) {
  115. QString anchor = tableOfContent.headers[p_outlineIndex].anchor;
  116. qDebug() << "scroll preview to" << p_outlineIndex << anchor;
  117. if (!anchor.isEmpty()) {
  118. document.scrollToAnchor(anchor.mid(1));
  119. }
  120. }
  121. }
  122. void VEditTab::previewByConverter()
  123. {
  124. VMarkdownConverter mdConverter;
  125. QString &content = m_file->getContent();
  126. QString html = mdConverter.generateHtml(content, vconfig.getMarkdownExtensions());
  127. QRegularExpression tocExp("<p>\\[TOC\\]<\\/p>", QRegularExpression::CaseInsensitiveOption);
  128. QString toc = mdConverter.generateToc(content, vconfig.getMarkdownExtensions());
  129. processHoedownToc(toc);
  130. html.replace(tocExp, toc);
  131. document.setHtml(html);
  132. // Hoedown will add '\n' while Marked does not
  133. updateTocFromHtml(toc);
  134. }
  135. void VEditTab::processHoedownToc(QString &p_toc)
  136. {
  137. // Hoedown will add '\n'.
  138. p_toc.replace("\n", "");
  139. // Hoedown will translate `_` in title to `<em>`.
  140. p_toc.replace("<em>", "_");
  141. p_toc.replace("</em>", "_");
  142. }
  143. void VEditTab::showFileEditMode()
  144. {
  145. isEditMode = true;
  146. // beginEdit() may change curHeader.
  147. int outlineIndex = curHeader.m_outlineIndex;
  148. m_textEditor->beginEdit();
  149. setCurrentWidget(m_textEditor);
  150. if (m_file->getDocType() == DocType::Markdown) {
  151. dynamic_cast<VMdEdit *>(m_textEditor)->scrollToHeader(outlineIndex);
  152. }
  153. m_textEditor->setFocus();
  154. noticeStatusChanged();
  155. }
  156. bool VEditTab::closeFile(bool p_forced)
  157. {
  158. if (p_forced && isEditMode) {
  159. // Discard buffer content
  160. m_textEditor->reloadFile();
  161. m_textEditor->endEdit();
  162. showFileReadMode();
  163. } else {
  164. readFile();
  165. }
  166. return !isEditMode;
  167. }
  168. void VEditTab::editFile()
  169. {
  170. if (isEditMode) {
  171. return;
  172. }
  173. showFileEditMode();
  174. }
  175. void VEditTab::readFile()
  176. {
  177. if (!isEditMode) {
  178. return;
  179. }
  180. if (m_textEditor->isModified()) {
  181. // Prompt to save the changes
  182. int ret = VUtils::showMessage(QMessageBox::Information, tr("Information"),
  183. QString("Note %1 has been modified.").arg(m_file->getName()),
  184. tr("Do you want to save your changes?"),
  185. QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel,
  186. QMessageBox::Save, this);
  187. switch (ret) {
  188. case QMessageBox::Save:
  189. saveFile();
  190. // Fall through
  191. case QMessageBox::Discard:
  192. m_textEditor->reloadFile();
  193. break;
  194. case QMessageBox::Cancel:
  195. // Nothing to do if user cancel this action
  196. return;
  197. default:
  198. qWarning() << "error: wrong return value from QMessageBox:" << ret;
  199. return;
  200. }
  201. }
  202. m_textEditor->endEdit();
  203. showFileReadMode();
  204. }
  205. bool VEditTab::saveFile()
  206. {
  207. bool ret;
  208. if (!isEditMode || !m_textEditor->isModified()) {
  209. return true;
  210. }
  211. // Make sure the file already exists. Temporary deal with cases when user delete or move
  212. // a file.
  213. QString filePath = m_file->retrivePath();
  214. if (!QFile(filePath).exists()) {
  215. qWarning() << filePath << "being written has been removed";
  216. VUtils::showMessage(QMessageBox::Warning, tr("Warning"), tr("Fail to save note"),
  217. QString("%1 being written has been removed.").arg(filePath),
  218. QMessageBox::Ok, QMessageBox::Ok, this);
  219. return false;
  220. }
  221. m_textEditor->saveFile();
  222. ret = m_file->save();
  223. if (!ret) {
  224. VUtils::showMessage(QMessageBox::Warning, tr("Warning"), tr("Fail to save note"),
  225. QString("Fail to write to disk when saving a note. Please try it again."),
  226. QMessageBox::Ok, QMessageBox::Ok, this);
  227. m_textEditor->setModified(true);
  228. }
  229. noticeStatusChanged();
  230. return ret;
  231. }
  232. void VEditTab::setupMarkdownPreview()
  233. {
  234. webPreviewer = new QWebEngineView(this);
  235. VPreviewPage *page = new VPreviewPage(this);
  236. webPreviewer->setPage(page);
  237. QWebChannel *channel = new QWebChannel(this);
  238. channel->registerObject(QStringLiteral("content"), &document);
  239. connect(&document, &VDocument::tocChanged,
  240. this, &VEditTab::updateTocFromHtml);
  241. connect(&document, SIGNAL(headerChanged(const QString&)),
  242. this, SLOT(updateCurHeader(const QString &)));
  243. page->setWebChannel(channel);
  244. if (mdConverterType == MarkdownConverterType::Marked) {
  245. webPreviewer->setHtml(VNote::templateHtml,
  246. QUrl::fromLocalFile(m_file->retriveBasePath() + QDir::separator()));
  247. } else {
  248. webPreviewer->setHtml(VNote::preTemplateHtml + VNote::postTemplateHtml,
  249. QUrl::fromLocalFile(m_file->retriveBasePath() + QDir::separator()));
  250. }
  251. addWidget(webPreviewer);
  252. }
  253. void VEditTab::focusTab()
  254. {
  255. currentWidget()->setFocus();
  256. emit getFocused();
  257. }
  258. void VEditTab::handleFocusChanged(QWidget * /* old */, QWidget *now)
  259. {
  260. if (isChild(now)) {
  261. emit getFocused();
  262. }
  263. }
  264. void VEditTab::updateTocFromHtml(const QString &tocHtml)
  265. {
  266. if (isEditMode) {
  267. return;
  268. }
  269. tableOfContent.type = VHeaderType::Anchor;
  270. QVector<VHeader> &headers = tableOfContent.headers;
  271. headers.clear();
  272. if (!tocHtml.isEmpty()) {
  273. QXmlStreamReader xml(tocHtml);
  274. if (xml.readNextStartElement()) {
  275. if (xml.name() == "ul") {
  276. parseTocUl(xml, headers, 1);
  277. } else {
  278. qWarning() << "error: TOC HTML does not start with <ul>";
  279. }
  280. }
  281. if (xml.hasError()) {
  282. qWarning() << "error: fail to parse TOC in HTML";
  283. return;
  284. }
  285. }
  286. tableOfContent.filePath = m_file->retrivePath();
  287. tableOfContent.valid = true;
  288. emit outlineChanged(tableOfContent);
  289. }
  290. void VEditTab::updateTocFromHeaders(const QVector<VHeader> &headers)
  291. {
  292. if (!isEditMode) {
  293. return;
  294. }
  295. tableOfContent.type = VHeaderType::LineNumber;
  296. tableOfContent.headers = headers;
  297. tableOfContent.filePath = m_file->retrivePath();
  298. tableOfContent.valid = true;
  299. emit outlineChanged(tableOfContent);
  300. }
  301. void VEditTab::parseTocUl(QXmlStreamReader &xml, QVector<VHeader> &headers, int level)
  302. {
  303. Q_ASSERT(xml.isStartElement() && xml.name() == "ul");
  304. while (xml.readNextStartElement()) {
  305. if (xml.name() == "li") {
  306. parseTocLi(xml, headers, level);
  307. } else {
  308. qWarning() << "error: TOC HTML <ul> should contain <li>" << xml.name();
  309. break;
  310. }
  311. }
  312. }
  313. void VEditTab::parseTocLi(QXmlStreamReader &xml, QVector<VHeader> &headers, int level)
  314. {
  315. Q_ASSERT(xml.isStartElement() && xml.name() == "li");
  316. if (xml.readNextStartElement()) {
  317. if (xml.name() == "a") {
  318. QString anchor = xml.attributes().value("href").toString();
  319. QString name;
  320. if (xml.readNext()) {
  321. if (xml.tokenString() == "Characters") {
  322. name = xml.text().toString();
  323. } else if (!xml.isEndElement()) {
  324. qWarning() << "error: TOC HTML <a> should be ended by </a>" << xml.name();
  325. return;
  326. }
  327. VHeader header(level, name, anchor, -1);
  328. headers.append(header);
  329. } else {
  330. // Error
  331. return;
  332. }
  333. } else if (xml.name() == "ul") {
  334. // Such as header 3 under header 1 directly
  335. VHeader header(level, "[Empty]", "#", -1);
  336. headers.append(header);
  337. parseTocUl(xml, headers, level + 1);
  338. } else {
  339. qWarning() << "error: TOC HTML <li> should contain <a> or <ul>" << xml.name();
  340. return;
  341. }
  342. }
  343. while (xml.readNext()) {
  344. if (xml.isEndElement()) {
  345. if (xml.name() == "li") {
  346. return;
  347. }
  348. continue;
  349. }
  350. if (xml.name() == "ul") {
  351. // Nested unordered list
  352. parseTocUl(xml, headers, level + 1);
  353. } else {
  354. return;
  355. }
  356. }
  357. }
  358. void VEditTab::requestUpdateCurHeader()
  359. {
  360. emit curHeaderChanged(curHeader);
  361. }
  362. void VEditTab::requestUpdateOutline()
  363. {
  364. emit outlineChanged(tableOfContent);
  365. }
  366. void VEditTab::scrollToAnchor(const VAnchor &anchor)
  367. {
  368. if (anchor == curHeader) {
  369. return;
  370. }
  371. curHeader = anchor;
  372. if (isEditMode) {
  373. if (anchor.lineNumber > -1) {
  374. m_textEditor->scrollToLine(anchor.lineNumber);
  375. }
  376. } else {
  377. if (!anchor.anchor.isEmpty()) {
  378. document.scrollToAnchor(anchor.anchor.mid(1));
  379. }
  380. }
  381. }
  382. void VEditTab::updateCurHeader(const QString &anchor)
  383. {
  384. if (isEditMode || curHeader.anchor.mid(1) == anchor) {
  385. return;
  386. }
  387. curHeader = VAnchor(m_file->retrivePath(), "#" + anchor, -1);
  388. if (!anchor.isEmpty()) {
  389. const QVector<VHeader> &headers = tableOfContent.headers;
  390. for (int i = 0; i < headers.size(); ++i) {
  391. if (headers[i].anchor == curHeader.anchor) {
  392. curHeader.m_outlineIndex = i;
  393. break;
  394. }
  395. }
  396. emit curHeaderChanged(curHeader);
  397. }
  398. }
  399. void VEditTab::updateCurHeader(int p_lineNumber, int p_outlineIndex)
  400. {
  401. if (!isEditMode || curHeader.lineNumber == p_lineNumber) {
  402. return;
  403. }
  404. curHeader = VAnchor(m_file->retrivePath(), "", p_lineNumber);
  405. curHeader.m_outlineIndex = p_outlineIndex;
  406. if (p_lineNumber > -1) {
  407. emit curHeaderChanged(curHeader);
  408. }
  409. }
  410. void VEditTab::insertImage()
  411. {
  412. qDebug() << "insert image";
  413. if (!isEditMode) {
  414. return;
  415. }
  416. m_textEditor->insertImage();
  417. }
  418. void VEditTab::findText(const QString &p_text, uint p_options, bool p_peek,
  419. bool p_forward)
  420. {
  421. if (isEditMode || !webPreviewer) {
  422. m_textEditor->findText(p_text, p_options, p_peek, p_forward);
  423. } else {
  424. findTextInWebView(p_text, p_options, p_peek, p_forward);
  425. }
  426. }
  427. void VEditTab::replaceText(const QString &p_text, uint p_options,
  428. const QString &p_replaceText, bool p_findNext)
  429. {
  430. if (isEditMode) {
  431. m_textEditor->replaceText(p_text, p_options, p_replaceText, p_findNext);
  432. }
  433. }
  434. void VEditTab::replaceTextAll(const QString &p_text, uint p_options,
  435. const QString &p_replaceText)
  436. {
  437. if (isEditMode) {
  438. m_textEditor->replaceTextAll(p_text, p_options, p_replaceText);
  439. }
  440. }
  441. void VEditTab::findTextInWebView(const QString &p_text, uint p_options,
  442. bool p_peek, bool p_forward)
  443. {
  444. Q_ASSERT(webPreviewer);
  445. QWebEnginePage::FindFlags flags;
  446. if (p_options & FindOption::CaseSensitive) {
  447. flags |= QWebEnginePage::FindCaseSensitively;
  448. }
  449. if (!p_forward) {
  450. flags |= QWebEnginePage::FindBackward;
  451. }
  452. webPreviewer->findText(p_text, flags);
  453. }
  454. QString VEditTab::getSelectedText() const
  455. {
  456. if (isEditMode || !webPreviewer) {
  457. QTextCursor cursor = m_textEditor->textCursor();
  458. return cursor.selectedText();
  459. } else {
  460. return webPreviewer->selectedText();
  461. }
  462. }
  463. void VEditTab::clearFindSelectionInWebView()
  464. {
  465. if (webPreviewer) {
  466. webPreviewer->findText("");
  467. }
  468. }