vmdeditor.cpp 36 KB

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