Forráskód Böngészése

support word count

Le Tan 3 éve
szülő
commit
791a5da245

+ 2 - 0
src/core/editorconfig.h

@@ -62,6 +62,8 @@ namespace vnotex
             Debug,
             Debug,
             Print,
             Print,
             ClearHighlights,
             ClearHighlights,
+            WordCount,
+            Attachment,
             MaxShortcut
             MaxShortcut
         };
         };
         Q_ENUM(Shortcut)
         Q_ENUM(Shortcut)

+ 1 - 0
src/data/core/core.qrc

@@ -43,6 +43,7 @@
         <file>icons/attachment_editor.svg</file>
         <file>icons/attachment_editor.svg</file>
         <file>icons/attachment_full_editor.svg</file>
         <file>icons/attachment_full_editor.svg</file>
         <file>icons/tag_editor.svg</file>
         <file>icons/tag_editor.svg</file>
+        <file>icons/word_count_editor.svg</file>
         <file>icons/split_menu.svg</file>
         <file>icons/split_menu.svg</file>
         <file>icons/split_window_list.svg</file>
         <file>icons/split_window_list.svg</file>
         <file>icons/type_heading_editor.svg</file>
         <file>icons/type_heading_editor.svg</file>

+ 1 - 0
src/data/core/icons/word_count_editor.svg

@@ -0,0 +1 @@
+<?xml version="1.0" encoding="UTF-8"?><svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="24" cy="24" r="20" fill="none" stroke="#000000" stroke-width="4"/><path d="M32 16H16" stroke="#000000" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/><path d="M24 34V16" stroke="#000000" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/></svg>

+ 2 - 0
src/data/core/vnotex.json

@@ -218,6 +218,8 @@
                 "FindPrevious" : "Shift+F3",
                 "FindPrevious" : "Shift+F3",
                 "ApplySnippet" : "Ctrl+G, I",
                 "ApplySnippet" : "Ctrl+G, I",
                 "Tag" : "Ctrl+G, B",
                 "Tag" : "Ctrl+G, B",
+                "Attachment" : "",
+                "WordCount" : "",
                 "Debug" : "F12",
                 "Debug" : "F12",
                 "Print" : "",
                 "Print" : "",
                 "ClearHighlights" : "Ctrl+G, Space"
                 "ClearHighlights" : "Ctrl+G, Space"

+ 34 - 0
src/widgets/buttonpopup.cpp

@@ -0,0 +1,34 @@
+#include "buttonpopup.h"
+
+#include <QVBoxLayout>
+
+#include <utils/widgetutils.h>
+
+using namespace vnotex;
+
+ButtonPopup::ButtonPopup(QToolButton *p_btn, QWidget *p_parent)
+    : QMenu(p_parent),
+      m_button(p_btn)
+{
+    setupUI();
+
+#if defined(Q_OS_MACOS) || defined(Q_OS_MAC)
+    // Qt::Popup on macOS does not work well with input method.
+    setWindowFlags(Qt::Tool | Qt::NoDropShadowWindowHint);
+    setWindowModality(Qt::ApplicationModal);
+#endif
+}
+
+void ButtonPopup::setupUI()
+{
+    auto mainLayout = new QVBoxLayout(this);
+    WidgetUtils::setContentsMargins(mainLayout);
+}
+
+void ButtonPopup::setCentralWidget(QWidget *p_widget)
+{
+    Q_ASSERT(p_widget);
+    auto mainLayout = layout();
+    Q_ASSERT(mainLayout->count() == 0);
+    mainLayout->addWidget(p_widget);
+}

+ 28 - 0
src/widgets/buttonpopup.h

