diff --git a/src/resources/markdown_template.js b/src/resources/markdown_template.js index 9ab07da7..0e4fc583 100644 --- a/src/resources/markdown_template.js +++ b/src/resources/markdown_template.js @@ -701,6 +701,8 @@ var insertImageCaption = function() { // markdown-specifi handle logics, such as Mermaid, MathJax. var finishLogics = function() { content.finishLogics(); + + calculateWordCount(); }; // Escape @text to Html. @@ -1107,3 +1109,52 @@ var postProcessMathJax = function() { finishLogics(); }; + +function getNodeText(el) { + ret = ""; + var length = el.childNodes.length; + for(var i = 0; i < length; i++) { + var node = el.childNodes[i]; + if(node.nodeType != 8) { + ret += node.nodeType != 1 ? node.nodeValue : getNodeText(node); + } + } + + return ret; +} + +var calculateWordCount = function() { + var words = getNodeText(placeholder); + + // Char without spaces. + var cns = 0; + var wc = 0; + var cc = words.length; + // 0 - not in word; + // 1 - in English word; + // 2 - in non-English word; + var state = 0; + + for (var i = 0; i < cc; ++i) { + var ch = words[i]; + if (/\s/.test(ch)) { + if (state != 0) { + state = 0; + } + + continue; + } else if (ch.charCodeAt() < 128) { + if (state != 1) { + state = 1; + ++wc; + } + } else { + state = 2; + ++wc; + } + + ++cns; + } + + content.updateWordCountInfo(wc, cns, cc); +}; diff --git a/src/src.pro b/src/src.pro index 65595726..49b906f8 100644 --- a/src/src.pro +++ b/src/src.pro @@ -213,7 +213,8 @@ HEADERS += vmainwindow.h \ vstyleditemdelegate.h \ vtreewidget.h \ dialog/vexportdialog.h \ - vexporter.h + vexporter.h \ + vwordcountinfo.h RESOURCES += \ vnote.qrc \ diff --git a/src/vdocument.cpp b/src/vdocument.cpp index 61890a8e..30ea6634 100644 --- a/src/vdocument.cpp +++ b/src/vdocument.cpp @@ -120,3 +120,15 @@ void VDocument::htmlContentCB(const QString &p_head, { emit htmlContentFinished(p_head, p_style, p_body); } + +void VDocument::updateWordCountInfo(int p_wordCount, + int p_charWithoutSpacesCount, + int p_charWithSpacesCount) +{ + m_wordCountInfo.m_mode = VWordCountInfo::Read; + m_wordCountInfo.m_wordCount = p_wordCount; + m_wordCountInfo.m_charWithoutSpacesCount = p_charWithoutSpacesCount; + m_wordCountInfo.m_charWithSpacesCount = p_charWithSpacesCount; + + emit wordCountInfoUpdated(); +} diff --git a/src/vdocument.h b/src/vdocument.h index 84f5a6ae..8f341513 100644 --- a/src/vdocument.h +++ b/src/vdocument.h @@ -4,6 +4,8 @@ #include #include +#include "vwordcountinfo.h" + class VFile; class VDocument : public QObject @@ -41,6 +43,8 @@ public: // Request to get the HTML content. void getHtmlContentAsync(); + const VWordCountInfo &getWordCountInfo() const; + public slots: // Will be called in the HTML side @@ -74,6 +78,10 @@ public slots: const QString &p_style, const QString &p_body); + void updateWordCountInfo(int p_wordCount, + int p_charWithoutSpacesCount, + int p_charWithSpacesCount); + signals: void textChanged(const QString &text); @@ -108,6 +116,8 @@ signals: const QString &p_styleContent, const QString &p_bodyContent); + void wordCountInfoUpdated(); + private: QString m_toc; QString m_header; @@ -125,6 +135,8 @@ private: // Whether the web side is ready to convert text to html. bool m_readyToTextToHtml; + + VWordCountInfo m_wordCountInfo; }; inline bool VDocument::isReadyToHighlight() const @@ -136,4 +148,10 @@ inline bool VDocument::isReadyToTextToHtml() const { return m_readyToTextToHtml; } + +inline const VWordCountInfo &VDocument::getWordCountInfo() const +{ + return m_wordCountInfo; +} + #endif // VDOCUMENT_H diff --git a/src/veditor.h b/src/veditor.h index a8591888..4721100a 100644 --- a/src/veditor.h +++ b/src/veditor.h @@ -10,6 +10,7 @@ #include "veditconfig.h" #include "vconstants.h" #include "vfile.h" +#include "vwordcountinfo.h" class QWidget; class VEditorObject; @@ -149,6 +150,8 @@ public: // @p_modified: if true, delete the whole content and insert the new content. virtual void setContent(const QString &p_content, bool p_modified = false) = 0; + virtual VWordCountInfo fetchWordCountInfo() const = 0; + // Wrapper functions for QPlainTextEdit/QTextEdit. // Ends with W to distinguish it from the original interfaces. public: diff --git a/src/vedittab.cpp b/src/vedittab.cpp index 79c43ff8..54d708e3 100644 --- a/src/vedittab.cpp +++ b/src/vedittab.cpp @@ -236,3 +236,10 @@ QString VEditTab::handleVimCmdRequestRegister(int p_key, int p_modifiers) return QString(); } + +VWordCountInfo VEditTab::fetchWordCountInfo(bool p_editMode) const +{ + Q_UNUSED(p_editMode); + + return VWordCountInfo(); +} diff --git a/src/vedittab.h b/src/vedittab.h index cf70ea99..6e591a89 100644 --- a/src/vedittab.h +++ b/src/vedittab.h @@ -8,6 +8,7 @@ #include "vfile.h" #include "utils/vvim.h" #include "vedittabinfo.h" +#include "vwordcountinfo.h" class VEditArea; class VSnippet; @@ -114,6 +115,9 @@ public: // Handle the change of file or directory, such as the file has been moved. virtual void handleFileOrDirectoryChange(bool p_isFile, UpdateAction p_act); + // Fetch tab stat info. + virtual VWordCountInfo fetchWordCountInfo(bool p_editMode) const; + public slots: // Enter edit mode virtual void editFile() = 0; diff --git a/src/vedittabinfo.h b/src/vedittabinfo.h index 3f4245c8..71a127fd 100644 --- a/src/vedittabinfo.h +++ b/src/vedittabinfo.h @@ -1,6 +1,8 @@ #ifndef VEDITTABINFO_H #define VEDITTABINFO_H +#include "vwordcountinfo.h" + class VEditTab; struct VEditTabInfo @@ -31,6 +33,7 @@ struct VEditTabInfo m_cursorBlockNumber = -1; m_cursorPositionInBlock = -1; m_blockCount = -1; + m_wordCountInfo.clear(); m_headerIndex = -1; } @@ -43,6 +46,8 @@ struct VEditTabInfo int m_cursorPositionInBlock; int m_blockCount; + VWordCountInfo m_wordCountInfo; + // Header index in outline. int m_headerIndex; }; diff --git a/src/vmdeditor.cpp b/src/vmdeditor.cpp index 6f677b8d..e5661434 100644 --- a/src/vmdeditor.cpp +++ b/src/vmdeditor.cpp @@ -1233,3 +1233,45 @@ void VMdEditor::insertImageLink(const QString &p_text, const QString &p_url) } } +VWordCountInfo VMdEditor::fetchWordCountInfo() const +{ + VWordCountInfo info; + QTextDocument *doc = document(); + + // Char without spaces. + int cns = 0; + int wc = 0; + // Remove th ending new line. + int cc = doc->characterCount() - 1; + // 0 - not in word; + // 1 - in English word; + // 2 - in non-English word; + int state = 0; + + for (int i = 0; i < cc; ++i) { + QChar ch = doc->characterAt(i); + if (ch.isSpace()) { + if (state) { + state = 0; + } + + continue; + } else if (ch.unicode() < 128) { + if (state != 1) { + state = 1; + ++wc; + } + } else { + state = 2; + ++wc; + } + + ++cns; + } + + info.m_mode = VWordCountInfo::Edit; + info.m_wordCount = wc; + info.m_charWithoutSpacesCount = cns; + info.m_charWithSpacesCount = cc; + return info; +} diff --git a/src/vmdeditor.h b/src/vmdeditor.h index 329c8ade..aa77d6ef 100644 --- a/src/vmdeditor.h +++ b/src/vmdeditor.h @@ -67,6 +67,8 @@ public: // Update m_initImages and m_insertedImages to handle the change of the note path. void updateInitAndInsertedImages(bool p_fileChanged, UpdateAction p_act); + VWordCountInfo fetchWordCountInfo() const Q_DECL_OVERRIDE; + public slots: bool jumpTitle(bool p_forward, int p_relativeLevel, int p_repeat) Q_DECL_OVERRIDE; diff --git a/src/vmdtab.cpp b/src/vmdtab.cpp index 55d2ad9d..2436cd5a 100644 --- a/src/vmdtab.cpp +++ b/src/vmdtab.cpp @@ -85,17 +85,22 @@ void VMdTab::showFileReadMode() // Will recover the header when web side is ready. m_headerFromEditMode = m_currentHeader; + updateWebView(); + + m_stacks->setCurrentWidget(m_webViewer); + clearSearchedWordHighlight(); + + updateStatus(); +} + +void VMdTab::updateWebView() +{ if (m_mdConType == MarkdownConverterType::Hoedown) { viewWebByConverter(); } else { m_document->updateText(); updateOutlineFromHtml(m_document->getToc()); } - - m_stacks->setCurrentWidget(m_webViewer); - clearSearchedWordHighlight(); - - updateStatus(); } bool VMdTab::scrollWebViewToHeader(const VHeaderPointer &p_header) @@ -424,6 +429,15 @@ void VMdTab::setupMarkdownViewer() Q_ASSERT(m_editor); m_editor->textToHtmlFinished(p_text, m_webViewer->url(), p_html); }); + connect(m_document, &VDocument::wordCountInfoUpdated, + this, [this]() { + VEditTabInfo info = fetchTabInfo(VEditTabInfo::InfoType::All); + if (m_isEditMode) { + info.m_wordCountInfo = m_document->getWordCountInfo(); + } + + emit statusUpdated(info); + }); page->setWebChannel(channel); @@ -790,6 +804,16 @@ VEditTabInfo VMdTab::fetchTabInfo(VEditTabInfo::InfoType p_type) const info.m_blockCount = m_editor->document()->blockCount(); } + if (m_isEditMode) { + if (m_editor) { + // We do not get the full word count info in edit mode. + info.m_wordCountInfo.m_mode = VWordCountInfo::Edit; + info.m_wordCountInfo.m_charWithSpacesCount = m_editor->document()->characterCount() - 1; + } + } else { + info.m_wordCountInfo = m_document->getWordCountInfo(); + } + info.m_headerIndex = m_currentHeader.m_index; return info; @@ -1257,3 +1281,21 @@ void VMdTab::handleSavePageRequested() m_webViewer->page()->save(fileName, format); } + +VWordCountInfo VMdTab::fetchWordCountInfo(bool p_editMode) const +{ + if (p_editMode) { + if (m_editor) { + return m_editor->fetchWordCountInfo(); + } + } else { + // Request to update with current text. + if (m_isEditMode) { + const_cast(this)->updateWebView(); + } + + return m_document->getWordCountInfo(); + } + + return VWordCountInfo(); +} diff --git a/src/vmdtab.h b/src/vmdtab.h index adaa4305..fce7b16d 100644 --- a/src/vmdtab.h +++ b/src/vmdtab.h @@ -88,6 +88,9 @@ public: void handleFileOrDirectoryChange(bool p_isFile, UpdateAction p_act) Q_DECL_OVERRIDE; + // Fetch tab stat info. + VWordCountInfo fetchWordCountInfo(bool p_editMode) const Q_DECL_OVERRIDE; + public slots: // Enter edit mode. void editFile() Q_DECL_OVERRIDE; @@ -207,6 +210,9 @@ private: bool executeVimCommandInWebView(const QString &p_cmd); + // Update web view by current content. + void updateWebView(); + VMdEditor *m_editor; VWebView *m_webViewer; VDocument *m_document; diff --git a/src/vtabindicator.cpp b/src/vtabindicator.cpp index 5361f93b..2d0739fa 100644 --- a/src/vtabindicator.cpp +++ b/src/vtabindicator.cpp @@ -1,13 +1,114 @@ #include "vtabindicator.h" -#include -#include +#include #include "vedittab.h" #include "vorphanfile.h" +#include "vbuttonwithwidget.h" +#include "vwordcountinfo.h" + +class VWordCountPanel : public QWidget +{ +public: + VWordCountPanel(QWidget *p_parent) + : QWidget(p_parent) + { + m_wordLabel = new QLabel(); + m_wordLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter); + m_charWithoutSpacesLabel = new QLabel(); + m_charWithoutSpacesLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter); + m_charWithSpacesLabel = new QLabel(); + m_charWithSpacesLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter); + + QFormLayout *readLayout = new QFormLayout(); + readLayout->addRow(tr("Words"), m_wordLabel); + readLayout->addRow(tr("Characters (no spaces)"), m_charWithoutSpacesLabel); + readLayout->addRow(tr("Characters (with spaces)"), m_charWithSpacesLabel); + m_readBox = new QGroupBox(tr("Read")); + m_readBox->setLayout(readLayout); + + m_wordEditLabel = new QLabel(); + m_wordEditLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter); + m_charWithoutSpacesEditLabel = new QLabel(); + m_charWithoutSpacesEditLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter); + m_charWithSpacesEditLabel = new QLabel(); + m_charWithSpacesEditLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter); + + QLabel *cwsLabel = new QLabel(tr("Characters (with spaces)")); + cwsLabel->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); + + QFormLayout *editLayout = new QFormLayout(); + editLayout->addRow(tr("Words"), m_wordEditLabel); + editLayout->addRow(tr("Characters (no spaces)"), m_charWithoutSpacesEditLabel); + editLayout->addRow(cwsLabel, m_charWithSpacesEditLabel); + m_editBox = new QGroupBox(tr("Edit")); + m_editBox->setLayout(editLayout); + + QLabel *titleLabel = new QLabel(tr("Word Count")); + titleLabel->setProperty("TitleLabel", true); + QVBoxLayout *mainLayout = new QVBoxLayout(); + mainLayout->addWidget(titleLabel); + mainLayout->addWidget(m_readBox); + mainLayout->addWidget(m_editBox); + + setLayout(mainLayout); + + setMinimumWidth(300); + } + + void updateReadInfo(const VWordCountInfo &p_readInfo) + { + if (p_readInfo.isNull()) { + m_wordLabel->clear(); + m_charWithoutSpacesLabel->clear(); + m_charWithSpacesLabel->clear(); + } else { + m_wordLabel->setText(QString::number(p_readInfo.m_wordCount)); + m_charWithoutSpacesLabel->setText(QString::number(p_readInfo.m_charWithoutSpacesCount)); + m_charWithSpacesLabel->setText(QString::number(p_readInfo.m_charWithSpacesCount)); + } + } + + void updateEditInfo(const VWordCountInfo &p_editInfo) + { + if (p_editInfo.isNull()) { + m_wordEditLabel->clear(); + m_charWithoutSpacesEditLabel->clear(); + m_charWithSpacesEditLabel->clear(); + } else { + m_wordEditLabel->setText(QString::number(p_editInfo.m_wordCount)); + m_charWithoutSpacesEditLabel->setText(QString::number(p_editInfo.m_charWithoutSpacesCount)); + m_charWithSpacesEditLabel->setText(QString::number(p_editInfo.m_charWithSpacesCount)); + } + } + + void clear() + { + m_wordLabel->clear(); + m_charWithoutSpacesLabel->clear(); + m_charWithSpacesLabel->clear(); + + m_wordEditLabel->clear(); + m_charWithoutSpacesEditLabel->clear(); + m_charWithSpacesEditLabel->clear(); + } + +private: + QLabel *m_wordLabel; + QLabel *m_charWithoutSpacesLabel; + QLabel *m_charWithSpacesLabel; + + QLabel *m_wordEditLabel; + QLabel *m_charWithoutSpacesEditLabel; + QLabel *m_charWithSpacesEditLabel; + + QGroupBox *m_readBox; + QGroupBox *m_editBox; +}; VTabIndicator::VTabIndicator(QWidget *p_parent) - : QWidget(p_parent) + : QWidget(p_parent), + m_editTab(NULL) { setupUI(); } @@ -33,8 +134,17 @@ void VTabIndicator::setupUI() m_cursorLabel = new QLabel(this); m_cursorLabel->setProperty("TabIndicatorLabel", true); + m_wordCountPanel = new VWordCountPanel(this); + m_wordCountBtn = new VButtonWithWidget(tr("[W]"), m_wordCountPanel, this); + m_wordCountBtn->setToolTip(tr("Word Count Information")); + m_wordCountBtn->setProperty("StatusBtn", true); + m_wordCountBtn->setFocusPolicy(Qt::NoFocus); + connect(m_wordCountBtn, &VButtonWithWidget::popupWidgetAboutToShow, + this, &VTabIndicator::updateWordCountInfo); + QHBoxLayout *mainLayout = new QHBoxLayout(this); mainLayout->addWidget(m_cursorLabel); + mainLayout->addWidget(m_wordCountBtn); mainLayout->addWidget(m_externalLabel); mainLayout->addWidget(m_systemLabel); mainLayout->addWidget(m_readonlyLabel); @@ -75,7 +185,6 @@ static QString docTypeToString(DocType p_type) void VTabIndicator::update(const VEditTabInfo &p_info) { - const VEditTab *editTab = NULL; const VFile *file = NULL; DocType docType = DocType::Html; bool readonly = false; @@ -83,16 +192,17 @@ void VTabIndicator::update(const VEditTabInfo &p_info) bool system = false; QString cursorStr; - if (p_info.m_editTab) + m_editTab = p_info.m_editTab; + + if (m_editTab) { - editTab = p_info.m_editTab; - file = editTab->getFile(); + file = m_editTab->getFile(); docType = file->getDocType(); readonly = !file->isModifiable(); external = file->getType() == FileType::Orphan; system = external && dynamic_cast(file)->isSystemFile(); - if (editTab->isEditMode()) { + if (m_editTab->isEditMode()) { int line = p_info.m_cursorBlockNumber + 1; int col = p_info.m_cursorPositionInBlock; if (col < 0) { @@ -113,8 +223,70 @@ void VTabIndicator::update(const VEditTabInfo &p_info) } } + updateWordCountBtn(p_info); + + if (p_info.m_wordCountInfo.m_mode == VWordCountInfo::Read) { + m_wordCountPanel->updateReadInfo(p_info.m_wordCountInfo); + } + m_docTypeLabel->setText(docTypeToString(docType)); m_readonlyLabel->setVisible(readonly); m_externalLabel->setVisible(external); m_systemLabel->setVisible(system); } + +void VTabIndicator::updateWordCountInfo(QWidget *p_widget) +{ + VWordCountPanel *wcp = dynamic_cast(p_widget); + if (!m_editTab) { + wcp->clear(); + return; + } + + wcp->updateReadInfo(m_editTab->fetchWordCountInfo(false)); + wcp->updateEditInfo(m_editTab->fetchWordCountInfo(true)); +} + +void VTabIndicator::updateWordCountBtn(const VEditTabInfo &p_info) +{ + const VEditTab *editTab = p_info.m_editTab; + if (!editTab) { + m_wordCountBtn->setText(tr("[W]")); + return; + } + + const VWordCountInfo &wci = p_info.m_wordCountInfo; + bool editMode = editTab->isEditMode(); + int wc = -1; + bool needUpdate = false; + switch (wci.m_mode) { + case VWordCountInfo::Read: + if (!editMode) { + wc = wci.m_wordCount; + needUpdate = true; + } + + break; + + case VWordCountInfo::Edit: + if (editMode) { + wc = wci.m_charWithSpacesCount; + needUpdate = true; + } + + break; + + case VWordCountInfo::Invalid: + needUpdate = true; + break; + + default: + break; + } + + if (needUpdate) { + QString text = tr("[%1]%2").arg(editMode ? tr("C") : tr("W")) + .arg(wc > -1 ? QString::number(wc) : ""); + m_wordCountBtn->setText(text); + } +} diff --git a/src/vtabindicator.h b/src/vtabindicator.h index b029eaa8..1adecea4 100644 --- a/src/vtabindicator.h +++ b/src/vtabindicator.h @@ -5,6 +5,9 @@ #include "vedittabinfo.h" class QLabel; +class VButtonWithWidget; +class VEditTab; +class VWordCountPanel; class VTabIndicator : public QWidget { @@ -16,9 +19,14 @@ public: // Update indicator. void update(const VEditTabInfo &p_info); +private slots: + void updateWordCountInfo(QWidget *p_widget); + private: void setupUI(); + void updateWordCountBtn(const VEditTabInfo &p_info); + // Indicate the doc type. QLabel *m_docTypeLabel; @@ -33,6 +41,13 @@ private: // Indicate the position of current cursor. QLabel *m_cursorLabel; + + // Indicate the word count. + VButtonWithWidget *m_wordCountBtn; + + VEditTab *m_editTab; + + VWordCountPanel *m_wordCountPanel; }; #endif // VTABINDICATOR_H diff --git a/src/vwordcountinfo.h b/src/vwordcountinfo.h new file mode 100644 index 00000000..693b0e84 --- /dev/null +++ b/src/vwordcountinfo.h @@ -0,0 +1,53 @@ +#ifndef VWORDCOUNTINFO_H +#define VWORDCOUNTINFO_H + +#include +#include + + +struct VWordCountInfo +{ + enum Mode + { + Read = 0, + Edit, + Invalid + }; + + VWordCountInfo() + : m_mode(Mode::Invalid), + m_wordCount(-1), + m_charWithoutSpacesCount(-1), + m_charWithSpacesCount(-1) + { + } + + bool isNull() const + { + return m_mode == Mode::Invalid; + } + + void clear() + { + m_mode = Mode::Invalid; + m_wordCount = -1; + m_charWithoutSpacesCount = -1; + m_charWithSpacesCount = -1; + } + + QString toString() const + { + return QString("VWordCountInfo mode %1 WC %2 CNSC %3 CSC %4") + .arg(m_mode) + .arg(m_wordCount) + .arg(m_charWithoutSpacesCount) + .arg(m_charWithSpacesCount); + } + + Mode m_mode; + int m_wordCount; + int m_charWithoutSpacesCount; + int m_charWithSpacesCount; +}; + +#endif // VWORDCOUNTINFO_H