vmdedit.cpp 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451
  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(this, &VMdEdit::cursorPositionChanged,
  46. this, &VMdEdit::updateCurHeader);
  47. connect(this, &VMdEdit::selectionChanged,
  48. this, &VMdEdit::handleSelectionChanged);
  49. connect(QApplication::clipboard(), &QClipboard::changed,
  50. this, &VMdEdit::handleClipboardChanged);
  51. updateFontAndPalette();
  52. updateConfig();
  53. }
  54. void VMdEdit::updateFontAndPalette()
  55. {
  56. setFont(vconfig.getMdEditFont());
  57. setPalette(vconfig.getMdEditPalette());
  58. }
  59. void VMdEdit::beginEdit()
  60. {
  61. updateFontAndPalette();
  62. updateConfig();
  63. Q_ASSERT(m_file->getContent() == toPlainTextWithoutImg());
  64. initInitImages();
  65. m_imagePreviewer->refresh();
  66. setReadOnly(false);
  67. setModified(false);
  68. // Request update outline.
  69. generateEditOutline();
  70. }
  71. void VMdEdit::endEdit()
  72. {
  73. setReadOnly(true);
  74. clearUnusedImages();
  75. }
  76. void VMdEdit::saveFile()
  77. {
  78. if (!document()->isModified()) {
  79. return;
  80. }
  81. m_file->setContent(toPlainTextWithoutImg());
  82. document()->setModified(false);
  83. }
  84. void VMdEdit::reloadFile()
  85. {
  86. const QString &content = m_file->getContent();
  87. V_ASSERT(content.indexOf(QChar::ObjectReplacementCharacter) == -1);
  88. setPlainText(content);
  89. setModified(false);
  90. }
  91. void VMdEdit::keyPressEvent(QKeyEvent *event)
  92. {
  93. if (m_editOps->handleKeyPressEvent(event)) {
  94. return;
  95. }
  96. VEdit::keyPressEvent(event);
  97. }
  98. bool VMdEdit::canInsertFromMimeData(const QMimeData *source) const
  99. {
  100. return source->hasImage() || source->hasUrls()
  101. || VEdit::canInsertFromMimeData(source);
  102. }
  103. void VMdEdit::insertFromMimeData(const QMimeData *source)
  104. {
  105. VSelectDialog dialog(tr("Insert From Clipboard"), this);
  106. dialog.addSelection(tr("Insert As Image"), 0);
  107. dialog.addSelection(tr("Insert As Text"), 1);
  108. if (source->hasImage()) {
  109. // Image data in the clipboard
  110. if (source->hasText()) {
  111. if (dialog.exec() == QDialog::Accepted) {
  112. if (dialog.getSelection() == 1) {
  113. // Insert as text.
  114. Q_ASSERT(source->hasText() && source->hasImage());
  115. VEdit::insertFromMimeData(source);
  116. return;
  117. }
  118. } else {
  119. return;
  120. }
  121. }
  122. m_editOps->insertImageFromMimeData(source);
  123. return;
  124. } else if (source->hasUrls()) {
  125. QList<QUrl> urls = source->urls();
  126. if (urls.size() == 1 && VUtils::isImageURL(urls[0])) {
  127. if (dialog.exec() == QDialog::Accepted) {
  128. // FIXME: After calling dialog.exec(), source->hasUrl() returns false.
  129. if (dialog.getSelection() == 0) {
  130. // Insert as image.
  131. m_editOps->insertImageFromURL(urls[0]);
  132. return;
  133. }
  134. QMimeData newSource;
  135. newSource.setUrls(urls);
  136. VEdit::insertFromMimeData(&newSource);
  137. return;
  138. } else {
  139. return;
  140. }
  141. }
  142. } else if (source->hasText()) {
  143. QString text = source->text();
  144. if (VUtils::isImageURLText(text)) {
  145. // The text is a URL to an image.
  146. if (dialog.exec() == QDialog::Accepted) {
  147. if (dialog.getSelection() == 0) {
  148. // Insert as image.
  149. QUrl url(text);
  150. if (url.isValid()) {
  151. m_editOps->insertImageFromURL(QUrl(text));
  152. }
  153. return;
  154. }
  155. } else {
  156. return;
  157. }
  158. }
  159. Q_ASSERT(source->hasText());
  160. }
  161. VEdit::insertFromMimeData(source);
  162. }
  163. void VMdEdit::imageInserted(const QString &p_path)
  164. {
  165. ImageLink link;
  166. link.m_path = p_path;
  167. link.m_type = ImageLink::LocalRelativeInternal;
  168. m_insertedImages.append(link);
  169. }
  170. void VMdEdit::initInitImages()
  171. {
  172. m_initImages = VUtils::fetchImagesFromMarkdownFile(m_file,
  173. ImageLink::LocalRelativeInternal);
  174. }
  175. void VMdEdit::clearUnusedImages()
  176. {
  177. QVector<ImageLink> images = VUtils::fetchImagesFromMarkdownFile(m_file,
  178. ImageLink::LocalRelativeInternal);
  179. if (!m_insertedImages.isEmpty()) {
  180. for (int i = 0; i < m_insertedImages.size(); ++i) {
  181. const ImageLink &link = m_insertedImages[i];
  182. V_ASSERT(link.m_type == ImageLink::LocalRelativeInternal);
  183. int j;
  184. for (j = 0; j < images.size(); ++j) {
  185. if (link.m_path == images[j].m_path) {
  186. break;
  187. }
  188. }
  189. // This inserted image is no longer in the file.
  190. if (j == images.size()) {
  191. if (!QFile(link.m_path).remove()) {
  192. qWarning() << "fail to delete unused inserted image" << link.m_path;
  193. } else {
  194. qDebug() << "delete unused inserted image" << link.m_path;
  195. }
  196. }
  197. }
  198. m_insertedImages.clear();
  199. }
  200. for (int i = 0; i < m_initImages.size(); ++i) {
  201. const ImageLink &link = m_initImages[i];
  202. V_ASSERT(link.m_type == ImageLink::LocalRelativeInternal);
  203. int j;
  204. for (j = 0; j < images.size(); ++j) {
  205. if (link.m_path == images[j].m_path) {
  206. break;
  207. }
  208. }
  209. // Original local relative image is no longer in the file.
  210. if (j == images.size()) {
  211. if (!QFile(link.m_path).remove()) {
  212. qWarning() << "fail to delete unused original image" << link.m_path;
  213. } else {
  214. qDebug() << "delete unused original image" << link.m_path;
  215. }
  216. }
  217. }
  218. m_initImages.clear();
  219. }
  220. void VMdEdit::updateCurHeader()
  221. {
  222. if (m_headers.isEmpty()) {
  223. return;
  224. }
  225. int curLine = textCursor().block().firstLineNumber();
  226. int i = 0;
  227. for (i = m_headers.size() - 1; i >= 0; --i) {
  228. if (!m_headers[i].isEmpty()) {
  229. if (m_headers[i].lineNumber <= curLine) {
  230. break;
  231. }
  232. }
  233. }
  234. if (i == -1) {
  235. emit curHeaderChanged(VAnchor(m_file, "", -1, -1));
  236. return;
  237. }
  238. V_ASSERT(m_headers[i].index == i);
  239. emit curHeaderChanged(VAnchor(m_file, "", m_headers[i].lineNumber, m_headers[i].index));
  240. }
  241. void VMdEdit::generateEditOutline()
  242. {
  243. QTextDocument *doc = document();
  244. m_headers.clear();
  245. QVector<VHeader> headers;
  246. // Assume that each block contains only one line
  247. // Only support # syntax for now
  248. QRegExp headerReg("(#{1,6})\\s*(\\S.*)"); // Need to trim the spaces
  249. int baseLevel = -1;
  250. for (QTextBlock block = doc->begin(); block != doc->end(); block = block.next()) {
  251. V_ASSERT(block.lineCount() == 1);
  252. if ((block.userState() == HighlightBlockState::Normal) &&
  253. headerReg.exactMatch(block.text())) {
  254. int level = headerReg.cap(1).length();
  255. VHeader header(level, headerReg.cap(2).trimmed(),
  256. "", block.firstLineNumber(), headers.size());
  257. headers.append(header);
  258. if (baseLevel == -1) {
  259. baseLevel = level;
  260. } else if (baseLevel > level) {
  261. baseLevel = level;
  262. }
  263. }
  264. }
  265. int curLevel = baseLevel - 1;
  266. for (auto & item : headers) {
  267. while (item.level > curLevel + 1) {
  268. curLevel += 1;
  269. // Insert empty level which is an invalid header.
  270. m_headers.append(VHeader(curLevel, c_emptyHeaderName, "", -1, m_headers.size()));
  271. }
  272. item.index = m_headers.size();
  273. m_headers.append(item);
  274. curLevel = item.level;
  275. }
  276. emit headersChanged(m_headers);
  277. updateCurHeader();
  278. }
  279. void VMdEdit::scrollToHeader(const VAnchor &p_anchor)
  280. {
  281. if (p_anchor.lineNumber == -1
  282. || p_anchor.m_outlineIndex < 0
  283. || p_anchor.m_outlineIndex >= m_headers.size()) {
  284. return;
  285. }
  286. scrollToLine(p_anchor.lineNumber);
  287. }
  288. QString VMdEdit::toPlainTextWithoutImg() const
  289. {
  290. QString text = toPlainText();
  291. int start = 0;
  292. do {
  293. int index = text.indexOf(QChar::ObjectReplacementCharacter, start);
  294. if (index == -1) {
  295. break;
  296. }
  297. start = removeObjectReplacementLine(text, index);
  298. } while (start > -1 && start < text.size());
  299. return text;
  300. }
  301. int VMdEdit::removeObjectReplacementLine(QString &p_text, int p_index) const
  302. {
  303. Q_ASSERT(p_text.size() > p_index && p_text.at(p_index) == QChar::ObjectReplacementCharacter);
  304. int prevLineIdx = p_text.lastIndexOf('\n', p_index);
  305. if (prevLineIdx == -1) {
  306. prevLineIdx = 0;
  307. }
  308. // Remove [\n....?]
  309. p_text.remove(prevLineIdx, p_index - prevLineIdx + 1);
  310. return prevLineIdx - 1;
  311. }
  312. void VMdEdit::handleSelectionChanged()
  313. {
  314. if (!vconfig.getEnablePreviewImages()) {
  315. return;
  316. }
  317. QString text = textCursor().selectedText();
  318. if (text.isEmpty() && !m_imagePreviewer->isPreviewEnabled()) {
  319. m_imagePreviewer->enableImagePreview();
  320. } else if (m_imagePreviewer->isPreviewEnabled()) {
  321. if (text.trimmed() == QString(QChar::ObjectReplacementCharacter)) {
  322. // Select the image and some whitespaces.
  323. // We can let the user copy the image.
  324. return;
  325. } else if (text.contains(QChar::ObjectReplacementCharacter)) {
  326. m_imagePreviewer->disableImagePreview();
  327. }
  328. }
  329. }
  330. void VMdEdit::handleClipboardChanged(QClipboard::Mode p_mode)
  331. {
  332. if (!hasFocus()) {
  333. return;
  334. }
  335. if (p_mode == QClipboard::Clipboard) {
  336. QClipboard *clipboard = QApplication::clipboard();
  337. const QMimeData *mimeData = clipboard->mimeData();
  338. if (mimeData->hasText()) {
  339. QString text = mimeData->text();
  340. if (clipboard->ownsClipboard() &&
  341. (text.trimmed() == QString(QChar::ObjectReplacementCharacter))) {
  342. QImage image = selectedImage();
  343. clipboard->clear(QClipboard::Clipboard);
  344. if (!image.isNull()) {
  345. clipboard->setImage(image, QClipboard::Clipboard);
  346. }
  347. }
  348. }
  349. }
  350. }
  351. QImage VMdEdit::selectedImage()
  352. {
  353. QImage image;
  354. QTextCursor cursor = textCursor();
  355. if (!cursor.hasSelection()) {
  356. return image;
  357. }
  358. int start = cursor.selectionStart();
  359. int end = cursor.selectionEnd();
  360. QTextDocument *doc = document();
  361. QTextBlock startBlock = doc->findBlock(start);
  362. QTextBlock endBlock = doc->findBlock(end);
  363. QTextBlock block = startBlock;
  364. while (block.isValid()) {
  365. if (m_imagePreviewer->isImagePreviewBlock(block)) {
  366. image = m_imagePreviewer->fetchCachedImageFromPreviewBlock(block);
  367. break;
  368. }
  369. if (block == endBlock) {
  370. break;
  371. }
  372. block = block.next();
  373. }
  374. return image;
  375. }
  376. void VMdEdit::resizeEvent(QResizeEvent *p_event)
  377. {
  378. m_imagePreviewer->update();
  379. VEdit::resizeEvent(p_event);
  380. }
  381. const QVector<VHeader> &VMdEdit::getHeaders() const
  382. {
  383. return m_headers;
  384. }