@@ -0,0 +1,28 @@
+#ifndef BUTTONPOPUP_H
+#define BUTTONPOPUP_H
+
+#include <QMenu>
+
+class QToolButton;
+
+namespace vnotex
+{
+    // Base class for the popup of a QToolButton.
+    class ButtonPopup : public QMenu
+    {
+        Q_OBJECT
+    public:
+        ButtonPopup(QToolButton *p_btn, QWidget *p_parent = nullptr);
+
+    protected:
+        void setCentralWidget(QWidget *p_widget);
+
+    private:
+        void setupUI();
+
+        // Button for this menu.
+        QToolButton *m_button = nullptr;
+    };
+}
+
+#endif // BUTTONPOPUP_H

+ 7 - 0
src/widgets/editors/markdownviewer.cpp

@@ -435,3 +435,10 @@ void MarkdownViewer::crossCopy(const QString &p_target, const QString &p_baseUrl
 {
 {
     emit m_adapter->crossCopyRequested(0, 0, p_target, p_baseUrl, p_html);
     emit m_adapter->crossCopyRequested(0, 0, p_target, p_baseUrl, p_html);
 }
 }
+
+void MarkdownViewer::saveContent(const std::function<void(const QString &p_content)> &p_callback)
+{
+    page()->runJavaScript("document.getElementById('vx-content').textContent", [p_callback](const QVariant &v) {
+        p_callback(v.toString());
+    });
+}

+ 2 - 0
src/widgets/editors/markdownviewer.h

@@ -31,6 +31,8 @@ namespace vnotex
 
 
         void setPreviewHelper(PreviewHelper *p_previewHelper);
         void setPreviewHelper(PreviewHelper *p_previewHelper);
 
 
+        void saveContent(const std::function<void(const QString &p_content)> &p_callback);
+
     signals:
     signals:
         void zoomFactorChanged(qreal p_factor);
         void zoomFactorChanged(qreal p_factor);
 
 

+ 37 - 0
src/widgets/markdownviewwindow.cpp

@@ -273,6 +273,7 @@ void MarkdownViewWindow::setupToolBar()
     addAction(toolBar, ViewWindowToolBarHelper::EditReadDiscard);
     addAction(toolBar, ViewWindowToolBarHelper::EditReadDiscard);
     addAction(toolBar, ViewWindowToolBarHelper::Save);
     addAction(toolBar, ViewWindowToolBarHelper::Save);
     addAction(toolBar, ViewWindowToolBarHelper::ViewMode);
     addAction(toolBar, ViewWindowToolBarHelper::ViewMode);
+    addAction(toolBar, ViewWindowToolBarHelper::WordCount);
 
 
     toolBar->addSeparator();
     toolBar->addSeparator();
 
 
@@ -1465,3 +1466,39 @@ void MarkdownViewWindow::updateEditorFromConfig()
         m_editor->setLeaderKeyToSkip(leaderKey.m_key, leaderKey.m_modifiers);
         m_editor->setLeaderKeyToSkip(leaderKey.m_key, leaderKey.m_modifiers);
     }
     }
 }
 }
+
+void MarkdownViewWindow::fetchWordCountInfo(const std::function<void(const WordCountInfo &)> &p_callback) const
+{
+    auto text = selectedText();
+    if (!text.isEmpty()) {
+        auto info = TextViewWindowHelper::calculateWordCountInfo(text);
+        info.m_isSelection = true;
+        p_callback(info);
+    }
+
+    switch (m_mode) {
+    case ViewWindowMode::Read:
+    {
+        Q_ASSERT(m_viewer);
+        m_viewer->saveContent([p_callback](const QString &content) {
+            auto info = TextViewWindowHelper::calculateWordCountInfo(content);
+            info.m_isSelection = false;
+            p_callback(info);
+        });
+        break;
+    }
+
+    case ViewWindowMode::Edit:
+    {
+        Q_ASSERT(m_editor);
+        auto info = TextViewWindowHelper::calculateWordCountInfo(m_editor->getText());
+        info.m_isSelection = false;
+        p_callback(info);
+        break;
+    }
+
+    default:
+        p_callback(WordCountInfo());
+        break;
+    }
+}

