| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517 |
- #include <QtWidgets>
- #include <QWebChannel>
- #include <QWebEngineView>
- #include <QFileInfo>
- #include <QXmlStreamReader>
- #include "vedittab.h"
- #include "vedit.h"
- #include "vdocument.h"
- #include "vnote.h"
- #include "utils/vutils.h"
- #include "vpreviewpage.h"
- #include "hgmarkdownhighlighter.h"
- #include "vconfigmanager.h"
- #include "vmarkdownconverter.h"
- #include "vnotebook.h"
- #include "vtoc.h"
- #include "vmdedit.h"
- #include "dialog/vfindreplacedialog.h"
- extern VConfigManager vconfig;
- VEditTab::VEditTab(VFile *p_file, OpenFileMode p_mode, QWidget *p_parent)
- : QStackedWidget(p_parent), m_file(p_file), isEditMode(false),
- mdConverterType(vconfig.getMdConverterType()), m_fileModified(false)
- {
- tableOfContent.filePath = p_file->retrivePath();
- curHeader.filePath = p_file->retrivePath();
- Q_ASSERT(!m_file->isOpened());
- m_file->open();
- setupUI();
- if (p_mode == OpenFileMode::Edit) {
- showFileEditMode();
- } else {
- showFileReadMode();
- }
- connect(qApp, &QApplication::focusChanged,
- this, &VEditTab::handleFocusChanged);
- }
- VEditTab::~VEditTab()
- {
- if (m_file) {
- m_file->close();
- }
- }
- void VEditTab::setupUI()
- {
- switch (m_file->getDocType()) {
- case DocType::Markdown:
- m_textEditor = new VMdEdit(m_file, this);
- connect(dynamic_cast<VMdEdit *>(m_textEditor), &VMdEdit::headersChanged,
- this, &VEditTab::updateTocFromHeaders);
- connect(dynamic_cast<VMdEdit *>(m_textEditor), &VMdEdit::statusChanged,
- this, &VEditTab::noticeStatusChanged);
- connect(m_textEditor, SIGNAL(curHeaderChanged(int, int)),
- this, SLOT(updateCurHeader(int, int)));
- connect(m_textEditor, &VEdit::textChanged,
- this, &VEditTab::handleTextChanged);
- m_textEditor->reloadFile();
- addWidget(m_textEditor);
- setupMarkdownPreview();
- break;
- case DocType::Html:
- m_textEditor = new VEdit(m_file, this);
- connect(m_textEditor, &VEdit::textChanged,
- this, &VEditTab::handleTextChanged);
- m_textEditor->reloadFile();
- addWidget(m_textEditor);
- webPreviewer = NULL;
- break;
- default:
- qWarning() << "error: unknown doc type" << int(m_file->getDocType());
- Q_ASSERT(false);
- }
- }
- void VEditTab::handleTextChanged()
- {
- if (m_fileModified) {
- return;
- }
- noticeStatusChanged();
- }
- void VEditTab::noticeStatusChanged()
- {
- m_fileModified = m_file->isModified();
- emit statusChanged();
- }
- void VEditTab::showFileReadMode()
- {
- qDebug() << "read" << m_file->getName();
- isEditMode = false;
- int outlineIndex = curHeader.m_outlineIndex;
- switch (m_file->getDocType()) {
- case DocType::Html:
- m_textEditor->setReadOnly(true);
- break;
- case DocType::Markdown:
- if (mdConverterType == MarkdownConverterType::Marked) {
- document.setText(m_file->getContent());
- updateTocFromHtml(document.getToc());
- } else {
- previewByConverter();
- }
- setCurrentWidget(webPreviewer);
- clearFindSelectionInWebView();
- scrollPreviewToHeader(outlineIndex);
- break;
- default:
- qWarning() << "error: unknown doc type" << int(m_file->getDocType());
- Q_ASSERT(false);
- }
- noticeStatusChanged();
- }
- void VEditTab::scrollPreviewToHeader(int p_outlineIndex)
- {
- Q_ASSERT(p_outlineIndex >= 0);
- if (p_outlineIndex < tableOfContent.headers.size()) {
- QString anchor = tableOfContent.headers[p_outlineIndex].anchor;
- qDebug() << "scroll preview to" << p_outlineIndex << anchor;
- if (!anchor.isEmpty()) {
- document.scrollToAnchor(anchor.mid(1));
- }
- }
- }
- void VEditTab::previewByConverter()
- {
- VMarkdownConverter mdConverter;
- QString &content = m_file->getContent();
- QString html = mdConverter.generateHtml(content, vconfig.getMarkdownExtensions());
- QRegularExpression tocExp("<p>\\[TOC\\]<\\/p>", QRegularExpression::CaseInsensitiveOption);
- QString toc = mdConverter.generateToc(content, vconfig.getMarkdownExtensions());
- processHoedownToc(toc);
- html.replace(tocExp, toc);
- document.setHtml(html);
- // Hoedown will add '\n' while Marked does not
- updateTocFromHtml(toc);
- }
- void VEditTab::processHoedownToc(QString &p_toc)
- {
- // Hoedown will add '\n'.
- p_toc.replace("\n", "");
- // Hoedown will translate `_` in title to `<em>`.
- p_toc.replace("<em>", "_");
- p_toc.replace("</em>", "_");
- }
- void VEditTab::showFileEditMode()
- {
- isEditMode = true;
- // beginEdit() may change curHeader.
- int outlineIndex = curHeader.m_outlineIndex;
- m_textEditor->beginEdit();
- setCurrentWidget(m_textEditor);
- if (m_file->getDocType() == DocType::Markdown) {
- dynamic_cast<VMdEdit *>(m_textEditor)->scrollToHeader(outlineIndex);
- }
- m_textEditor->setFocus();
- noticeStatusChanged();
- }
- bool VEditTab::closeFile(bool p_forced)
- {
- if (p_forced && isEditMode) {
- // Discard buffer content
- m_textEditor->reloadFile();
- m_textEditor->endEdit();
- showFileReadMode();
- } else {
- readFile();
- }
- return !isEditMode;
- }
- void VEditTab::editFile()
- {
- if (isEditMode) {
- return;
- }
- showFileEditMode();
- }
- void VEditTab::readFile()
- {
- if (!isEditMode) {
- return;
- }
- if (m_textEditor->isModified()) {
- // Prompt to save the changes
- int ret = VUtils::showMessage(QMessageBox::Information, tr("Information"),
- QString("Note %1 has been modified.").arg(m_file->getName()),
- tr("Do you want to save your changes?"),
- QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel,
- QMessageBox::Save, this);
- switch (ret) {
- case QMessageBox::Save:
- saveFile();
- // Fall through
- case QMessageBox::Discard:
- m_textEditor->reloadFile();
- break;
- case QMessageBox::Cancel:
- // Nothing to do if user cancel this action
- return;
- default:
- qWarning() << "error: wrong return value from QMessageBox:" << ret;
- return;
- }
- }
- m_textEditor->endEdit();
- showFileReadMode();
- }
- bool VEditTab::saveFile()
- {
- bool ret;
- if (!isEditMode || !m_textEditor->isModified()) {
- return true;
- }
- // Make sure the file already exists. Temporary deal with cases when user delete or move
- // a file.
- QString filePath = m_file->retrivePath();
- if (!QFile(filePath).exists()) {
- qWarning() << filePath << "being written has been removed";
- VUtils::showMessage(QMessageBox::Warning, tr("Warning"), tr("Fail to save note"),
- QString("%1 being written has been removed.").arg(filePath),
- QMessageBox::Ok, QMessageBox::Ok, this);
- return false;
- }
- m_textEditor->saveFile();
- ret = m_file->save();
- if (!ret) {
- VUtils::showMessage(QMessageBox::Warning, tr("Warning"), tr("Fail to save note"),
- QString("Fail to write to disk when saving a note. Please try it again."),
- QMessageBox::Ok, QMessageBox::Ok, this);
- m_textEditor->setModified(true);
- }
- noticeStatusChanged();
- return ret;
- }
- void VEditTab::setupMarkdownPreview()
- {
- webPreviewer = new QWebEngineView(this);
- VPreviewPage *page = new VPreviewPage(this);
- webPreviewer->setPage(page);
- QWebChannel *channel = new QWebChannel(this);
- channel->registerObject(QStringLiteral("content"), &document);
- connect(&document, &VDocument::tocChanged,
- this, &VEditTab::updateTocFromHtml);
- connect(&document, SIGNAL(headerChanged(const QString&)),
- this, SLOT(updateCurHeader(const QString &)));
- page->setWebChannel(channel);
- if (mdConverterType == MarkdownConverterType::Marked) {
- webPreviewer->setHtml(VNote::templateHtml,
- QUrl::fromLocalFile(m_file->retriveBasePath() + QDir::separator()));
- } else {
- webPreviewer->setHtml(VNote::preTemplateHtml + VNote::postTemplateHtml,
- QUrl::fromLocalFile(m_file->retriveBasePath() + QDir::separator()));
- }
- addWidget(webPreviewer);
- }
- void VEditTab::focusTab()
- {
- currentWidget()->setFocus();
- emit getFocused();
- }
- void VEditTab::handleFocusChanged(QWidget * /* old */, QWidget *now)
- {
- if (isChild(now)) {
- emit getFocused();
- }
- }
- void VEditTab::updateTocFromHtml(const QString &tocHtml)
- {
- if (isEditMode) {
- return;
- }
- tableOfContent.type = VHeaderType::Anchor;
- QVector<VHeader> &headers = tableOfContent.headers;
- headers.clear();
- if (!tocHtml.isEmpty()) {
- QXmlStreamReader xml(tocHtml);
- if (xml.readNextStartElement()) {
- if (xml.name() == "ul") {
- parseTocUl(xml, headers, 1);
- } else {
- qWarning() << "error: TOC HTML does not start with <ul>";
- }
- }
- if (xml.hasError()) {
- qWarning() << "error: fail to parse TOC in HTML";
- return;
- }
- }
- tableOfContent.filePath = m_file->retrivePath();
- tableOfContent.valid = true;
- emit outlineChanged(tableOfContent);
- }
- void VEditTab::updateTocFromHeaders(const QVector<VHeader> &headers)
- {
- if (!isEditMode) {
- return;
- }
- tableOfContent.type = VHeaderType::LineNumber;
- tableOfContent.headers = headers;
- tableOfContent.filePath = m_file->retrivePath();
- tableOfContent.valid = true;
- emit outlineChanged(tableOfContent);
- }
- void VEditTab::parseTocUl(QXmlStreamReader &xml, QVector<VHeader> &headers, int level)
- {
- Q_ASSERT(xml.isStartElement() && xml.name() == "ul");
- while (xml.readNextStartElement()) {
- if (xml.name() == "li") {
- parseTocLi(xml, headers, level);
- } else {
- qWarning() << "error: TOC HTML <ul> should contain <li>" << xml.name();
- break;
- }
- }
- }
- void VEditTab::parseTocLi(QXmlStreamReader &xml, QVector<VHeader> &headers, int level)
- {
- Q_ASSERT(xml.isStartElement() && xml.name() == "li");
- if (xml.readNextStartElement()) {
- if (xml.name() == "a") {
- QString anchor = xml.attributes().value("href").toString();
- QString name;
- if (xml.readNext()) {
- if (xml.tokenString() == "Characters") {
- name = xml.text().toString();
- } else if (!xml.isEndElement()) {
- qWarning() << "error: TOC HTML <a> should be ended by </a>" << xml.name();
- return;
- }
- VHeader header(level, name, anchor, -1);
- headers.append(header);
- } else {
- // Error
- return;
- }
- } else if (xml.name() == "ul") {
- // Such as header 3 under header 1 directly
- VHeader header(level, "[Empty]", "#", -1);
- headers.append(header);
- parseTocUl(xml, headers, level + 1);
- } else {
- qWarning() << "error: TOC HTML <li> should contain <a> or <ul>" << xml.name();
- return;
- }
- }
- while (xml.readNext()) {
- if (xml.isEndElement()) {
- if (xml.name() == "li") {
- return;
- }
- continue;
- }
- if (xml.name() == "ul") {
- // Nested unordered list
- parseTocUl(xml, headers, level + 1);
- } else {
- return;
- }
- }
- }
- void VEditTab::requestUpdateCurHeader()
- {
- emit curHeaderChanged(curHeader);
- }
- void VEditTab::requestUpdateOutline()
- {
- emit outlineChanged(tableOfContent);
- }
- void VEditTab::scrollToAnchor(const VAnchor &anchor)
- {
- if (anchor == curHeader) {
- return;
- }
- curHeader = anchor;
- if (isEditMode) {
- if (anchor.lineNumber > -1) {
- m_textEditor->scrollToLine(anchor.lineNumber);
- }
- } else {
- if (!anchor.anchor.isEmpty()) {
- document.scrollToAnchor(anchor.anchor.mid(1));
- }
- }
- }
- void VEditTab::updateCurHeader(const QString &anchor)
- {
- if (isEditMode || curHeader.anchor.mid(1) == anchor) {
- return;
- }
- curHeader = VAnchor(m_file->retrivePath(), "#" + anchor, -1);
- if (!anchor.isEmpty()) {
- const QVector<VHeader> &headers = tableOfContent.headers;
- for (int i = 0; i < headers.size(); ++i) {
- if (headers[i].anchor == curHeader.anchor) {
- curHeader.m_outlineIndex = i;
- break;
- }
- }
- emit curHeaderChanged(curHeader);
- }
- }
- void VEditTab::updateCurHeader(int p_lineNumber, int p_outlineIndex)
- {
- if (!isEditMode || curHeader.lineNumber == p_lineNumber) {
- return;
- }
- curHeader = VAnchor(m_file->retrivePath(), "", p_lineNumber);
- curHeader.m_outlineIndex = p_outlineIndex;
- if (p_lineNumber > -1) {
- emit curHeaderChanged(curHeader);
- }
- }
- void VEditTab::insertImage()
- {
- qDebug() << "insert image";
- if (!isEditMode) {
- return;
- }
- m_textEditor->insertImage();
- }
- void VEditTab::findText(const QString &p_text, uint p_options, bool p_peek,
- bool p_forward)
- {
- if (isEditMode || !webPreviewer) {
- m_textEditor->findText(p_text, p_options, p_peek, p_forward);
- } else {
- findTextInWebView(p_text, p_options, p_peek, p_forward);
- }
- }
- void VEditTab::replaceText(const QString &p_text, uint p_options,
- const QString &p_replaceText, bool p_findNext)
- {
- if (isEditMode) {
- m_textEditor->replaceText(p_text, p_options, p_replaceText, p_findNext);
- }
- }
- void VEditTab::replaceTextAll(const QString &p_text, uint p_options,
- const QString &p_replaceText)
- {
- if (isEditMode) {
- m_textEditor->replaceTextAll(p_text, p_options, p_replaceText);
- }
- }
- void VEditTab::findTextInWebView(const QString &p_text, uint p_options,
- bool p_peek, bool p_forward)
- {
- Q_ASSERT(webPreviewer);
- QWebEnginePage::FindFlags flags;
- if (p_options & FindOption::CaseSensitive) {
- flags |= QWebEnginePage::FindCaseSensitively;
- }
- if (!p_forward) {
- flags |= QWebEnginePage::FindBackward;
- }
- webPreviewer->findText(p_text, flags);
- }
- QString VEditTab::getSelectedText() const
- {
- if (isEditMode || !webPreviewer) {
- QTextCursor cursor = m_textEditor->textCursor();
- return cursor.selectedText();
- } else {
- return webPreviewer->selectedText();
- }
- }
- void VEditTab::clearFindSelectionInWebView()
- {
- if (webPreviewer) {
- webPreviewer->findText("");
- }
- }
|