vmdeditor.cpp 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164
  1. #include "vmdeditor.h"
  2. #include <QtWidgets>
  3. #include <QMenu>
  4. #include <QDebug>
  5. #include "vdocument.h"
  6. #include "utils/veditutils.h"
  7. #include "vedittab.h"
  8. #include "hgmarkdownhighlighter.h"
  9. #include "vcodeblockhighlighthelper.h"
  10. #include "vmdeditoperations.h"
  11. #include "vtableofcontent.h"
  12. #include "utils/veditutils.h"
  13. #include "dialog/vselectdialog.h"
  14. #include "dialog/vconfirmdeletiondialog.h"
  15. #include "vtextblockdata.h"
  16. #include "vorphanfile.h"
  17. #include "vnotefile.h"
  18. #include "vpreviewmanager.h"
  19. #include "utils/viconutils.h"
  20. #include "dialog/vcopytextashtmldialog.h"
  21. #include "utils/vwebutils.h"
  22. extern VWebUtils *g_webUtils;
  23. extern VConfigManager *g_config;
  24. VMdEditor::VMdEditor(VFile *p_file,
  25. VDocument *p_doc,
  26. MarkdownConverterType p_type,
  27. QWidget *p_parent)
  28. : VTextEdit(p_parent),
  29. VEditor(p_file, this),
  30. m_mdHighlighter(NULL),
  31. m_freshEdit(true),
  32. m_textToHtmlDialog(NULL),
  33. m_zoomDelta(0)
  34. {
  35. Q_ASSERT(p_file->getDocType() == DocType::Markdown);
  36. VEditor::init();
  37. // Hook functions from VEditor.
  38. connect(this, &VTextEdit::cursorPositionChanged,
  39. this, [this]() {
  40. highlightOnCursorPositionChanged();
  41. });
  42. connect(this, &VTextEdit::selectionChanged,
  43. this, [this]() {
  44. highlightSelectedWord();
  45. });
  46. // End.
  47. setReadOnly(true);
  48. m_mdHighlighter = new HGMarkdownHighlighter(g_config->getMdHighlightingStyles(),
  49. g_config->getCodeBlockStyles(),
  50. g_config->getMarkdownHighlightInterval(),
  51. document());
  52. connect(m_mdHighlighter, &HGMarkdownHighlighter::headersUpdated,
  53. this, &VMdEditor::updateHeaders);
  54. // After highlight, the cursor may trun into non-visible. We should make it visible
  55. // in this case.
  56. connect(m_mdHighlighter, &HGMarkdownHighlighter::highlightCompleted,
  57. this, [this]() {
  58. makeBlockVisible(textCursor().block());
  59. if (m_freshEdit) {
  60. m_freshEdit = false;
  61. emit m_object->ready();
  62. }
  63. });
  64. m_cbHighlighter = new VCodeBlockHighlightHelper(m_mdHighlighter,
  65. p_doc,
  66. p_type);
  67. m_previewMgr = new VPreviewManager(this, m_mdHighlighter);
  68. connect(m_mdHighlighter, &HGMarkdownHighlighter::imageLinksUpdated,
  69. m_previewMgr, &VPreviewManager::imageLinksUpdated);
  70. connect(m_previewMgr, &VPreviewManager::requestUpdateImageLinks,
  71. m_mdHighlighter, &HGMarkdownHighlighter::updateHighlight);
  72. m_editOps = new VMdEditOperations(this, m_file);
  73. connect(m_editOps, &VEditOperations::statusMessage,
  74. m_object, &VEditorObject::statusMessage);
  75. connect(m_editOps, &VEditOperations::vimStatusUpdated,
  76. m_object, &VEditorObject::vimStatusUpdated);
  77. connect(this, &VTextEdit::cursorPositionChanged,
  78. this, &VMdEditor::updateCurrentHeader);
  79. updateFontAndPalette();
  80. updateConfig();
  81. }
  82. void VMdEditor::updateFontAndPalette()
  83. {
  84. setFont(g_config->getMdEditFont());
  85. setPalette(g_config->getMdEditPalette());
  86. // setPalette() won't change the foreground.
  87. setTextColor(g_config->getMdEditPalette().color(QPalette::Text));
  88. }
  89. void VMdEditor::beginEdit()
  90. {
  91. updateConfig();
  92. initInitImages();
  93. setModified(false);
  94. setReadOnlyAndHighlightCurrentLine(false);
  95. emit statusChanged();
  96. if (m_freshEdit) {
  97. m_mdHighlighter->updateHighlight();
  98. relayout();
  99. } else {
  100. updateHeaders(m_mdHighlighter->getHeaderRegions());
  101. }
  102. }
  103. void VMdEditor::endEdit()
  104. {
  105. setReadOnlyAndHighlightCurrentLine(true);
  106. clearUnusedImages();
  107. }
  108. void VMdEditor::saveFile()
  109. {
  110. Q_ASSERT(m_file->isModifiable());
  111. if (!document()->isModified()) {
  112. return;
  113. }
  114. m_file->setContent(toPlainText());
  115. setModified(false);
  116. clearUnusedImages();
  117. initInitImages();
  118. }
  119. void VMdEditor::reloadFile()
  120. {
  121. bool readonly = isReadOnly();
  122. setReadOnly(true);
  123. const QString &content = m_file->getContent();
  124. setPlainText(content);
  125. setModified(false);
  126. m_mdHighlighter->updateHighlightFast();
  127. m_freshEdit = true;
  128. setReadOnly(readonly);
  129. }
  130. bool VMdEditor::scrollToBlock(int p_blockNumber)
  131. {
  132. QTextBlock block = document()->findBlockByNumber(p_blockNumber);
  133. if (block.isValid()) {
  134. VEditUtils::scrollBlockInPage(this, block.blockNumber(), 0);
  135. moveCursor(QTextCursor::EndOfBlock);
  136. return true;
  137. }
  138. return false;
  139. }
  140. // Get the visual offset of a block.
  141. #define GETVISUALOFFSETY (contentOffsetY() + (int)rect.y())
  142. void VMdEditor::makeBlockVisible(const QTextBlock &p_block)
  143. {
  144. if (!p_block.isValid() || !p_block.isVisible()) {
  145. return;
  146. }
  147. QScrollBar *vbar = verticalScrollBar();
  148. if (!vbar || (vbar->minimum() == vbar->maximum())) {
  149. // No vertical scrollbar. No need to scroll.
  150. return;
  151. }
  152. int height = rect().height();
  153. QScrollBar *hbar = horizontalScrollBar();
  154. if (hbar && (hbar->minimum() != hbar->maximum())) {
  155. height -= hbar->height();
  156. }
  157. bool moved = false;
  158. QAbstractTextDocumentLayout *layout = document()->documentLayout();
  159. QRectF rect = layout->blockBoundingRect(p_block);
  160. int y = GETVISUALOFFSETY;
  161. int rectHeight = (int)rect.height();
  162. // Handle the case rectHeight >= height.
  163. if (rectHeight >= height) {
  164. if (y < 0) {
  165. // Need to scroll up.
  166. while (y + rectHeight < height && vbar->value() > vbar->minimum()) {
  167. moved = true;
  168. vbar->setValue(vbar->value() - vbar->singleStep());
  169. rect = layout->blockBoundingRect(p_block);
  170. rectHeight = (int)rect.height();
  171. y = GETVISUALOFFSETY;
  172. }
  173. } else if (y > 0) {
  174. // Need to scroll down.
  175. while (y > 0 && vbar->value() < vbar->maximum()) {
  176. moved = true;
  177. vbar->setValue(vbar->value() + vbar->singleStep());
  178. rect = layout->blockBoundingRect(p_block);
  179. rectHeight = (int)rect.height();
  180. y = GETVISUALOFFSETY;
  181. }
  182. if (y < 0) {
  183. // One step back.
  184. moved = true;
  185. vbar->setValue(vbar->value() - vbar->singleStep());
  186. }
  187. }
  188. if (moved) {
  189. qDebug() << "scroll to make huge block visible";
  190. }
  191. return;
  192. }
  193. while (y < 0 && vbar->value() > vbar->minimum()) {
  194. moved = true;
  195. vbar->setValue(vbar->value() - vbar->singleStep());
  196. rect = layout->blockBoundingRect(p_block);
  197. rectHeight = (int)rect.height();
  198. y = GETVISUALOFFSETY;
  199. }
  200. if (moved) {
  201. qDebug() << "scroll page down to make block visible";
  202. return;
  203. }
  204. while (y + rectHeight > height && vbar->value() < vbar->maximum()) {
  205. moved = true;
  206. vbar->setValue(vbar->value() + vbar->singleStep());
  207. rect = layout->blockBoundingRect(p_block);
  208. rectHeight = (int)rect.height();
  209. y = GETVISUALOFFSETY;
  210. }
  211. if (moved) {
  212. qDebug() << "scroll page up to make block visible";
  213. }
  214. }
  215. void VMdEditor::contextMenuEvent(QContextMenuEvent *p_event)
  216. {
  217. QMenu *menu = createStandardContextMenu();
  218. menu->setToolTipsVisible(true);
  219. VEditTab *editTab = dynamic_cast<VEditTab *>(parent());
  220. Q_ASSERT(editTab);
  221. if (editTab->isEditMode()) {
  222. const QList<QAction *> actions = menu->actions();
  223. if (textCursor().hasSelection()) {
  224. initCopyAsMenu(actions.isEmpty() ? NULL : actions.last(), menu);
  225. } else {
  226. QAction *saveExitAct = new QAction(VIconUtils::menuIcon(":/resources/icons/save_exit.svg"),
  227. tr("&Save Changes And Read"),
  228. menu);
  229. saveExitAct->setToolTip(tr("Save changes and exit edit mode"));
  230. connect(saveExitAct, &QAction::triggered,
  231. this, [this]() {
  232. emit m_object->saveAndRead();
  233. });
  234. QAction *discardExitAct = new QAction(VIconUtils::menuIcon(":/resources/icons/discard_exit.svg"),
  235. tr("&Discard Changes And Read"),
  236. menu);
  237. discardExitAct->setToolTip(tr("Discard changes and exit edit mode"));
  238. connect(discardExitAct, &QAction::triggered,
  239. this, [this]() {
  240. emit m_object->discardAndRead();
  241. });
  242. menu->insertAction(actions.isEmpty() ? NULL : actions[0], discardExitAct);
  243. menu->insertAction(discardExitAct, saveExitAct);
  244. }
  245. if (!actions.isEmpty()) {
  246. menu->insertSeparator(actions[0]);
  247. }
  248. }
  249. menu->exec(p_event->globalPos());
  250. delete menu;
  251. }
  252. void VMdEditor::mousePressEvent(QMouseEvent *p_event)
  253. {
  254. if (handleMousePressEvent(p_event)) {
  255. return;
  256. }
  257. VTextEdit::mousePressEvent(p_event);
  258. emit m_object->mousePressed(p_event);
  259. }
  260. void VMdEditor::mouseReleaseEvent(QMouseEvent *p_event)
  261. {
  262. if (handleMouseReleaseEvent(p_event)) {
  263. return;
  264. }
  265. VTextEdit::mouseReleaseEvent(p_event);
  266. emit m_object->mouseReleased(p_event);
  267. }
  268. void VMdEditor::mouseMoveEvent(QMouseEvent *p_event)
  269. {
  270. if (handleMouseMoveEvent(p_event)) {
  271. return;
  272. }
  273. VTextEdit::mouseMoveEvent(p_event);
  274. emit m_object->mouseMoved(p_event);
  275. }
  276. QVariant VMdEditor::inputMethodQuery(Qt::InputMethodQuery p_query) const
  277. {
  278. QVariant ret;
  279. if (handleInputMethodQuery(p_query, ret)) {
  280. return ret;
  281. }
  282. return VTextEdit::inputMethodQuery(p_query);
  283. }
  284. bool VMdEditor::isBlockVisible(const QTextBlock &p_block)
  285. {
  286. if (!p_block.isValid() || !p_block.isVisible()) {
  287. return false;
  288. }
  289. QScrollBar *vbar = verticalScrollBar();
  290. if (!vbar || !vbar->isVisible()) {
  291. // No vertical scrollbar.
  292. return true;
  293. }
  294. int height = rect().height();
  295. QScrollBar *hbar = horizontalScrollBar();
  296. if (hbar && hbar->isVisible()) {
  297. height -= hbar->height();
  298. }
  299. QAbstractTextDocumentLayout *layout = document()->documentLayout();
  300. QRectF rect = layout->blockBoundingRect(p_block);
  301. int y = GETVISUALOFFSETY;
  302. int rectHeight = (int)rect.height();
  303. return (y >= 0 && y < height) || (y < 0 && y + rectHeight > 0);
  304. }
  305. static void addHeaderSequence(QVector<int> &p_sequence, int p_level, int p_baseLevel)
  306. {
  307. Q_ASSERT(p_level >= 1 && p_level < p_sequence.size());
  308. if (p_level < p_baseLevel) {
  309. p_sequence.fill(0);
  310. return;
  311. }
  312. ++p_sequence[p_level];
  313. for (int i = p_level + 1; i < p_sequence.size(); ++i) {
  314. p_sequence[i] = 0;
  315. }
  316. }
  317. static QString headerSequenceStr(const QVector<int> &p_sequence)
  318. {
  319. QString res;
  320. for (int i = 1; i < p_sequence.size(); ++i) {
  321. if (p_sequence[i] != 0) {
  322. res = res + QString::number(p_sequence[i]) + '.';
  323. } else if (res.isEmpty()) {
  324. continue;
  325. } else {
  326. break;
  327. }
  328. }
  329. return res;
  330. }
  331. static void insertSequenceToHeader(QTextBlock p_block,
  332. QRegExp &p_reg,
  333. QRegExp &p_preReg,
  334. const QString &p_seq)
  335. {
  336. if (!p_block.isValid()) {
  337. return;
  338. }
  339. QString text = p_block.text();
  340. bool matched = p_reg.exactMatch(text);
  341. Q_ASSERT(matched);
  342. matched = p_preReg.exactMatch(text);
  343. Q_ASSERT(matched);
  344. int start = p_reg.cap(1).length() + 1;
  345. int end = p_preReg.cap(1).length();
  346. Q_ASSERT(start <= end);
  347. QTextCursor cursor(p_block);
  348. cursor.setPosition(p_block.position() + start);
  349. if (start != end) {
  350. cursor.setPosition(p_block.position() + end, QTextCursor::KeepAnchor);
  351. }
  352. if (p_seq.isEmpty()) {
  353. cursor.removeSelectedText();
  354. } else {
  355. cursor.insertText(p_seq + ' ');
  356. }
  357. }
  358. void VMdEditor::updateHeaders(const QVector<VElementRegion> &p_headerRegions)
  359. {
  360. QTextDocument *doc = document();
  361. QVector<VTableOfContentItem> headers;
  362. QVector<int> headerBlockNumbers;
  363. QVector<QString> headerSequences;
  364. if (!p_headerRegions.isEmpty()) {
  365. headers.reserve(p_headerRegions.size());
  366. headerBlockNumbers.reserve(p_headerRegions.size());
  367. headerSequences.reserve(p_headerRegions.size());
  368. }
  369. // Assume that each block contains only one line
  370. // Only support # syntax for now
  371. QRegExp headerReg(VUtils::c_headerRegExp);
  372. int baseLevel = -1;
  373. for (auto const & reg : p_headerRegions) {
  374. QTextBlock block = doc->findBlock(reg.m_startPos);
  375. if (!block.isValid()) {
  376. continue;
  377. }
  378. if (!block.contains(reg.m_endPos - 1)) {
  379. qWarning() << "header accross multiple blocks, starting from block"
  380. << block.blockNumber()
  381. << block.text();
  382. }
  383. if ((block.userState() == HighlightBlockState::Normal)
  384. && headerReg.exactMatch(block.text())) {
  385. int level = headerReg.cap(1).length();
  386. VTableOfContentItem header(headerReg.cap(2).trimmed(),
  387. level,
  388. block.blockNumber(),
  389. headers.size());
  390. headers.append(header);
  391. headerBlockNumbers.append(block.blockNumber());
  392. headerSequences.append(headerReg.cap(3));
  393. if (baseLevel == -1) {
  394. baseLevel = level;
  395. } else if (baseLevel > level) {
  396. baseLevel = level;
  397. }
  398. }
  399. }
  400. m_headers.clear();
  401. bool autoSequence = m_config.m_enableHeadingSequence
  402. && !isReadOnly()
  403. && m_file->isModifiable();
  404. int headingSequenceBaseLevel = g_config->getHeadingSequenceBaseLevel();
  405. if (headingSequenceBaseLevel < 1 || headingSequenceBaseLevel > 6) {
  406. headingSequenceBaseLevel = 1;
  407. }
  408. QVector<int> seqs(7, 0);
  409. QRegExp preReg(VUtils::c_headerPrefixRegExp);
  410. int curLevel = baseLevel - 1;
  411. for (int i = 0; i < headers.size(); ++i) {
  412. VTableOfContentItem &item = headers[i];
  413. while (item.m_level > curLevel + 1) {
  414. curLevel += 1;
  415. // Insert empty level which is an invalid header.
  416. m_headers.append(VTableOfContentItem(c_emptyHeaderName,
  417. curLevel,
  418. -1,
  419. m_headers.size()));
  420. if (autoSequence) {
  421. addHeaderSequence(seqs, curLevel, headingSequenceBaseLevel);
  422. }
  423. }
  424. item.m_index = m_headers.size();
  425. m_headers.append(item);
  426. curLevel = item.m_level;
  427. if (autoSequence) {
  428. addHeaderSequence(seqs, item.m_level, headingSequenceBaseLevel);
  429. QString seqStr = headerSequenceStr(seqs);
  430. if (headerSequences[i] != seqStr) {
  431. // Insert correct sequence.
  432. insertSequenceToHeader(doc->findBlockByNumber(headerBlockNumbers[i]),
  433. headerReg,
  434. preReg,
  435. seqStr);
  436. }
  437. }
  438. }
  439. emit headersChanged(m_headers);
  440. updateCurrentHeader();
  441. }
  442. void VMdEditor::updateCurrentHeader()
  443. {
  444. emit currentHeaderChanged(textCursor().block().blockNumber());
  445. }
  446. void VMdEditor::initInitImages()
  447. {
  448. m_initImages = VUtils::fetchImagesFromMarkdownFile(m_file,
  449. ImageLink::LocalRelativeInternal);
  450. }
  451. void VMdEditor::clearUnusedImages()
  452. {
  453. QVector<ImageLink> images = VUtils::fetchImagesFromMarkdownFile(m_file,
  454. ImageLink::LocalRelativeInternal);
  455. QSet<QString> unusedImages;
  456. if (!m_insertedImages.isEmpty()) {
  457. for (int i = 0; i < m_insertedImages.size(); ++i) {
  458. const ImageLink &link = m_insertedImages[i];
  459. if (link.m_type != ImageLink::LocalRelativeInternal) {
  460. continue;
  461. }
  462. int j;
  463. for (j = 0; j < images.size(); ++j) {
  464. if (VUtils::equalPath(link.m_path, images[j].m_path)) {
  465. break;
  466. }
  467. }
  468. // This inserted image is no longer in the file.
  469. if (j == images.size()) {
  470. unusedImages.insert(link.m_path);
  471. }
  472. }
  473. m_insertedImages.clear();
  474. }
  475. for (int i = 0; i < m_initImages.size(); ++i) {
  476. const ImageLink &link = m_initImages[i];
  477. V_ASSERT(link.m_type == ImageLink::LocalRelativeInternal);
  478. int j;
  479. for (j = 0; j < images.size(); ++j) {
  480. if (VUtils::equalPath(link.m_path, images[j].m_path)) {
  481. break;
  482. }
  483. }
  484. // Original local relative image is no longer in the file.
  485. if (j == images.size()) {
  486. unusedImages.insert(link.m_path);
  487. }
  488. }
  489. if (!unusedImages.isEmpty()) {
  490. if (g_config->getConfirmImagesCleanUp()) {
  491. QVector<ConfirmItemInfo> items;
  492. for (auto const & img : unusedImages) {
  493. items.push_back(ConfirmItemInfo(img,
  494. img,
  495. img,
  496. NULL));
  497. }
  498. QString text = tr("Following images seems not to be used in this note anymore. "
  499. "Please confirm the deletion of these images.");
  500. QString info = tr("Deleted files could be found in the recycle "
  501. "bin of this note.<br>"
  502. "Click \"Cancel\" to leave them untouched.");
  503. VConfirmDeletionDialog dialog(tr("Confirm Cleaning Up Unused Images"),
  504. text,
  505. info,
  506. items,
  507. true,
  508. true,
  509. true,
  510. this);
  511. unusedImages.clear();
  512. if (dialog.exec()) {
  513. items = dialog.getConfirmedItems();
  514. g_config->setConfirmImagesCleanUp(dialog.getAskAgainEnabled());
  515. for (auto const & item : items) {
  516. unusedImages.insert(item.m_name);
  517. }
  518. }
  519. }
  520. for (auto const & item : unusedImages) {
  521. bool ret = false;
  522. if (m_file->getType() == FileType::Note) {
  523. const VNoteFile *tmpFile = dynamic_cast<const VNoteFile *>((VFile *)m_file);
  524. ret = VUtils::deleteFile(tmpFile->getNotebook(), item, false);
  525. } else if (m_file->getType() == FileType::Orphan) {
  526. const VOrphanFile *tmpFile = dynamic_cast<const VOrphanFile *>((VFile *)m_file);
  527. ret = VUtils::deleteFile(tmpFile, item, false);
  528. } else {
  529. Q_ASSERT(false);
  530. }
  531. if (!ret) {
  532. qWarning() << "fail to delete unused original image" << item;
  533. } else {
  534. qDebug() << "delete unused image" << item;
  535. }
  536. }
  537. }
  538. m_initImages.clear();
  539. }
  540. void VMdEditor::keyPressEvent(QKeyEvent *p_event)
  541. {
  542. int key = p_event->key();
  543. int modifiers = p_event->modifiers();
  544. switch (key) {
  545. case Qt::Key_Minus:
  546. case Qt::Key_Underscore:
  547. // Zoom out.
  548. if (modifiers & Qt::ControlModifier) {
  549. zoomPage(false);
  550. return;
  551. }
  552. break;
  553. case Qt::Key_Plus:
  554. case Qt::Key_Equal:
  555. // Zoom in.
  556. if (modifiers & Qt::ControlModifier) {
  557. zoomPage(true);
  558. return;
  559. }
  560. break;
  561. case Qt::Key_0:
  562. // Restore zoom.
  563. if (modifiers & Qt::ControlModifier) {
  564. if (m_zoomDelta > 0) {
  565. zoomPage(false, m_zoomDelta);
  566. } else if (m_zoomDelta < 0) {
  567. zoomPage(true, -m_zoomDelta);
  568. }
  569. return;
  570. }
  571. break;
  572. default:
  573. break;
  574. }
  575. if (m_editOps && m_editOps->handleKeyPressEvent(p_event)) {
  576. return;
  577. }
  578. // Esc to exit edit mode when Vim is disabled.
  579. if (key == Qt::Key_Escape) {
  580. emit m_object->discardAndRead();
  581. return;
  582. }
  583. VTextEdit::keyPressEvent(p_event);
  584. }
  585. bool VMdEditor::canInsertFromMimeData(const QMimeData *p_source) const
  586. {
  587. return p_source->hasImage()
  588. || p_source->hasUrls()
  589. || VTextEdit::canInsertFromMimeData(p_source);
  590. }
  591. void VMdEditor::insertFromMimeData(const QMimeData *p_source)
  592. {
  593. VSelectDialog dialog(tr("Insert From Clipboard"), this);
  594. dialog.addSelection(tr("Insert As Image"), 0);
  595. dialog.addSelection(tr("Insert As Text"), 1);
  596. if (p_source->hasImage()) {
  597. // Image data in the clipboard
  598. if (p_source->hasText()) {
  599. if (dialog.exec() == QDialog::Accepted) {
  600. if (dialog.getSelection() == 1) {
  601. // Insert as text.
  602. Q_ASSERT(p_source->hasText() && p_source->hasImage());
  603. VTextEdit::insertFromMimeData(p_source);
  604. return;
  605. }
  606. } else {
  607. return;
  608. }
  609. }
  610. m_editOps->insertImageFromMimeData(p_source);
  611. return;
  612. } else if (p_source->hasUrls()) {
  613. QList<QUrl> urls = p_source->urls();
  614. if (urls.size() == 1 && VUtils::isImageURL(urls[0])) {
  615. if (dialog.exec() == QDialog::Accepted) {
  616. // FIXME: After calling dialog.exec(), p_source->hasUrl() returns false.
  617. if (dialog.getSelection() == 0) {
  618. // Insert as image.
  619. m_editOps->insertImageFromURL(urls[0]);
  620. return;
  621. }
  622. QMimeData newSource;
  623. newSource.setUrls(urls);
  624. VTextEdit::insertFromMimeData(&newSource);
  625. return;
  626. } else {
  627. return;
  628. }
  629. }
  630. } else if (p_source->hasText()) {
  631. QString text = p_source->text();
  632. if (VUtils::isImageURLText(text)) {
  633. // The text is a URL to an image.
  634. if (dialog.exec() == QDialog::Accepted) {
  635. if (dialog.getSelection() == 0) {
  636. // Insert as image.
  637. QUrl url(text);
  638. if (url.isValid()) {
  639. m_editOps->insertImageFromURL(QUrl(text));
  640. }
  641. return;
  642. }
  643. } else {
  644. return;
  645. }
  646. }
  647. Q_ASSERT(p_source->hasText());
  648. }
  649. VTextEdit::insertFromMimeData(p_source);
  650. }
  651. void VMdEditor::imageInserted(const QString &p_path, const QString &p_url)
  652. {
  653. ImageLink link;
  654. link.m_path = p_path;
  655. link.m_url = p_url;
  656. if (m_file->useRelativeImageFolder()) {
  657. link.m_type = ImageLink::LocalRelativeInternal;
  658. } else {
  659. link.m_type = ImageLink::LocalAbsolute;
  660. }
  661. m_insertedImages.append(link);
  662. }
  663. bool VMdEditor::scrollToHeader(int p_blockNumber)
  664. {
  665. if (p_blockNumber < 0) {
  666. return false;
  667. }
  668. return scrollToBlock(p_blockNumber);
  669. }
  670. int VMdEditor::indexOfCurrentHeader() const
  671. {
  672. if (m_headers.isEmpty()) {
  673. return -1;
  674. }
  675. int blockNumber = textCursor().block().blockNumber();
  676. for (int i = m_headers.size() - 1; i >= 0; --i) {
  677. if (!m_headers[i].isEmpty()
  678. && m_headers[i].m_blockNumber <= blockNumber) {
  679. return i;
  680. }
  681. }
  682. return -1;
  683. }
  684. bool VMdEditor::jumpTitle(bool p_forward, int p_relativeLevel, int p_repeat)
  685. {
  686. if (m_headers.isEmpty()) {
  687. return false;
  688. }
  689. QTextCursor cursor = textCursor();
  690. int cursorLine = cursor.block().blockNumber();
  691. int targetIdx = -1;
  692. // -1: skip level check.
  693. int targetLevel = 0;
  694. int idx = indexOfCurrentHeader();
  695. if (idx == -1) {
  696. // Cursor locates at the beginning, before any headers.
  697. if (p_relativeLevel < 0 || !p_forward) {
  698. return false;
  699. }
  700. }
  701. int delta = 1;
  702. if (!p_forward) {
  703. delta = -1;
  704. }
  705. bool firstHeader = true;
  706. for (targetIdx = idx == -1 ? 0 : idx;
  707. targetIdx >= 0 && targetIdx < m_headers.size();
  708. targetIdx += delta) {
  709. const VTableOfContentItem &header = m_headers[targetIdx];
  710. if (header.isEmpty()) {
  711. continue;
  712. }
  713. if (targetLevel == 0) {
  714. // The target level has not been init yet.
  715. Q_ASSERT(firstHeader);
  716. targetLevel = header.m_level;
  717. if (p_relativeLevel < 0) {
  718. targetLevel += p_relativeLevel;
  719. if (targetLevel < 1) {
  720. // Invalid level.
  721. return false;
  722. }
  723. } else if (p_relativeLevel > 0) {
  724. targetLevel = -1;
  725. }
  726. }
  727. if (targetLevel == -1 || header.m_level == targetLevel) {
  728. if (firstHeader
  729. && (cursorLine == header.m_blockNumber
  730. || p_forward)
  731. && idx != -1) {
  732. // This header is not counted for the repeat.
  733. firstHeader = false;
  734. continue;
  735. }
  736. if (--p_repeat == 0) {
  737. // Found.
  738. break;
  739. }
  740. } else if (header.m_level < targetLevel) {
  741. // Stop by higher level.
  742. return false;
  743. }
  744. firstHeader = false;
  745. }
  746. if (targetIdx < 0 || targetIdx >= m_headers.size()) {
  747. return false;
  748. }
  749. // Jump to target header.
  750. int line = m_headers[targetIdx].m_blockNumber;
  751. if (line > -1) {
  752. QTextBlock block = document()->findBlockByNumber(line);
  753. if (block.isValid()) {
  754. cursor.setPosition(block.position());
  755. setTextCursor(cursor);
  756. return true;
  757. }
  758. }
  759. return false;
  760. }
  761. void VMdEditor::scrollBlockInPage(int p_blockNum, int p_dest)
  762. {
  763. VEditUtils::scrollBlockInPage(this, p_blockNum, p_dest);
  764. }
  765. void VMdEditor::updateTextEditConfig()
  766. {
  767. setBlockImageEnabled(g_config->getEnablePreviewImages());
  768. setImageWidthConstrainted(g_config->getEnablePreviewImageConstraint());
  769. setLineLeading(m_config.m_lineDistanceHeight);
  770. setImageLineColor(g_config->getEditorPreviewImageLineFg());
  771. int lineNumber = g_config->getEditorLineNumber();
  772. if (lineNumber < (int)LineNumberType::None || lineNumber >= (int)LineNumberType::Invalid) {
  773. lineNumber = (int)LineNumberType::None;
  774. }
  775. setLineNumberType((LineNumberType)lineNumber);
  776. setLineNumberColor(g_config->getEditorLineNumberFg(),
  777. g_config->getEditorLineNumberBg());
  778. m_previewMgr->setPreviewEnabled(g_config->getEnablePreviewImages());
  779. }
  780. void VMdEditor::updateConfig()
  781. {
  782. updateEditConfig();
  783. updateTextEditConfig();
  784. }
  785. QString VMdEditor::getContent() const
  786. {
  787. return toPlainText();
  788. }
  789. void VMdEditor::setContent(const QString &p_content, bool p_modified)
  790. {
  791. if (p_modified) {
  792. QTextCursor cursor = textCursor();
  793. cursor.select(QTextCursor::Document);
  794. cursor.insertText(p_content);
  795. setTextCursor(cursor);
  796. } else {
  797. setPlainText(p_content);
  798. }
  799. }
  800. void VMdEditor::refreshPreview()
  801. {
  802. m_previewMgr->refreshPreview();
  803. }
  804. void VMdEditor::updateInitAndInsertedImages(bool p_fileChanged, UpdateAction p_act)
  805. {
  806. if (p_fileChanged && p_act == UpdateAction::InfoChanged) {
  807. return;
  808. }
  809. if (!isModified()) {
  810. Q_ASSERT(m_insertedImages.isEmpty());
  811. m_insertedImages.clear();
  812. if (!m_initImages.isEmpty()) {
  813. // Re-generate init images.
  814. initInitImages();
  815. }
  816. return;
  817. }
  818. // Update init images.
  819. QVector<ImageLink> tmp = m_initImages;
  820. initInitImages();
  821. Q_ASSERT(tmp.size() == m_initImages.size());
  822. QDir dir(m_file->fetchBasePath());
  823. // File has been moved.
  824. if (p_fileChanged) {
  825. // Since we clear unused images once user save the note, all images
  826. // in m_initImages now are moved already.
  827. // Update inserted images.
  828. // Inserted images should be moved manually here. Then update all the
  829. // paths.
  830. for (auto & link : m_insertedImages) {
  831. if (link.m_type == ImageLink::LocalAbsolute) {
  832. continue;
  833. }
  834. QString newPath = QDir::cleanPath(dir.absoluteFilePath(link.m_url));
  835. if (VUtils::equalPath(link.m_path, newPath)) {
  836. continue;
  837. }
  838. if (!VUtils::copyFile(link.m_path, newPath, true)) {
  839. VUtils::showMessage(QMessageBox::Warning,
  840. tr("Warning"),
  841. tr("Fail to move unsaved inserted image %1 to %2.")
  842. .arg(link.m_path)
  843. .arg(newPath),
  844. tr("Please check it manually to avoid image loss."),
  845. QMessageBox::Ok,
  846. QMessageBox::Ok,
  847. this);
  848. continue;
  849. }
  850. link.m_path = newPath;
  851. }
  852. } else {
  853. // Directory changed.
  854. // Update inserted images.
  855. for (auto & link : m_insertedImages) {
  856. if (link.m_type == ImageLink::LocalAbsolute) {
  857. continue;
  858. }
  859. QString newPath = QDir::cleanPath(dir.absoluteFilePath(link.m_url));
  860. link.m_path = newPath;
  861. }
  862. }
  863. }
  864. void VMdEditor::handleCopyAsAction(QAction *p_act)
  865. {
  866. QTextCursor cursor = textCursor();
  867. Q_ASSERT(cursor.hasSelection());
  868. QString text = VEditUtils::selectedText(cursor);
  869. Q_ASSERT(!text.isEmpty());
  870. Q_ASSERT(!m_textToHtmlDialog);
  871. m_textToHtmlDialog = new VCopyTextAsHtmlDialog(text, p_act->data().toString(), this);
  872. // For Hoedown, we use marked.js to convert the text to have a general interface.
  873. emit requestTextToHtml(text);
  874. m_textToHtmlDialog->exec();
  875. delete m_textToHtmlDialog;
  876. m_textToHtmlDialog = NULL;
  877. }
  878. void VMdEditor::textToHtmlFinished(const QString &p_text,
  879. const QUrl &p_baseUrl,
  880. const QString &p_html)
  881. {
  882. if (m_textToHtmlDialog && m_textToHtmlDialog->getText() == p_text) {
  883. m_textToHtmlDialog->setConvertedHtml(p_baseUrl, p_html);
  884. }
  885. }
  886. void VMdEditor::wheelEvent(QWheelEvent *p_event)
  887. {
  888. if (handleWheelEvent(p_event)) {
  889. return;
  890. }
  891. VTextEdit::wheelEvent(p_event);
  892. }
  893. void VMdEditor::zoomPage(bool p_zoomIn, int p_range)
  894. {
  895. int delta;
  896. const int minSize = 2;
  897. if (p_zoomIn) {
  898. delta = p_range;
  899. zoomIn(p_range);
  900. } else {
  901. delta = -p_range;
  902. zoomOut(p_range);
  903. }
  904. m_zoomDelta += delta;
  905. QVector<HighlightingStyle> &styles = m_mdHighlighter->getHighlightingStyles();
  906. for (auto & it : styles) {
  907. int size = it.format.fontPointSize();
  908. if (size == 0) {
  909. // It contains no font size format.
  910. continue;
  911. }
  912. size += delta;
  913. if (size < minSize) {
  914. size = minSize;
  915. }
  916. it.format.setFontPointSize(size);
  917. }
  918. QHash<QString, QTextCharFormat> &cbStyles = m_mdHighlighter->getCodeBlockStyles();
  919. for (auto it = cbStyles.begin(); it != cbStyles.end(); ++it) {
  920. int size = it.value().fontPointSize();
  921. if (size == 0) {
  922. // It contains no font size format.
  923. continue;
  924. }
  925. size += delta;
  926. if (size < minSize) {
  927. size = minSize;
  928. }
  929. it.value().setFontPointSize(size);
  930. }
  931. m_mdHighlighter->rehighlight();
  932. }
  933. void VMdEditor::initCopyAsMenu(QAction *p_before, QMenu *p_menu)
  934. {
  935. QStringList targets = g_webUtils->getCopyTargetsName();
  936. if (targets.isEmpty()) {
  937. return;
  938. }
  939. QMenu *subMenu = new QMenu(tr("Copy HTML As"), p_menu);
  940. subMenu->setToolTipsVisible(true);
  941. for (auto const & target : targets) {
  942. QAction *act = new QAction(target, subMenu);
  943. act->setData(target);
  944. act->setToolTip(tr("Copy selected content as HTML using rules specified by target %1").arg(target));
  945. subMenu->addAction(act);
  946. }
  947. connect(subMenu, &QMenu::triggered,
  948. this, &VMdEditor::handleCopyAsAction);
  949. QAction *menuAct = p_menu->insertMenu(p_before, subMenu);
  950. if (p_before) {
  951. p_menu->removeAction(p_before);
  952. p_menu->insertAction(menuAct, p_before);
  953. p_menu->insertSeparator(menuAct);
  954. }
  955. }