+ 2 - 0
src/widgets/markdownviewwindow.h

@@ -56,6 +56,8 @@ namespace vnotex
 
 
         void applySnippet() Q_DECL_OVERRIDE;
         void applySnippet() Q_DECL_OVERRIDE;
 
 
+        void fetchWordCountInfo(const std::function<void(const WordCountInfo &)> &p_callback) const Q_DECL_OVERRIDE;
+
     public slots:
     public slots:
         void handleEditorConfigChange() Q_DECL_OVERRIDE;
         void handleEditorConfigChange() Q_DECL_OVERRIDE;
 
 

+ 2 - 12
src/widgets/tagpopup.cpp

@@ -11,17 +11,10 @@
 using namespace vnotex;
 using namespace vnotex;
 
 
 TagPopup::TagPopup(QToolButton *p_btn, QWidget *p_parent)
 TagPopup::TagPopup(QToolButton *p_btn, QWidget *p_parent)
-    : QMenu(p_parent),
-      m_button(p_btn)
+    : ButtonPopup(p_btn, p_parent)
 {
 {
     setupUI();
     setupUI();
 
 
-#if defined(Q_OS_MACOS) || defined(Q_OS_MAC)
-    // Qt::Popup on macOS does not work well with input method.
-    setWindowFlags(Qt::Tool | Qt::NoDropShadowWindowHint);
-    setWindowModality(Qt::ApplicationModal);
-#endif
-
     connect(this, &QMenu::aboutToShow,
     connect(this, &QMenu::aboutToShow,
             this, [this]() {
             this, [this]() {
                 m_tagViewer->setNode(m_buffer ? m_buffer->getNode() : nullptr);
                 m_tagViewer->setNode(m_buffer ? m_buffer->getNode() : nullptr);
@@ -36,11 +29,8 @@ TagPopup::TagPopup(QToolButton *p_btn, QWidget *p_parent)
 
 
 void TagPopup::setupUI()
 void TagPopup::setupUI()
 {
 {
-    auto mainLayout = new QVBoxLayout(this);
-    WidgetUtils::setContentsMargins(mainLayout);
-
     m_tagViewer = new TagViewer(true, this);
     m_tagViewer = new TagViewer(true, this);
-    mainLayout->addWidget(m_tagViewer);
+    setCentralWidget(m_tagViewer);
 
 
     setMinimumSize(256, 320);
     setMinimumSize(256, 320);
 }
 }

+ 2 - 5
src/widgets/tagpopup.h

@@ -1,7 +1,7 @@
 #ifndef TAGPOPUP_H
 #ifndef TAGPOPUP_H
 #define TAGPOPUP_H
 #define TAGPOPUP_H
 
 
-#include <QMenu>
+#include "buttonpopup.h"
 
 
 class QToolButton;
 class QToolButton;
 
 
@@ -10,7 +10,7 @@ namespace vnotex
     class Buffer;
     class Buffer;
     class TagViewer;
     class TagViewer;
 
 
-    class TagPopup : public QMenu
+    class TagPopup : public ButtonPopup
     {
     {
         Q_OBJECT
         Q_OBJECT
     public:
     public:
@@ -23,9 +23,6 @@ namespace vnotex
 
 
         Buffer *m_buffer = nullptr;
         Buffer *m_buffer = nullptr;
 
 
-        // Button for this menu.
-        QToolButton *m_button = nullptr;
-
         // Managed by QObject.
         // Managed by QObject.
         TagViewer *m_tagViewer = nullptr;
         TagViewer *m_tagViewer = nullptr;
     };
     };

+ 16 - 0
src/widgets/textviewwindow.cpp

@@ -66,6 +66,7 @@ void TextViewWindow::setupToolBar()
     addToolBar(toolBar);
     addToolBar(toolBar);
 
 
     addAction(toolBar, ViewWindowToolBarHelper::Save);
     addAction(toolBar, ViewWindowToolBarHelper::Save);
+    addAction(toolBar, ViewWindowToolBarHelper::WordCount);
 
 
     toolBar->addSeparator();
     toolBar->addSeparator();
 
 
@@ -322,3 +323,18 @@ void TextViewWindow::clearHighlights()
 {
 {
     TextViewWindowHelper::clearSearchHighlights(this);
     TextViewWindowHelper::clearSearchHighlights(this);
 }
 }
+
+void TextViewWindow::fetchWordCountInfo(const std::function<void(const WordCountInfo &)> &p_callback) const
+{
+    auto text = selectedText();
+    if (text.isEmpty()) {
+        text = getLatestContent();
+        auto info = TextViewWindowHelper::calculateWordCountInfo(text);
+        info.m_isSelection = false;
+        p_callback(info);
+    } else {
+        auto info = TextViewWindowHelper::calculateWordCountInfo(text);
+        info.m_isSelection = true;
+        p_callback(info);
+    }
+}

+ 2 - 0
src/widgets/textviewwindow.h

@@ -37,6 +37,8 @@ namespace vnotex
 
 
         void applySnippet() Q_DECL_OVERRIDE;
         void applySnippet() Q_DECL_OVERRIDE;
 
 
+        void fetchWordCountInfo(const std::function<void(const WordCountInfo &)> &p_callback) const Q_DECL_OVERRIDE;
+
     public slots:
     public slots:
         void handleEditorConfigChange() Q_DECL_OVERRIDE;
         void handleEditorConfigChange() Q_DECL_OVERRIDE;
 
 

+ 41 - 0
src/widgets/textviewwindowhelper.h

@@ -325,6 +325,47 @@ namespace vnotex
             const auto result = p_win->m_editor->findText(patterns.first, toEditorFindFlags(patterns.second), 0, -1, p_currentMatchLine);
             const auto result = p_win->m_editor->findText(patterns.first, toEditorFindFlags(patterns.second), 0, -1, p_currentMatchLine);
             p_win->showFindResult(patterns.first, result.m_totalMatches, result.m_currentMatchIndex);
             p_win->showFindResult(patterns.first, result.m_totalMatches, result.m_currentMatchIndex);
         }
         }
+
+        static ViewWindow::WordCountInfo calculateWordCountInfo(const QString &p_text)
+        {
+            ViewWindow::WordCountInfo info;
+
+            // Char without spaces.
+            int cns = 0;
+            int wc = 0;
+            // Remove th ending new line.
+            int cc = p_text.size();
+            // 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 = p_text[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_wordCount = wc;
+            info.m_charWithoutSpaceCount = cns;
+            info.m_charWithSpaceCount = cc;
+            return info;
+        }
     };
     };
 }
 }
 
 

