vmdeditor.cpp 39 KB

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