vmdedit.cpp 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424
  1. #include <QtWidgets>
  2. #include "vmdedit.h"
  3. #include "hgmarkdownhighlighter.h"
  4. #include "vmdeditoperations.h"
  5. #include "vnote.h"
  6. #include "vconfigmanager.h"
  7. #include "vtoc.h"
  8. #include "utils/vutils.h"
  9. extern VConfigManager vconfig;
  10. enum ImageProperty { ImagePath = 1 };
  11. VMdEdit::VMdEdit(VFile *p_file, QWidget *p_parent)
  12. : VEdit(p_file, p_parent), m_mdHighlighter(NULL)
  13. {
  14. Q_ASSERT(p_file->getDocType() == DocType::Markdown);
  15. setAcceptRichText(false);
  16. m_mdHighlighter = new HGMarkdownHighlighter(vconfig.getMdHighlightingStyles(),
  17. 500, document());
  18. connect(m_mdHighlighter, &HGMarkdownHighlighter::highlightCompleted,
  19. this, &VMdEdit::generateEditOutline);
  20. connect(m_mdHighlighter, &HGMarkdownHighlighter::imageBlocksUpdated,
  21. this, &VMdEdit::updateImageBlocks);
  22. m_editOps = new VMdEditOperations(this, m_file);
  23. connect(this, &VMdEdit::cursorPositionChanged,
  24. this, &VMdEdit::updateCurHeader);
  25. m_editOps->updateTabSettings();
  26. updateFontAndPalette();
  27. }
  28. void VMdEdit::updateFontAndPalette()
  29. {
  30. setFont(vconfig.getMdEditFont());
  31. setPalette(vconfig.getMdEditPalette());
  32. }
  33. void VMdEdit::beginEdit()
  34. {
  35. m_editOps->updateTabSettings();
  36. updateFontAndPalette();
  37. setFont(vconfig.getMdEditFont());
  38. Q_ASSERT(m_file->getContent() == toPlainTextWithoutImg());
  39. initInitImages();
  40. setReadOnly(false);
  41. setModified(false);
  42. // Request update outline.
  43. generateEditOutline();
  44. }
  45. void VMdEdit::endEdit()
  46. {
  47. setReadOnly(true);
  48. clearUnusedImages();
  49. }
  50. void VMdEdit::saveFile()
  51. {
  52. if (!document()->isModified()) {
  53. return;
  54. }
  55. m_file->setContent(toPlainTextWithoutImg());
  56. document()->setModified(false);
  57. }
  58. void VMdEdit::reloadFile()
  59. {
  60. QString &content = m_file->getContent();
  61. Q_ASSERT(content.indexOf(QChar::ObjectReplacementCharacter) == -1);
  62. setPlainText(content);
  63. setModified(false);
  64. }
  65. void VMdEdit::keyPressEvent(QKeyEvent *event)
  66. {
  67. if (m_editOps->handleKeyPressEvent(event)) {
  68. return;
  69. }
  70. VEdit::keyPressEvent(event);
  71. }
  72. bool VMdEdit::canInsertFromMimeData(const QMimeData *source) const
  73. {
  74. return source->hasImage() || source->hasUrls()
  75. || VEdit::canInsertFromMimeData(source);
  76. }
  77. void VMdEdit::insertFromMimeData(const QMimeData *source)
  78. {
  79. if (source->hasImage()) {
  80. // Image data in the clipboard
  81. bool ret = m_editOps->insertImageFromMimeData(source);
  82. if (ret) {
  83. return;
  84. }
  85. } else if (source->hasUrls()) {
  86. // Paste an image file
  87. bool ret = m_editOps->insertURLFromMimeData(source);
  88. if (ret) {
  89. return;
  90. }
  91. }
  92. VEdit::insertFromMimeData(source);
  93. }
  94. void VMdEdit::imageInserted(const QString &p_name)
  95. {
  96. m_insertedImages.append(p_name);
  97. }
  98. void VMdEdit::initInitImages()
  99. {
  100. m_initImages = VUtils::imagesFromMarkdownFile(m_file->retrivePath());
  101. }
  102. void VMdEdit::clearUnusedImages()
  103. {
  104. QVector<QString> images = VUtils::imagesFromMarkdownFile(m_file->retrivePath());
  105. if (!m_insertedImages.isEmpty()) {
  106. QVector<QString> imageNames(images.size());
  107. for (int i = 0; i < imageNames.size(); ++i) {
  108. imageNames[i] = VUtils::fileNameFromPath(images[i]);
  109. }
  110. QDir dir = QDir(m_file->retriveImagePath());
  111. for (int i = 0; i < m_insertedImages.size(); ++i) {
  112. QString name = m_insertedImages[i];
  113. int j;
  114. for (j = 0; j < imageNames.size(); ++j) {
  115. if (name == imageNames[j]) {
  116. break;
  117. }
  118. }
  119. // Delete it
  120. if (j == imageNames.size()) {
  121. QString imagePath = dir.filePath(name);
  122. QFile(imagePath).remove();
  123. qDebug() << "delete inserted image" << imagePath;
  124. }
  125. }
  126. m_insertedImages.clear();
  127. }
  128. for (int i = 0; i < m_initImages.size(); ++i) {
  129. QString imagePath = m_initImages[i];
  130. int j;
  131. for (j = 0; j < images.size(); ++j) {
  132. if (imagePath == images[j]) {
  133. break;
  134. }
  135. }
  136. // Delete it
  137. if (j == images.size()) {
  138. QFile(imagePath).remove();
  139. qDebug() << "delete existing image" << imagePath;
  140. }
  141. }
  142. m_initImages.clear();
  143. }
  144. void VMdEdit::updateCurHeader()
  145. {
  146. int curHeader = 0;
  147. QTextCursor cursor(this->textCursor());
  148. int curLine = cursor.block().firstLineNumber();
  149. int i = 0;
  150. for (i = m_headers.size() - 1; i >= 0; --i) {
  151. if (m_headers[i].lineNumber <= curLine) {
  152. curHeader = m_headers[i].lineNumber;
  153. break;
  154. }
  155. }
  156. emit curHeaderChanged(curHeader, i == -1 ? 0 : i);
  157. }
  158. void VMdEdit::generateEditOutline()
  159. {
  160. QTextDocument *doc = document();
  161. m_headers.clear();
  162. // Assume that each block contains only one line
  163. // Only support # syntax for now
  164. QRegExp headerReg("(#{1,6})\\s*(\\S.*)"); // Need to trim the spaces
  165. for (QTextBlock block = doc->begin(); block != doc->end(); block = block.next()) {
  166. Q_ASSERT(block.lineCount() == 1);
  167. if ((block.userState() == HighlightBlockState::BlockNormal) &&
  168. headerReg.exactMatch(block.text())) {
  169. VHeader header(headerReg.cap(1).length(),
  170. headerReg.cap(2).trimmed(), "", block.firstLineNumber());
  171. m_headers.append(header);
  172. }
  173. }
  174. emit headersChanged(m_headers);
  175. updateCurHeader();
  176. }
  177. void VMdEdit::scrollToHeader(int p_headerIndex)
  178. {
  179. Q_ASSERT(p_headerIndex >= 0);
  180. if (p_headerIndex < m_headers.size()) {
  181. int line = m_headers[p_headerIndex].lineNumber;
  182. qDebug() << "scroll editor to" << p_headerIndex << "line" << line;
  183. scrollToLine(line);
  184. }
  185. }
  186. void VMdEdit::updateImageBlocks(QSet<int> p_imageBlocks)
  187. {
  188. // We need to handle blocks backward to avoid shifting all the following blocks.
  189. // Inserting the preview image block may cause highlighter to emit signal again.
  190. QList<int> blockList = p_imageBlocks.toList();
  191. std::sort(blockList.begin(), blockList.end(), std::greater<int>());
  192. auto it = blockList.begin();
  193. while (it != blockList.end()) {
  194. previewImageOfBlock(*it);
  195. ++it;
  196. }
  197. // Clean up un-referenced QChar::ObjectReplacementCharacter.
  198. clearOrphanImagePreviewBlock();
  199. emit statusChanged();
  200. }
  201. void VMdEdit::clearOrphanImagePreviewBlock()
  202. {
  203. QTextDocument *doc = document();
  204. QTextBlock block = doc->begin();
  205. while (block.isValid()) {
  206. if (isOrphanImagePreviewBlock(block)) {
  207. qDebug() << "remove orphan image preview block" << block.blockNumber();
  208. QTextBlock nextBlock = block.next();
  209. removeBlock(block);
  210. block = nextBlock;
  211. } else {
  212. block = block.next();
  213. }
  214. }
  215. }
  216. bool VMdEdit::isOrphanImagePreviewBlock(QTextBlock p_block)
  217. {
  218. if (isImagePreviewBlock(p_block)) {
  219. // It is an orphan image preview block if previous block is not
  220. // a block need to preview (containing exactly one image).
  221. QTextBlock prevBlock = p_block.previous();
  222. if (prevBlock.isValid()) {
  223. if (fetchImageToPreview(prevBlock.text()).isEmpty()) {
  224. return true;
  225. } else {
  226. return false;
  227. }
  228. } else {
  229. return true;
  230. }
  231. }
  232. return false;
  233. }
  234. QString VMdEdit::fetchImageToPreview(const QString &p_text)
  235. {
  236. QRegExp regExp("\\!\\[[^\\]]*\\]\\((images/[^/\\)]+)\\)");
  237. int index = regExp.indexIn(p_text);
  238. if (index == -1) {
  239. return QString();
  240. }
  241. int lastIndex = regExp.lastIndexIn(p_text);
  242. if (lastIndex != index) {
  243. return QString();
  244. }
  245. return regExp.capturedTexts()[1];
  246. }
  247. void VMdEdit::previewImageOfBlock(int p_block)
  248. {
  249. QTextDocument *doc = document();
  250. QTextBlock block = doc->findBlockByNumber(p_block);
  251. if (!block.isValid()) {
  252. return;
  253. }
  254. QString text = block.text();
  255. QString imageLink = fetchImageToPreview(text);
  256. if (imageLink.isEmpty()) {
  257. return;
  258. }
  259. QString imagePath = QDir(m_file->retriveBasePath()).filePath(imageLink);
  260. qDebug() << "block" << p_block << "image" << imagePath;
  261. if (isImagePreviewBlock(p_block + 1)) {
  262. updateImagePreviewBlock(p_block + 1, imagePath);
  263. return;
  264. }
  265. insertImagePreviewBlock(p_block, imagePath);
  266. }
  267. bool VMdEdit::isImagePreviewBlock(int p_block)
  268. {
  269. QTextDocument *doc = document();
  270. QTextBlock block = doc->findBlockByNumber(p_block);
  271. if (!block.isValid()) {
  272. return false;
  273. }
  274. QString text = block.text().trimmed();
  275. return text == QString(QChar::ObjectReplacementCharacter);
  276. }
  277. bool VMdEdit::isImagePreviewBlock(QTextBlock p_block)
  278. {
  279. if (!p_block.isValid()) {
  280. return false;
  281. }
  282. QString text = p_block.text().trimmed();
  283. return text == QString(QChar::ObjectReplacementCharacter);
  284. }
  285. void VMdEdit::insertImagePreviewBlock(int p_block, const QString &p_image)
  286. {
  287. QTextDocument *doc = document();
  288. QImage image(p_image);
  289. if (image.isNull()) {
  290. return;
  291. }
  292. // Store current status.
  293. bool modified = isModified();
  294. int pos = textCursor().position();
  295. QTextCursor cursor(doc->findBlockByNumber(p_block));
  296. cursor.beginEditBlock();
  297. cursor.movePosition(QTextCursor::EndOfBlock);
  298. cursor.insertBlock();
  299. QTextImageFormat imgFormat;
  300. imgFormat.setName(p_image);
  301. imgFormat.setProperty(ImagePath, p_image);
  302. cursor.insertImage(imgFormat);
  303. Q_ASSERT(cursor.block().text().at(0) == QChar::ObjectReplacementCharacter);
  304. cursor.endEditBlock();
  305. QTextCursor tmp = textCursor();
  306. tmp.setPosition(pos);
  307. setTextCursor(tmp);
  308. setModified(modified);
  309. emit statusChanged();
  310. }
  311. void VMdEdit::updateImagePreviewBlock(int p_block, const QString &p_image)
  312. {
  313. Q_ASSERT(isImagePreviewBlock(p_block));
  314. QTextDocument *doc = document();
  315. QTextBlock block = doc->findBlockByNumber(p_block);
  316. if (!block.isValid()) {
  317. return;
  318. }
  319. QTextCursor cursor(block);
  320. int shift = block.text().indexOf(QChar::ObjectReplacementCharacter);
  321. if (shift > 0) {
  322. cursor.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, shift + 1);
  323. }
  324. QTextImageFormat format = cursor.charFormat().toImageFormat();
  325. Q_ASSERT(format.isValid());
  326. QString curPath = format.property(ImagePath).toString();
  327. if (curPath == p_image) {
  328. return;
  329. }
  330. // Update it with the new image.
  331. QImage image(p_image);
  332. if (image.isNull()) {
  333. // Delete current preview block.
  334. removeBlock(block);
  335. qDebug() << "remove invalid image in block" << p_block;
  336. return;
  337. }
  338. format.setName(p_image);
  339. qDebug() << "update block" << p_block << "to image" << p_image;
  340. }
  341. void VMdEdit::removeBlock(QTextBlock p_block)
  342. {
  343. QTextCursor cursor(p_block);
  344. cursor.select(QTextCursor::BlockUnderCursor);
  345. cursor.removeSelectedText();
  346. }
  347. QString VMdEdit::toPlainTextWithoutImg() const
  348. {
  349. QString text = toPlainText();
  350. int start = 0;
  351. do {
  352. int index = text.indexOf(QChar::ObjectReplacementCharacter, start);
  353. if (index == -1) {
  354. break;
  355. }
  356. start = removeObjectReplacementLine(text, index);
  357. } while (start < text.size());
  358. return text;
  359. }
  360. int VMdEdit::removeObjectReplacementLine(QString &p_text, int p_index) const
  361. {
  362. Q_ASSERT(p_text.size() > p_index && p_text.at(p_index) == QChar::ObjectReplacementCharacter);
  363. Q_ASSERT(p_text.at(p_index + 1) == '\n');
  364. int prevLineIdx = p_text.lastIndexOf('\n', p_index);
  365. if (prevLineIdx == -1) {
  366. prevLineIdx = 0;
  367. }
  368. // Remove \n[....?\n]
  369. p_text.remove(prevLineIdx + 1, p_index - prevLineIdx + 1);
  370. return prevLineIdx;
  371. }