+ 6 - 0
src/widgets/viewwindow.cpp

@@ -456,6 +456,12 @@ QAction *ViewWindow::addAction(QToolBar *p_toolBar, ViewWindowToolBarHelper::Act
         break;
         break;
     }
     }
 
 
+    case ViewWindowToolBarHelper::WordCount:
+    {
+        act = ViewWindowToolBarHelper::addAction(p_toolBar, p_action);
+        break;
+    }
+
     case ViewWindowToolBarHelper::Outline:
     case ViewWindowToolBarHelper::Outline:
     {
     {
         act = ViewWindowToolBarHelper::addAction(p_toolBar, p_action);
         act = ViewWindowToolBarHelper::addAction(p_toolBar, p_action);

+ 10 - 0
src/widgets/viewwindow.h

@@ -40,6 +40,14 @@ namespace vnotex
         };
         };
         Q_DECLARE_FLAGS(WindowFlags, WindowFlag);
         Q_DECLARE_FLAGS(WindowFlags, WindowFlag);
 
 
+        struct WordCountInfo
+        {
+            bool m_isSelection = false;
+            int m_wordCount = 0;
+            int m_charWithoutSpaceCount = 0;
+            int m_charWithSpaceCount = 0;
+        };
+
         explicit ViewWindow(QWidget *p_parent = nullptr);
         explicit ViewWindow(QWidget *p_parent = nullptr);
 
 
         virtual ~ViewWindow();
         virtual ~ViewWindow();
