vmdedit.cpp 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557
  1. #include <QtWidgets>
  2. #include "vmdedit.h"
  3. #include "hgmarkdownhighlighter.h"
  4. #include "vcodeblockhighlighthelper.h"
  5. #include "vmdeditoperations.h"
  6. #include "vnote.h"
  7. #include "vconfigmanager.h"
  8. #include "vtoc.h"
  9. #include "utils/vutils.h"
  10. #include "dialog/vselectdialog.h"
  11. #include "vimagepreviewer.h"
  12. extern VConfigManager vconfig;
  13. extern VNote *g_vnote;
  14. VMdEdit::VMdEdit(VFile *p_file, VDocument *p_vdoc, MarkdownConverterType p_type,
  15. QWidget *p_parent)
  16. : VEdit(p_file, p_parent), m_mdHighlighter(NULL)
  17. {
  18. V_ASSERT(p_file->getDocType() == DocType::Markdown);
  19. setAcceptRichText(false);
  20. m_mdHighlighter = new HGMarkdownHighlighter(vconfig.getMdHighlightingStyles(),
  21. vconfig.getCodeBlockStyles(),
  22. 700, document());
  23. connect(m_mdHighlighter, &HGMarkdownHighlighter::highlightCompleted,
  24. this, &VMdEdit::generateEditOutline);
  25. // After highlight, the cursor may trun into non-visible. We should make it visible
  26. // in this case.
  27. connect(m_mdHighlighter, &HGMarkdownHighlighter::highlightCompleted,
  28. this, [this]() {
  29. QRect rect = this->cursorRect();
  30. int height = this->rect().height();
  31. QScrollBar *sbar = this->horizontalScrollBar();
  32. if (sbar && sbar->isVisible()) {
  33. height -= sbar->height();
  34. }
  35. if ((rect.y() < height
  36. && rect.y() + rect.height() > height)
  37. || (rect.y() < 0 && rect.y() + rect.height() > 0)) {
  38. this->ensureCursorVisible();
  39. }
  40. });
  41. m_cbHighlighter = new VCodeBlockHighlightHelper(m_mdHighlighter, p_vdoc,
  42. p_type);
  43. m_imagePreviewer = new VImagePreviewer(this, 500);
  44. m_editOps = new VMdEditOperations(this, m_file);
  45. connect(m_editOps, &VEditOperations::statusMessage,
  46. this, &VEdit::statusMessage);
  47. connect(m_editOps, &VEditOperations::vimStatusUpdated,
  48. this, &VEdit::vimStatusUpdated);
  49. connect(this, &VMdEdit::cursorPositionChanged,
  50. this, &VMdEdit::updateCurHeader);
  51. connect(this, &VMdEdit::selectionChanged,
  52. this, &VMdEdit::handleSelectionChanged);
  53. connect(QApplication::clipboard(), &QClipboard::changed,
  54. this, &VMdEdit::handleClipboardChanged);
  55. updateFontAndPalette();
  56. updateConfig();
  57. }
  58. void VMdEdit::updateFontAndPalette()
  59. {
  60. setFont(vconfig.getMdEditFont());
  61. setPalette(vconfig.getMdEditPalette());
  62. }
  63. void VMdEdit::beginEdit()
  64. {
  65. updateFontAndPalette();
  66. updateConfig();
  67. Q_ASSERT(m_file->getContent() == toPlainTextWithoutImg());
  68. initInitImages();
  69. m_imagePreviewer->refresh();
  70. setReadOnly(false);
  71. setModified(false);
  72. // Request update outline.
  73. generateEditOutline();
  74. }
  75. void VMdEdit::endEdit()
  76. {
  77. setReadOnly(true);
  78. clearUnusedImages();
  79. }
  80. void VMdEdit::saveFile()
  81. {
  82. if (!document()->isModified()) {
  83. return;
  84. }
  85. m_file->setContent(toPlainTextWithoutImg());
  86. document()->setModified(false);
  87. }
  88. void VMdEdit::reloadFile()
  89. {
  90. const QString &content = m_file->getContent();
  91. V_ASSERT(content.indexOf(QChar::ObjectReplacementCharacter) == -1);
  92. setPlainText(content);
  93. setModified(false);
  94. }
  95. void VMdEdit::keyPressEvent(QKeyEvent *event)
  96. {
  97. if (m_editOps->handleKeyPressEvent(event)) {
  98. return;
  99. }
  100. VEdit::keyPressEvent(event);
  101. }
  102. bool VMdEdit::canInsertFromMimeData(const QMimeData *source) const
  103. {
  104. return source->hasImage() || source->hasUrls()
  105. || VEdit::canInsertFromMimeData(source);
  106. }
  107. void VMdEdit::insertFromMimeData(const QMimeData *source)
  108. {
  109. VSelectDialog dialog(tr("Insert From Clipboard"), this);
  110. dialog.addSelection(tr("Insert As Image"), 0);
  111. dialog.addSelection(tr("Insert As Text"), 1);
  112. if (source->hasImage()) {
  113. // Image data in the clipboard
  114. if (source->hasText()) {
  115. if (dialog.exec() == QDialog::Accepted) {
  116. if (dialog.getSelection() == 1) {
  117. // Insert as text.
  118. Q_ASSERT(source->hasText() && source->hasImage());
  119. VEdit::insertFromMimeData(source);
  120. return;
  121. }
  122. } else {
  123. return;
  124. }
  125. }
  126. m_editOps->insertImageFromMimeData(source);
  127. return;
  128. } else if (source->hasUrls()) {
  129. QList<QUrl> urls = source->urls();
  130. if (urls.size() == 1 && VUtils::isImageURL(urls[0])) {
  131. if (dialog.exec() == QDialog::Accepted) {
  132. // FIXME: After calling dialog.exec(), source->hasUrl() returns false.
  133. if (dialog.getSelection() == 0) {
  134. // Insert as image.
  135. m_editOps->insertImageFromURL(urls[0]);
  136. return;
  137. }
  138. QMimeData newSource;
  139. newSource.setUrls(urls);
  140. VEdit::insertFromMimeData(&newSource);
  141. return;
  142. } else {
  143. return;
  144. }
  145. }
  146. } else if (source->hasText()) {
  147. QString text = source->text();
  148. if (VUtils::isImageURLText(text)) {
  149. // The text is a URL to an image.
  150. if (dialog.exec() == QDialog::Accepted) {
  151. if (dialog.getSelection() == 0) {
  152. // Insert as image.
  153. QUrl url(text);
  154. if (url.isValid()) {
  155. m_editOps->insertImageFromURL(QUrl(text));
  156. }
  157. return;
  158. }
  159. } else {
  160. return;
  161. }
  162. }
  163. Q_ASSERT(source->hasText());
  164. }
  165. VEdit::insertFromMimeData(source);
  166. }
  167. void VMdEdit::imageInserted(const QString &p_path)
  168. {
  169. ImageLink link;
  170. link.m_path = p_path;
  171. link.m_type = ImageLink::LocalRelativeInternal;
  172. m_insertedImages.append(link);
  173. }
  174. void VMdEdit::initInitImages()
  175. {
  176. m_initImages = VUtils::fetchImagesFromMarkdownFile(m_file,
  177. ImageLink::LocalRelativeInternal);
  178. }
  179. void VMdEdit::clearUnusedImages()
  180. {
  181. QVector<ImageLink> images = VUtils::fetchImagesFromMarkdownFile(m_file,
  182. ImageLink::LocalRelativeInternal);
  183. if (!m_insertedImages.isEmpty()) {
  184. for (int i = 0; i < m_insertedImages.size(); ++i) {
  185. const ImageLink &link = m_insertedImages[i];
  186. V_ASSERT(link.m_type == ImageLink::LocalRelativeInternal);
  187. int j;
  188. for (j = 0; j < images.size(); ++j) {
  189. if (VUtils::equalPath(link.m_path, images[j].m_path)) {
  190. break;
  191. }
  192. }
  193. // This inserted image is no longer in the file.
  194. if (j == images.size()) {
  195. if (!QFile(link.m_path).remove()) {
  196. qWarning() << "fail to delete unused inserted image" << link.m_path;
  197. } else {
  198. qDebug() << "delete unused inserted image" << link.m_path;
  199. }
  200. }
  201. }
  202. m_insertedImages.clear();
  203. }
  204. for (int i = 0; i < m_initImages.size(); ++i) {
  205. const ImageLink &link = m_initImages[i];
  206. V_ASSERT(link.m_type == ImageLink::LocalRelativeInternal);
  207. int j;
  208. for (j = 0; j < images.size(); ++j) {
  209. if (VUtils::equalPath(link.m_path, images[j].m_path)) {
  210. break;
  211. }
  212. }
  213. // Original local relative image is no longer in the file.
  214. if (j == images.size()) {
  215. if (!QFile(link.m_path).remove()) {
  216. qWarning() << "fail to delete unused original image" << link.m_path;
  217. } else {
  218. qDebug() << "delete unused original image" << link.m_path;
  219. }
  220. }
  221. }
  222. m_initImages.clear();
  223. }
  224. int VMdEdit::currentCursorHeader() const
  225. {
  226. if (m_headers.isEmpty()) {
  227. return -1;
  228. }
  229. int curLine = textCursor().block().firstLineNumber();
  230. int i = 0;
  231. for (i = m_headers.size() - 1; i >= 0; --i) {
  232. if (!m_headers[i].isEmpty()) {
  233. if (m_headers[i].lineNumber <= curLine) {
  234. break;
  235. }
  236. }
  237. }
  238. if (i == -1) {
  239. return -1;
  240. } else {
  241. Q_ASSERT(m_headers[i].index == i);
  242. return i;
  243. }
  244. }
  245. void VMdEdit::updateCurHeader()
  246. {
  247. if (m_headers.isEmpty()) {
  248. return;
  249. }
  250. int idx = currentCursorHeader();
  251. if (idx == -1) {
  252. emit curHeaderChanged(VAnchor(m_file, "", -1, -1));
  253. return;
  254. }
  255. emit curHeaderChanged(VAnchor(m_file, "", m_headers[idx].lineNumber, m_headers[idx].index));
  256. }
  257. void VMdEdit::generateEditOutline()
  258. {
  259. QTextDocument *doc = document();
  260. m_headers.clear();
  261. QVector<VHeader> headers;
  262. // Assume that each block contains only one line
  263. // Only support # syntax for now
  264. QRegExp headerReg("(#{1,6})\\s*(\\S.*)"); // Need to trim the spaces
  265. int baseLevel = -1;
  266. for (QTextBlock block = doc->begin(); block != doc->end(); block = block.next()) {
  267. V_ASSERT(block.lineCount() == 1);
  268. if ((block.userState() == HighlightBlockState::Normal) &&
  269. headerReg.exactMatch(block.text())) {
  270. int level = headerReg.cap(1).length();
  271. VHeader header(level, headerReg.cap(2).trimmed(),
  272. "", block.firstLineNumber(), headers.size());
  273. headers.append(header);
  274. if (baseLevel == -1) {
  275. baseLevel = level;
  276. } else if (baseLevel > level) {
  277. baseLevel = level;
  278. }
  279. }
  280. }
  281. int curLevel = baseLevel - 1;
  282. for (auto & item : headers) {
  283. while (item.level > curLevel + 1) {
  284. curLevel += 1;
  285. // Insert empty level which is an invalid header.
  286. m_headers.append(VHeader(curLevel, c_emptyHeaderName, "", -1, m_headers.size()));
  287. }
  288. item.index = m_headers.size();
  289. m_headers.append(item);
  290. curLevel = item.level;
  291. }
  292. emit headersChanged(m_headers);
  293. updateCurHeader();
  294. }
  295. void VMdEdit::scrollToHeader(const VAnchor &p_anchor)
  296. {
  297. if (p_anchor.lineNumber == -1
  298. || p_anchor.m_outlineIndex < 0
  299. || p_anchor.m_outlineIndex >= m_headers.size()) {
  300. return;
  301. }
  302. scrollToLine(p_anchor.lineNumber);
  303. }
  304. QString VMdEdit::toPlainTextWithoutImg() const
  305. {
  306. QString text = toPlainText();
  307. int start = 0;
  308. do {
  309. int index = text.indexOf(QChar::ObjectReplacementCharacter, start);
  310. if (index == -1) {
  311. break;
  312. }
  313. start = removeObjectReplacementLine(text, index);
  314. } while (start > -1 && start < text.size());
  315. return text;
  316. }
  317. int VMdEdit::removeObjectReplacementLine(QString &p_text, int p_index) const
  318. {
  319. Q_ASSERT(p_text.size() > p_index && p_text.at(p_index) == QChar::ObjectReplacementCharacter);
  320. int prevLineIdx = p_text.lastIndexOf('\n', p_index);
  321. if (prevLineIdx == -1) {
  322. prevLineIdx = 0;
  323. }
  324. // Remove [\n....?]
  325. p_text.remove(prevLineIdx, p_index - prevLineIdx + 1);
  326. return prevLineIdx - 1;
  327. }
  328. void VMdEdit::handleSelectionChanged()
  329. {
  330. if (!vconfig.getEnablePreviewImages()) {
  331. return;
  332. }
  333. QString text = textCursor().selectedText();
  334. if (text.isEmpty() && !m_imagePreviewer->isPreviewEnabled()) {
  335. m_imagePreviewer->enableImagePreview();
  336. } else if (m_imagePreviewer->isPreviewEnabled()) {
  337. if (text.trimmed() == QString(QChar::ObjectReplacementCharacter)) {
  338. // Select the image and some whitespaces.
  339. // We can let the user copy the image.
  340. return;
  341. } else if (text.contains(QChar::ObjectReplacementCharacter)) {
  342. m_imagePreviewer->disableImagePreview();
  343. }
  344. }
  345. }
  346. void VMdEdit::handleClipboardChanged(QClipboard::Mode p_mode)
  347. {
  348. if (!hasFocus()) {
  349. return;
  350. }
  351. if (p_mode == QClipboard::Clipboard) {
  352. QClipboard *clipboard = QApplication::clipboard();
  353. const QMimeData *mimeData = clipboard->mimeData();
  354. if (mimeData->hasText()) {
  355. QString text = mimeData->text();
  356. if (clipboard->ownsClipboard() &&
  357. (text.trimmed() == QString(QChar::ObjectReplacementCharacter))) {
  358. QImage image = selectedImage();
  359. clipboard->clear(QClipboard::Clipboard);
  360. if (!image.isNull()) {
  361. clipboard->setImage(image, QClipboard::Clipboard);
  362. }
  363. }
  364. }
  365. }
  366. }
  367. QImage VMdEdit::selectedImage()
  368. {
  369. QImage image;
  370. QTextCursor cursor = textCursor();
  371. if (!cursor.hasSelection()) {
  372. return image;
  373. }
  374. int start = cursor.selectionStart();
  375. int end = cursor.selectionEnd();
  376. QTextDocument *doc = document();
  377. QTextBlock startBlock = doc->findBlock(start);
  378. QTextBlock endBlock = doc->findBlock(end);
  379. QTextBlock block = startBlock;
  380. while (block.isValid()) {
  381. if (m_imagePreviewer->isImagePreviewBlock(block)) {
  382. image = m_imagePreviewer->fetchCachedImageFromPreviewBlock(block);
  383. break;
  384. }
  385. if (block == endBlock) {
  386. break;
  387. }
  388. block = block.next();
  389. }
  390. return image;
  391. }
  392. void VMdEdit::resizeEvent(QResizeEvent *p_event)
  393. {
  394. m_imagePreviewer->update();
  395. VEdit::resizeEvent(p_event);
  396. }
  397. const QVector<VHeader> &VMdEdit::getHeaders() const
  398. {
  399. return m_headers;
  400. }
  401. bool VMdEdit::jumpTitle(bool p_forward, int p_relativeLevel, int p_repeat)
  402. {
  403. if (m_headers.isEmpty()) {
  404. return false;
  405. }
  406. QTextCursor cursor = textCursor();
  407. int cursorLine = cursor.block().firstLineNumber();
  408. int targetIdx = -1;
  409. // -1: skip level check.
  410. int targetLevel = 0;
  411. int idx = currentCursorHeader();
  412. if (idx == -1) {
  413. // Cursor locates at the beginning, before any headers.
  414. if (p_relativeLevel < 0 || !p_forward) {
  415. return false;
  416. }
  417. }
  418. int delta = 1;
  419. if (!p_forward) {
  420. delta = -1;
  421. }
  422. bool firstHeader = true;
  423. for (targetIdx = idx == -1 ? 0 : idx;
  424. targetIdx >= 0 && targetIdx < m_headers.size();
  425. targetIdx += delta) {
  426. const VHeader &header = m_headers[targetIdx];
  427. if (header.isEmpty()) {
  428. continue;
  429. }
  430. if (targetLevel == 0) {
  431. // The target level has not been init yet.
  432. Q_ASSERT(firstHeader);
  433. targetLevel = header.level;
  434. if (p_relativeLevel < 0) {
  435. targetLevel += p_relativeLevel;
  436. if (targetLevel < 1) {
  437. // Invalid level.
  438. return false;
  439. }
  440. } else if (p_relativeLevel > 0) {
  441. targetLevel = -1;
  442. }
  443. }
  444. if (targetLevel == -1 || header.level == targetLevel) {
  445. if (firstHeader
  446. && (cursorLine == header.lineNumber
  447. || p_forward)
  448. && idx != -1) {
  449. // This header is not counted for the repeat.
  450. firstHeader = false;
  451. continue;
  452. }
  453. if (--p_repeat == 0) {
  454. // Found.
  455. break;
  456. }
  457. } else if (header.level < targetLevel) {
  458. // Stop by higher level.
  459. return false;
  460. }
  461. firstHeader = false;
  462. }
  463. if (targetIdx < 0 || targetIdx >= m_headers.size()) {
  464. return false;
  465. }
  466. // Jump to target header.
  467. int line = m_headers[targetIdx].lineNumber;
  468. if (line > -1) {
  469. QTextBlock block = document()->findBlockByLineNumber(line);
  470. if (block.isValid()) {
  471. cursor.setPosition(block.position());
  472. setTextCursor(cursor);
  473. return true;
  474. }
  475. }
  476. return false;
  477. }