diff --git a/src/core/editorconfig.h b/src/core/editorconfig.h index 0e6b2426..0eb95fe6 100644 --- a/src/core/editorconfig.h +++ b/src/core/editorconfig.h @@ -62,6 +62,8 @@ namespace vnotex Debug, Print, ClearHighlights, + WordCount, + Attachment, MaxShortcut }; Q_ENUM(Shortcut) diff --git a/src/data/core/core.qrc b/src/data/core/core.qrc index cf27d264..5d8aed59 100644 --- a/src/data/core/core.qrc +++ b/src/data/core/core.qrc @@ -43,6 +43,7 @@ icons/attachment_editor.svg icons/attachment_full_editor.svg icons/tag_editor.svg + icons/word_count_editor.svg icons/split_menu.svg icons/split_window_list.svg icons/type_heading_editor.svg diff --git a/src/data/core/icons/word_count_editor.svg b/src/data/core/icons/word_count_editor.svg new file mode 100644 index 00000000..a1b4a510 --- /dev/null +++ b/src/data/core/icons/word_count_editor.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/data/core/vnotex.json b/src/data/core/vnotex.json index 1926eefe..d664f229 100644 --- a/src/data/core/vnotex.json +++ b/src/data/core/vnotex.json @@ -218,6 +218,8 @@ "FindPrevious" : "Shift+F3", "ApplySnippet" : "Ctrl+G, I", "Tag" : "Ctrl+G, B", + "Attachment" : "", + "WordCount" : "", "Debug" : "F12", "Print" : "", "ClearHighlights" : "Ctrl+G, Space" diff --git a/src/widgets/buttonpopup.cpp b/src/widgets/buttonpopup.cpp new file mode 100644 index 00000000..44de5f3f --- /dev/null +++ b/src/widgets/buttonpopup.cpp @@ -0,0 +1,34 @@ +#include "buttonpopup.h" + +#include + +#include + +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); +} diff --git a/src/widgets/buttonpopup.h b/src/widgets/buttonpopup.h new file mode 100644 index 00000000..ce29ae96 --- /dev/null +++ b/src/widgets/buttonpopup.h @@ -0,0 +1,28 @@ +#ifndef BUTTONPOPUP_H +#define BUTTONPOPUP_H + +#include + +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 diff --git a/src/widgets/editors/markdownviewer.cpp b/src/widgets/editors/markdownviewer.cpp index bd8ed0cb..61be2ce5 100644 --- a/src/widgets/editors/markdownviewer.cpp +++ b/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); } + +void MarkdownViewer::saveContent(const std::function &p_callback) +{ + page()->runJavaScript("document.getElementById('vx-content').textContent", [p_callback](const QVariant &v) { + p_callback(v.toString()); + }); +} diff --git a/src/widgets/editors/markdownviewer.h b/src/widgets/editors/markdownviewer.h index ed7ca96f..9d0ffd04 100644 --- a/src/widgets/editors/markdownviewer.h +++ b/src/widgets/editors/markdownviewer.h @@ -31,6 +31,8 @@ namespace vnotex void setPreviewHelper(PreviewHelper *p_previewHelper); + void saveContent(const std::function &p_callback); + signals: void zoomFactorChanged(qreal p_factor); diff --git a/src/widgets/markdownviewwindow.cpp b/src/widgets/markdownviewwindow.cpp index 06052209..46663e93 100644 --- a/src/widgets/markdownviewwindow.cpp +++ b/src/widgets/markdownviewwindow.cpp @@ -273,6 +273,7 @@ void MarkdownViewWindow::setupToolBar() addAction(toolBar, ViewWindowToolBarHelper::EditReadDiscard); addAction(toolBar, ViewWindowToolBarHelper::Save); addAction(toolBar, ViewWindowToolBarHelper::ViewMode); + addAction(toolBar, ViewWindowToolBarHelper::WordCount); toolBar->addSeparator(); @@ -1465,3 +1466,39 @@ void MarkdownViewWindow::updateEditorFromConfig() m_editor->setLeaderKeyToSkip(leaderKey.m_key, leaderKey.m_modifiers); } } + +void MarkdownViewWindow::fetchWordCountInfo(const std::function &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; + } +} diff --git a/src/widgets/markdownviewwindow.h b/src/widgets/markdownviewwindow.h index 4bc84548..1e6f8087 100644 --- a/src/widgets/markdownviewwindow.h +++ b/src/widgets/markdownviewwindow.h @@ -56,6 +56,8 @@ namespace vnotex void applySnippet() Q_DECL_OVERRIDE; + void fetchWordCountInfo(const std::function &p_callback) const Q_DECL_OVERRIDE; + public slots: void handleEditorConfigChange() Q_DECL_OVERRIDE; diff --git a/src/widgets/tagpopup.cpp b/src/widgets/tagpopup.cpp index e20b3f0d..3c89f364 100644 --- a/src/widgets/tagpopup.cpp +++ b/src/widgets/tagpopup.cpp @@ -11,17 +11,10 @@ using namespace vnotex; TagPopup::TagPopup(QToolButton *p_btn, QWidget *p_parent) - : QMenu(p_parent), - m_button(p_btn) + : ButtonPopup(p_btn, p_parent) { 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, this, [this]() { m_tagViewer->setNode(m_buffer ? m_buffer->getNode() : nullptr); @@ -36,11 +29,8 @@ TagPopup::TagPopup(QToolButton *p_btn, QWidget *p_parent) void TagPopup::setupUI() { - auto mainLayout = new QVBoxLayout(this); - WidgetUtils::setContentsMargins(mainLayout); - m_tagViewer = new TagViewer(true, this); - mainLayout->addWidget(m_tagViewer); + setCentralWidget(m_tagViewer); setMinimumSize(256, 320); } diff --git a/src/widgets/tagpopup.h b/src/widgets/tagpopup.h index d549baea..edc358d4 100644 --- a/src/widgets/tagpopup.h +++ b/src/widgets/tagpopup.h @@ -1,7 +1,7 @@ #ifndef TAGPOPUP_H #define TAGPOPUP_H -#include +#include "buttonpopup.h" class QToolButton; @@ -10,7 +10,7 @@ namespace vnotex class Buffer; class TagViewer; - class TagPopup : public QMenu + class TagPopup : public ButtonPopup { Q_OBJECT public: @@ -23,9 +23,6 @@ namespace vnotex Buffer *m_buffer = nullptr; - // Button for this menu. - QToolButton *m_button = nullptr; - // Managed by QObject. TagViewer *m_tagViewer = nullptr; }; diff --git a/src/widgets/textviewwindow.cpp b/src/widgets/textviewwindow.cpp index 44ffda11..1b30562d 100644 --- a/src/widgets/textviewwindow.cpp +++ b/src/widgets/textviewwindow.cpp @@ -66,6 +66,7 @@ void TextViewWindow::setupToolBar() addToolBar(toolBar); addAction(toolBar, ViewWindowToolBarHelper::Save); + addAction(toolBar, ViewWindowToolBarHelper::WordCount); toolBar->addSeparator(); @@ -322,3 +323,18 @@ void TextViewWindow::clearHighlights() { TextViewWindowHelper::clearSearchHighlights(this); } + +void TextViewWindow::fetchWordCountInfo(const std::function &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); + } +} diff --git a/src/widgets/textviewwindow.h b/src/widgets/textviewwindow.h index 52ebcfc4..11da33e2 100644 --- a/src/widgets/textviewwindow.h +++ b/src/widgets/textviewwindow.h @@ -37,6 +37,8 @@ namespace vnotex void applySnippet() Q_DECL_OVERRIDE; + void fetchWordCountInfo(const std::function &p_callback) const Q_DECL_OVERRIDE; + public slots: void handleEditorConfigChange() Q_DECL_OVERRIDE; diff --git a/src/widgets/textviewwindowhelper.h b/src/widgets/textviewwindowhelper.h index 1e3e51a3..3db9fabf 100644 --- a/src/widgets/textviewwindowhelper.h +++ b/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); 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; + } }; } diff --git a/src/widgets/viewwindow.cpp b/src/widgets/viewwindow.cpp index e2c8506a..50b024d9 100644 --- a/src/widgets/viewwindow.cpp +++ b/src/widgets/viewwindow.cpp @@ -456,6 +456,12 @@ QAction *ViewWindow::addAction(QToolBar *p_toolBar, ViewWindowToolBarHelper::Act break; } + case ViewWindowToolBarHelper::WordCount: + { + act = ViewWindowToolBarHelper::addAction(p_toolBar, p_action); + break; + } + case ViewWindowToolBarHelper::Outline: { act = ViewWindowToolBarHelper::addAction(p_toolBar, p_action); diff --git a/src/widgets/viewwindow.h b/src/widgets/viewwindow.h index da8e1f3d..853a9585 100644 --- a/src/widgets/viewwindow.h +++ b/src/widgets/viewwindow.h @@ -40,6 +40,14 @@ namespace vnotex }; 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); virtual ~ViewWindow(); @@ -99,6 +107,8 @@ namespace vnotex virtual QString selectedText() const; + virtual void fetchWordCountInfo(const std::function &p_callback) const = 0; + public slots: virtual void handleEditorConfigChange() = 0; diff --git a/src/widgets/viewwindowtoolbarhelper.cpp b/src/widgets/viewwindowtoolbarhelper.cpp index c935d4f6..ab4701b9 100644 --- a/src/widgets/viewwindowtoolbarhelper.cpp +++ b/src/widgets/viewwindowtoolbarhelper.cpp @@ -19,6 +19,7 @@ #include "widgetsfactory.h" #include "attachmentpopup.h" #include "tagpopup.h" +#include "wordcountpopup.h" #include "propertydefs.h" #include "outlinepopup.h" #include "viewwindow.h" @@ -84,7 +85,7 @@ void ViewWindowToolBarHelper::addButtonShortcut(QToolButton *p_btn, QAction *ViewWindowToolBarHelper::addAction(QToolBar *p_tb, Action p_action) { - auto viewWindow = static_cast(p_tb->parent()); + auto viewWindow = static_cast(p_tb->parent()); const auto &editorConfig = ConfigMgr::getInst().getEditorConfig(); QAction *act = nullptr; @@ -301,6 +302,8 @@ QAction *ViewWindowToolBarHelper::addAction(QToolBar *p_tb, Action p_action) toolBtn->setPopupMode(QToolButton::InstantPopup); toolBtn->setProperty(PropertyDefs::c_toolButtonWithoutMenuIndicator, true); + addButtonShortcut(toolBtn, editorConfig.getShortcut(Shortcut::Attachment), viewWindow); + auto menu = new AttachmentPopup(toolBtn, p_tb); toolBtn->setMenu(menu); break; @@ -419,6 +422,23 @@ QAction *ViewWindowToolBarHelper::addAction(QToolBar *p_tb, Action p_action) break; } + case Action::WordCount: + { + act = p_tb->addAction(ToolBarHelper::generateIcon("word_count_editor.svg"), + ViewWindow::tr("Word Count")); + + auto toolBtn = dynamic_cast(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: Q_ASSERT(false); break; diff --git a/src/widgets/viewwindowtoolbarhelper.h b/src/widgets/viewwindowtoolbarhelper.h index f260ec8e..79f96ac5 100644 --- a/src/widgets/viewwindowtoolbarhelper.h +++ b/src/widgets/viewwindowtoolbarhelper.h @@ -49,7 +49,8 @@ namespace vnotex InplacePreview, ImageHost, Debug, - Print + Print, + WordCount }; static QAction *addAction(QToolBar *p_tb, Action p_action); diff --git a/src/widgets/widgets.pri b/src/widgets/widgets.pri index 561121c8..1240f494 100644 --- a/src/widgets/widgets.pri +++ b/src/widgets/widgets.pri @@ -2,6 +2,7 @@ SOURCES += \ $$PWD/attachmentdragdropareaindicator.cpp \ $$PWD/attachmentpopup.cpp \ $$PWD/biaction.cpp \ + $$PWD/buttonpopup.cpp \ $$PWD/combobox.cpp \ $$PWD/consoleviewer.cpp \ $$PWD/dialogs/dialog.cpp \ @@ -126,12 +127,14 @@ SOURCES += \ $$PWD/titletoolbar.cpp \ $$PWD/viewarea.cpp \ $$PWD/windowspanel.cpp \ - $$PWD/windowsprovider.cpp + $$PWD/windowsprovider.cpp \ + $$PWD/wordcountpopup.cpp HEADERS += \ $$PWD/attachmentdragdropareaindicator.h \ $$PWD/attachmentpopup.h \ $$PWD/biaction.h \ + $$PWD/buttonpopup.h \ $$PWD/combobox.h \ $$PWD/consoleviewer.h \ $$PWD/dialogs/dialog.h \ @@ -260,4 +263,5 @@ HEADERS += \ $$PWD/titletoolbar.h \ $$PWD/viewarea.h \ $$PWD/windowspanel.h \ - $$PWD/windowsprovider.h + $$PWD/windowsprovider.h \ + $$PWD/wordcountpopup.h diff --git a/src/widgets/wordcountpopup.cpp b/src/widgets/wordcountpopup.cpp new file mode 100644 index 00000000..1e075daa --- /dev/null +++ b/src/widgets/wordcountpopup.cpp @@ -0,0 +1,80 @@ +#include "wordcountpopup.h" + +#include +#include +#include +#include + +#include + +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 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); +} diff --git a/src/widgets/wordcountpopup.h b/src/widgets/wordcountpopup.h new file mode 100644 index 00000000..1cc515e3 --- /dev/null +++ b/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