@@ -99,6 +107,8 @@ namespace vnotex
 
 
         virtual QString selectedText() const;
         virtual QString selectedText() const;
 
 
+        virtual void fetchWordCountInfo(const std::function<void(const WordCountInfo &)> &p_callback) const = 0;
+
     public slots:
     public slots:
         virtual void handleEditorConfigChange() = 0;
         virtual void handleEditorConfigChange() = 0;
 
 

+ 21 - 1
src/widgets/viewwindowtoolbarhelper.cpp

@@ -19,6 +19,7 @@
 #include "widgetsfactory.h"
 #include "widgetsfactory.h"
 #include "attachmentpopup.h"
 #include "attachmentpopup.h"
 #include "tagpopup.h"
 #include "tagpopup.h"
+#include "wordcountpopup.h"
 #include "propertydefs.h"
 #include "propertydefs.h"
 #include "outlinepopup.h"
 #include "outlinepopup.h"
 #include "viewwindow.h"
 #include "viewwindow.h"
@@ -84,7 +85,7 @@ void ViewWindowToolBarHelper::addButtonShortcut(QToolButton *p_btn,
 
 
 QAction *ViewWindowToolBarHelper::addAction(QToolBar *p_tb, Action p_action)
 QAction *ViewWindowToolBarHelper::addAction(QToolBar *p_tb, Action p_action)
 {
 {
-    auto viewWindow = static_cast<QWidget *>(p_tb->parent());
+    auto viewWindow = static_cast<ViewWindow *>(p_tb->parent());
     const auto &editorConfig = ConfigMgr::getInst().getEditorConfig();
     const auto &editorConfig = ConfigMgr::getInst().getEditorConfig();
 
 
     QAction *act = nullptr;
     QAction *act = nullptr;
@@ -301,6 +302,8 @@ QAction *ViewWindowToolBarHelper::addAction(QToolBar *p_tb, Action p_action)
         toolBtn->setPopupMode(QToolButton::InstantPopup);
         toolBtn->setPopupMode(QToolButton::InstantPopup);
         toolBtn->setProperty(PropertyDefs::c_toolButtonWithoutMenuIndicator, true);
         toolBtn->setProperty(PropertyDefs::c_toolButtonWithoutMenuIndicator, true);
 
 
+        addButtonShortcut(toolBtn, editorConfig.getShortcut(Shortcut::Attachment), viewWindow);
+
         auto menu = new AttachmentPopup(toolBtn, p_tb);
         auto menu = new AttachmentPopup(toolBtn, p_tb);
         toolBtn->setMenu(menu);
         toolBtn->setMenu(menu);
         break;
         break;
@@ -419,6 +422,23 @@ QAction *ViewWindowToolBarHelper::addAction(QToolBar *p_tb, Action p_action)
         break;
         break;
     }
     }
 
 
+    case Action::WordCount:
+    {
+        act = p_tb->addAction(ToolBarHelper::generateIcon("word_count_editor.svg"),
+                              ViewWindow::tr("Word Count"));
+
+        auto toolBtn = dynamic_cast<QToolButton *>(p_tb->widgetForAction(act));
+        Q_ASSERT(toolBtn);
+        toolBtn->setPopupMode(QToolButton::InstantPopup);
+        toolBtn->setProperty(PropertyDefs::c_toolButtonWithoutMenuIndicator, true);
+
+        addButtonShortcut(toolBtn, editorConfig.getShortcut(Shortcut::WordCount), viewWindow);
+
+        auto menu = new WordCountPopup(toolBtn, viewWindow, p_tb);
+        toolBtn->setMenu(menu);
+        break;
+    }
+
     default:
     default:
         Q_ASSERT(false);
         Q_ASSERT(false);
         break;
         break;

+ 2 - 1
src/widgets/viewwindowtoolbarhelper.h

@@ -49,7 +49,8 @@ namespace vnotex
             InplacePreview,
             InplacePreview,
             ImageHost,
             ImageHost,
             Debug,
             Debug,
-            Print
+            Print,
+            WordCount
         };
         };
 
 
         static QAction *addAction(QToolBar *p_tb, Action p_action);
         static QAction *addAction(QToolBar *p_tb, Action p_action);

+ 6 - 2
src/widgets/widgets.pri

@@ -2,6 +2,7 @@ SOURCES += \
     $$PWD/attachmentdragdropareaindicator.cpp \
     $$PWD/attachmentdragdropareaindicator.cpp \
     $$PWD/attachmentpopup.cpp \
     $$PWD/attachmentpopup.cpp \
     $$PWD/biaction.cpp \
     $$PWD/biaction.cpp \
+    $$PWD/buttonpopup.cpp \
     $$PWD/combobox.cpp \
     $$PWD/combobox.cpp \
     $$PWD/consoleviewer.cpp \
     $$PWD/consoleviewer.cpp \
     $$PWD/dialogs/dialog.cpp \
     $$PWD/dialogs/dialog.cpp \
@@ -126,12 +127,14 @@ SOURCES += \
     $$PWD/titletoolbar.cpp \
     $$PWD/titletoolbar.cpp \
     $$PWD/viewarea.cpp \
     $$PWD/viewarea.cpp \
     $$PWD/windowspanel.cpp \
     $$PWD/windowspanel.cpp \
-    $$PWD/windowsprovider.cpp
+    $$PWD/windowsprovider.cpp \
+    $$PWD/wordcountpopup.cpp
 
 
 HEADERS += \
 HEADERS += \
     $$PWD/attachmentdragdropareaindicator.h \
     $$PWD/attachmentdragdropareaindicator.h \
     $$PWD/attachmentpopup.h \
     $$PWD/attachmentpopup.h \
     $$PWD/biaction.h \
     $$PWD/biaction.h \
+    $$PWD/buttonpopup.h \
     $$PWD/combobox.h \
     $$PWD/combobox.h \
     $$PWD/consoleviewer.h \
     $$PWD/consoleviewer.h \
     $$PWD/dialogs/dialog.h \
     $$PWD/dialogs/dialog.h \
@@ -260,4 +263,5 @@ HEADERS += \
     $$PWD/titletoolbar.h \
     $$PWD/titletoolbar.h \
     $$PWD/viewarea.h \
     $$PWD/viewarea.h \
     $$PWD/windowspanel.h \
     $$PWD/windowspanel.h \
-    $$PWD/windowsprovider.h
+    $$PWD/windowsprovider.h \
+    $$PWD/wordcountpopup.h

+ 80 - 0
src/widgets/wordcountpopup.cpp

@@ -0,0 +1,80 @@
+#include "wordcountpopup.h"
+
+#include <QFormLayout>
+#include <QLabel>
+#include <QGroupBox>
+#include <QPointer>
+
+#include <utils/widgetutils.h>
+
+using namespace vnotex;
+
+WordCountPanel::WordCountPanel(QWidget *p_parent)
+    : QWidget(p_parent)
+{
+    auto mainLayout = new QFormLayout(this);
+
+    m_selectionLabel = new QLabel(tr("Selection Area"), this);
+    mainLayout->addRow(m_selectionLabel);
+    m_selectionLabel->hide();
+
+    const auto alignment = Qt::AlignRight | Qt::AlignVCenter;
+    m_wordLabel = new QLabel("0", this);
+    m_wordLabel->setAlignment(alignment);
+    mainLayout->addRow(tr("Words"), m_wordLabel);
+
+    m_charWithoutSpaceLabel = new QLabel("0", this);
+    m_charWithoutSpaceLabel->setAlignment(alignment);
+    mainLayout->addRow(tr("Characters (no spaces)"), m_charWithoutSpaceLabel);
+
+    m_charWithSpaceLabel = new QLabel("0", this);
+    m_charWithSpaceLabel->setAlignment(alignment);
+    mainLayout->addRow(tr("Characters (with spaces)"), m_charWithSpaceLabel);
+}
+
+void WordCountPanel::updateCount(bool p_isSelection,
+                                 int p_words,
+                                 int p_charsWithoutSpace,
+                                 int p_charsWithSpace)
+{
+    m_selectionLabel->setVisible(p_isSelection);
+    m_wordLabel->setText(QString::number(p_words));
+    m_charWithoutSpaceLabel->setText(QString::number(p_charsWithoutSpace));
+    m_charWithSpaceLabel->setText(QString::number(p_charsWithSpace));
+}
+
+WordCountPopup::WordCountPopup(QToolButton *p_btn, const ViewWindow *p_viewWindow, QWidget *p_parent)
+    : ButtonPopup(p_btn, p_parent),
+      m_viewWindow(p_viewWindow)
+{
+    setupUI();
+
+    connect(this, &QMenu::aboutToShow,
+            this, [this]() {
+                QPointer<WordCountPopup> popup(this);
+                m_viewWindow->fetchWordCountInfo([popup](const ViewWindow::WordCountInfo &info) {
+                    if (popup) {
+                        popup->updateCount(info);
+                    }
+                });
+            });
+}
+
+void WordCountPopup::updateCount(const ViewWindow::WordCountInfo &p_info)
+{
+    m_panel->updateCount(p_info.m_isSelection,
+                         p_info.m_wordCount,
+                         p_info.m_charWithoutSpaceCount,
+                         p_info.m_charWithSpaceCount);
+}
+
+void WordCountPopup::setupUI()
+{
+    QWidget *mainWidget = new QWidget(this);
+    setCentralWidget(mainWidget);
+
+    auto mainLayout = new QVBoxLayout(mainWidget);
+
+    m_panel = new WordCountPanel(mainWidget);
+    mainLayout->addWidget(m_panel);
+}

+ 45 - 0
src/widgets/wordcountpopup.h

@@ -0,0 +1,45 @@
+#ifndef WORDCOUNTPOPUP_H
+#define WORDCOUNTPOPUP_H
+
+#include "buttonpopup.h"
+
+#include "viewwindow.h"
+
+class QToolButton;
+class QLabel;
+
+namespace vnotex
+{
+    class WordCountPanel : public QWidget
+    {
+        Q_OBJECT
+    public:
+        explicit WordCountPanel(QWidget *p_parent = nullptr);
+
+        void updateCount(bool p_isSelection, int p_words, int p_charsWithoutSpace, int p_charsWithSpace);
+
+    private:
+        QLabel *m_selectionLabel = nullptr;
+        QLabel *m_wordLabel = nullptr;
+        QLabel *m_charWithoutSpaceLabel = nullptr;
+        QLabel *m_charWithSpaceLabel = nullptr;
+    };
+
+    class WordCountPopup : public ButtonPopup
+    {
+        Q_OBJECT
+    public:
+        WordCountPopup(QToolButton *p_btn, const ViewWindow *p_viewWindow, QWidget *p_parent = nullptr);
+
+        void updateCount(const ViewWindow::WordCountInfo &p_info);
+
+    private:
+        void setupUI();
+
+        WordCountPanel *m_panel = nullptr;
+
+        const ViewWindow *m_viewWindow = nullptr;
+    };
+}
+
+#endif // WORDCOUNTPOPUP_H