diff --git a/src/resources/styles/default.mdhl b/src/resources/styles/default.mdhl index eeea9a97..0ee7db3d 100644 --- a/src/resources/styles/default.mdhl +++ b/src/resources/styles/default.mdhl @@ -5,6 +5,8 @@ editor # QTextEdit just choose the first available font, so specify the Chinese fonts first # Do not use "" to quote the name font-family: Hiragino Sans GB, 冬青黑体, Microsoft YaHei, 微软雅黑, Microsoft YaHei UI, WenQuanYi Micro Hei, 文泉驿雅黑, Dengxian, 等线体, STXihei, 华文细黑, Liberation Sans, Droid Sans, NSimSun, 新宋体, SimSun, 宋体, Helvetica, sans-serif, Tahoma, Arial, Verdana, Geneva, Georgia, Times New Roman +# Style for trailing space +trailing-space: ffebee editor-selection foreground: eeeeee diff --git a/src/resources/vnote.ini b/src/resources/vnote.ini index 9b4c7591..8fed963a 100644 --- a/src/resources/vnote.ini +++ b/src/resources/vnote.ini @@ -42,6 +42,9 @@ enable_image_caption=false ; Image folder name for the notes image_folder=_v_images +; Enable trailing space highlight +enable_trailing_space_highlight=true + [session] tools_dock_checked=true diff --git a/src/vconfigmanager.cpp b/src/vconfigmanager.cpp index 5dbcbbfa..879ae866 100644 --- a/src/vconfigmanager.cpp +++ b/src/vconfigmanager.cpp @@ -143,6 +143,9 @@ void VConfigManager::initialize() m_imageFolder = getConfigFromSettings("global", "image_folder").toString(); + + m_enableTrailingSpaceHighlight = getConfigFromSettings("global", + "enable_trailing_space_highlight").toBool(); } void VConfigManager::readPredefinedColorsFromSettings() @@ -325,6 +328,7 @@ void VConfigManager::updateMarkdownEditStyle() { static const QString defaultCurrentLineBackground = "#C5CAE9"; static const QString defaultCurrentLineVimBackground = "#A5D6A7"; + static const QString defaultTrailingSpaceBackground = "#FFEBEE"; // Read style file .mdhl QString file(getEditorStyleUrl()); @@ -349,6 +353,7 @@ void VConfigManager::updateMarkdownEditStyle() if (editorCurrentLineIt != styles.end()) { auto backgroundIt = editorCurrentLineIt->find("background"); if (backgroundIt != editorCurrentLineIt->end()) { + // Do not need to add "#" here, since this is a built-in attribute. m_editorCurrentLineBackground = *backgroundIt; } @@ -357,6 +362,19 @@ void VConfigManager::updateMarkdownEditStyle() m_editorCurrentLineVimBackground = "#" + *vimBackgroundIt; } } + + m_editorTrailingSpaceBackground = defaultTrailingSpaceBackground; + auto editorIt = styles.find("editor"); + if (editorIt != styles.end()) { + auto trailingIt = editorIt->find("trailing-space"); + if (trailingIt != editorIt->end()) { + m_editorTrailingSpaceBackground = "#" + *trailingIt; + } + } + + qDebug() << "editor-current-line" << m_editorCurrentLineBackground; + qDebug() << "editor-current-line-vim" << m_editorCurrentLineVimBackground; + qDebug() << "editor-trailing-space" << m_editorTrailingSpaceBackground; } void VConfigManager::updateEditStyle() diff --git a/src/vconfigmanager.h b/src/vconfigmanager.h index 2890a3c4..40378406 100644 --- a/src/vconfigmanager.h +++ b/src/vconfigmanager.h @@ -157,6 +157,7 @@ public: inline QString getEditorCurrentLineBackground() const; inline QString getEditorCurrentLineVimBackground() const; + inline QString getEditorTrailingSpaceBackground() const; inline bool getEnableCodeBlockHighlight() const; inline void setEnableCodeBlockHighlight(bool p_enabled); @@ -174,12 +175,13 @@ public: inline void setEnableImageCaption(bool p_enabled); inline const QString &getImageFolder() const; - // Empty string to reset the default folder. inline void setImageFolder(const QString &p_folder); - inline bool isCustomImageFolder() const; + inline bool getEnableTrailingSpaceHighlight() const; + inline void setEnableTrailingSapceHighlight(bool p_enabled); + // Get the folder the ini file exists. QString getConfigFolder() const; @@ -290,9 +292,13 @@ private: // Current line background color in editor. QString m_editorCurrentLineBackground; + // Current line background color in editor in Vim mode. QString m_editorCurrentLineVimBackground; + // Trailing space background color in editor. + QString m_editorTrailingSpaceBackground; + // Enable colde block syntax highlight. bool m_enableCodeBlockHighlight; @@ -312,6 +318,9 @@ private: // Each notebook can specify its custom folder. QString m_imageFolder; + // Enable trailing-space highlight. + bool m_enableTrailingSpaceHighlight; + // The name of the config file in each directory, obsolete. // Use c_dirConfigFile instead. static const QString c_obsoleteDirConfigFile; @@ -722,6 +731,11 @@ inline QString VConfigManager::getEditorCurrentLineVimBackground() const return m_editorCurrentLineVimBackground; } +inline QString VConfigManager::getEditorTrailingSpaceBackground() const +{ + return m_editorTrailingSpaceBackground; +} + inline bool VConfigManager::getEnableCodeBlockHighlight() const { return m_enableCodeBlockHighlight; @@ -826,4 +840,20 @@ inline bool VConfigManager::isCustomImageFolder() const return m_imageFolder != getDefaultConfig("global", "image_folder").toString(); } +inline bool VConfigManager::getEnableTrailingSpaceHighlight() const +{ + return m_enableTrailingSpaceHighlight; +} + +inline void VConfigManager::setEnableTrailingSapceHighlight(bool p_enabled) +{ + if (m_enableTrailingSpaceHighlight == p_enabled) { + return; + } + + m_enableTrailingSpaceHighlight = p_enabled; + setConfigToSettings("global", "enable_trailing_space_highlight", + m_enableTrailingSpaceHighlight); +} + #endif // VCONFIGMANAGER_H diff --git a/src/vedit.cpp b/src/vedit.cpp index d4487f72..e2f79b31 100644 --- a/src/vedit.cpp +++ b/src/vedit.cpp @@ -17,12 +17,13 @@ VEdit::VEdit(VFile *p_file, QWidget *p_parent) : QTextEdit(p_parent), m_file(p_file), m_editOps(NULL) { const int labelTimerInterval = 500; - const int selectedWordTimerInterval = 500; + const int extraSelectionHighlightTimer = 500; const int labelSize = 64; m_cursorLineColor = QColor(g_vnote->getColorFromPalette("Indigo1")); m_selectedWordColor = QColor("Yellow"); m_searchedWordColor = QColor(g_vnote->getColorFromPalette("Green4")); + m_trailingSpaceColor = QColor(vconfig.getEditorTrailingSpaceBackground()); QPixmap wrapPixmap(":/resources/icons/search_wrap.svg"); m_wrapLabel = new QLabel(this); @@ -34,11 +35,11 @@ VEdit::VEdit(VFile *p_file, QWidget *p_parent) connect(m_labelTimer, &QTimer::timeout, this, &VEdit::labelTimerTimeout); - m_selectedWordTimer = new QTimer(this); - m_selectedWordTimer->setSingleShot(true); - m_selectedWordTimer->setInterval(selectedWordTimerInterval); - connect(m_selectedWordTimer, &QTimer::timeout, - this, &VEdit::highlightSelectedWord); + m_highlightTimer = new QTimer(this); + m_highlightTimer->setSingleShot(true); + m_highlightTimer->setInterval(extraSelectionHighlightTimer); + connect(m_highlightTimer, &QTimer::timeout, + this, &VEdit::doHighlightExtraSelections); connect(document(), &QTextDocument::modificationChanged, (VFile *)m_file, &VFile::setModified); @@ -47,9 +48,10 @@ VEdit::VEdit(VFile *p_file, QWidget *p_parent) updateFontAndPalette(); connect(this, &VEdit::cursorPositionChanged, - this, &VEdit::highlightCurrentLine); + this, &VEdit::handleCursorPositionChanged); + connect(this, &VEdit::selectionChanged, - this, &VEdit::triggerHighlightSelectedWord); + this, &VEdit::highlightSelectedWord); } VEdit::~VEdit() @@ -374,7 +376,17 @@ void VEdit::updateFontAndPalette() setPalette(vconfig.getBaseEditPalette()); } -void VEdit::highlightExtraSelections() +void VEdit::highlightExtraSelections(bool p_now) +{ + m_highlightTimer->stop(); + if (p_now) { + doHighlightExtraSelections(); + } else { + m_highlightTimer->start(); + } +} + +void VEdit::doHighlightExtraSelections() { int nrExtra = m_extraSelections.size(); Q_ASSERT(nrExtra == (int)SelectionId::MaxSelection); @@ -382,6 +394,7 @@ void VEdit::highlightExtraSelections() for (int i = 0; i < nrExtra; ++i) { extraSelects.append(m_extraSelections[i]); } + setExtraSelections(extraSelects); } @@ -390,23 +403,24 @@ void VEdit::highlightCurrentLine() QList &selects = m_extraSelections[(int)SelectionId::CurrentLine]; if (vconfig.getHighlightCursorLine() && !isReadOnly()) { // Need to highlight current line. - selects.clear(); - QTextEdit::ExtraSelection select; select.format.setBackground(m_cursorLineColor); select.format.setProperty(QTextFormat::FullWidthSelection, true); select.cursor = textCursor(); select.cursor.clearSelection(); + + selects.clear(); selects.append(select); } else { // Need to clear current line highlight. if (selects.isEmpty()) { return; } + selects.clear(); } - highlightExtraSelections(); + highlightExtraSelections(true); } void VEdit::setReadOnly(bool p_ro) @@ -417,20 +431,67 @@ void VEdit::setReadOnly(bool p_ro) void VEdit::highlightSelectedWord() { - QString text; - if (vconfig.getHighlightSelectedWord()) { - text = textCursor().selectedText().trimmed(); - if (wordInSearchedSelection(text)) { - qDebug() << "select searched word, just skip"; - text = ""; + QList &selects = m_extraSelections[(int)SelectionId::SelectedWord]; + if (!vconfig.getHighlightSelectedWord()) { + if (!selects.isEmpty()) { + selects.clear(); + highlightExtraSelections(true); } + + return; } + + QString text = textCursor().selectedText().trimmed(); + if (text.isEmpty() || wordInSearchedSelection(text)) { + selects.clear(); + highlightExtraSelections(true); + return; + } + QTextCharFormat format; format.setBackground(m_selectedWordColor); highlightTextAll(text, FindOption::CaseSensitive, SelectionId::SelectedWord, format); } +// Do not highlight trailing spaces with current cursor right behind. +static void trailingSpaceFilter(VEdit *p_editor, QList &p_result) +{ + QTextCursor cursor = p_editor->textCursor(); + if (!cursor.atBlockEnd()) { + return; + } + + int cursorPos = cursor.position(); + for (auto it = p_result.begin(); it != p_result.end(); ++it) { + if (it->cursor.selectionEnd() == cursorPos) { + p_result.erase(it); + + // There will be only one. + return; + } + } +} + +void VEdit::highlightTrailingSpace() +{ + if (!vconfig.getEnableTrailingSpaceHighlight()) { + QList &selects = m_extraSelections[(int)SelectionId::TrailingSapce]; + if (!selects.isEmpty()) { + selects.clear(); + highlightExtraSelections(true); + } + return; + } + + QTextCharFormat format; + format.setBackground(m_trailingSpaceColor); + QString text("\\s+$"); + highlightTextAll(text, FindOption::RegularExpression, + SelectionId::TrailingSapce, format, + trailingSpaceFilter); +} + bool VEdit::wordInSearchedSelection(const QString &p_text) { QString text = p_text.trimmed(); @@ -444,24 +505,9 @@ bool VEdit::wordInSearchedSelection(const QString &p_text) return false; } -void VEdit::triggerHighlightSelectedWord() -{ - QList &selects = m_extraSelections[(int)SelectionId::SelectedWord]; - if (!vconfig.getHighlightSelectedWord() && selects.isEmpty()) { - return; - } - m_selectedWordTimer->stop(); - QString text = textCursor().selectedText().trimmed(); - if (text.isEmpty()) { - // Clear previous selection right now. - highlightSelectedWord(); - } else { - m_selectedWordTimer->start(); - } -} - void VEdit::highlightTextAll(const QString &p_text, uint p_options, - SelectionId p_id, QTextCharFormat p_format) + SelectionId p_id, QTextCharFormat p_format, + void (*p_filter)(VEdit *, QList &)) { QList &selects = m_extraSelections[(int)p_id]; if (!p_text.isEmpty()) { @@ -474,32 +520,42 @@ void VEdit::highlightTextAll(const QString &p_text, uint p_options, select.cursor = occurs[i]; selects.append(select); } - qDebug() << "highlight" << selects.size() << "occurences of" << p_text; } else { if (selects.isEmpty()) { return; } selects.clear(); } + + if (p_filter) { + p_filter(this, selects); + } + highlightExtraSelections(); } void VEdit::highlightSearchedWord(const QString &p_text, uint p_options) { + QList &selects = m_extraSelections[(int)SelectionId::SearchedKeyword]; + if (!vconfig.getHighlightSearchedWord() || p_text.isEmpty()) { + if (!selects.isEmpty()) { + selects.clear(); + highlightExtraSelections(true); + } + + return; + } + QTextCharFormat format; format.setBackground(m_searchedWordColor); - QString text; - if (vconfig.getHighlightSearchedWord()) { - text = p_text; - } - highlightTextAll(text, p_options, SelectionId::SearchedKeyword, format); + highlightTextAll(p_text, p_options, SelectionId::SearchedKeyword, format); } void VEdit::clearSearchedWordHighlight() { QList &selects = m_extraSelections[(int)SelectionId::SearchedKeyword]; selects.clear(); - highlightExtraSelections(); + highlightExtraSelections(true); } void VEdit::contextMenuEvent(QContextMenuEvent *p_event) @@ -569,3 +625,29 @@ VFile *VEdit::getFile() const return m_file; } +void VEdit::handleCursorPositionChanged() +{ + static QTextCursor lastCursor; + + QTextCursor cursor = textCursor(); + if (lastCursor.isNull() || cursor.blockNumber() != lastCursor.blockNumber()) { + highlightCurrentLine(); + highlightTrailingSpace(); + } else { + // Judge whether we have trailing space at current line. + QString text = cursor.block().text(); + if (text.rbegin()->isSpace()) { + highlightTrailingSpace(); + } + + // Handle word-wrap in one block. + // Highlight current line if in different visual line. + if ((lastCursor.positionInBlock() - lastCursor.columnNumber()) != + (cursor.positionInBlock() - cursor.columnNumber())) { + highlightCurrentLine(); + } + } + + lastCursor = cursor; +} + diff --git a/src/vedit.h b/src/vedit.h index c347eb12..4642b98c 100644 --- a/src/vedit.h +++ b/src/vedit.h @@ -19,6 +19,7 @@ enum class SelectionId { CurrentLine = 0, SelectedWord, SearchedKeyword, + TrailingSapce, MaxSelection }; @@ -57,11 +58,12 @@ signals: private slots: void labelTimerTimeout(); - void triggerHighlightSelectedWord(); void highlightSelectedWord(); void handleSaveExitAct(); void handleDiscardExitAct(); void handleEditAct(); + void highlightTrailingSpace(); + void handleCursorPositionChanged(); protected slots: virtual void highlightCurrentLine(); @@ -77,19 +79,35 @@ protected: private: QLabel *m_wrapLabel; QTimer *m_labelTimer; - // highlightExtraSelections() will highlight these selections. + + // doHighlightExtraSelections() will highlight these selections. // Selections are indexed by SelectionId. QVector > m_extraSelections; - QTimer *m_selectedWordTimer; + QColor m_selectedWordColor; QColor m_searchedWordColor; + QColor m_trailingSpaceColor; + + // Timer for extra selections highlight. + QTimer *m_highlightTimer; void showWrapLabel(); - void highlightExtraSelections(); + + // Trigger the timer to request highlight. + // If @p_now is true, stop the timer and highlight immediately. + void highlightExtraSelections(bool p_now = false); + + // Do the real work to highlight extra selections. + void doHighlightExtraSelections(); + // Find all the occurences of @p_text. QList findTextAll(const QString &p_text, uint p_options); + + // @p_fileter: a function to filter out highlight results. void highlightTextAll(const QString &p_text, uint p_options, - SelectionId p_id, QTextCharFormat p_format); + SelectionId p_id, QTextCharFormat p_format, + void (*p_filter)(VEdit *, QList &) = NULL); + void highlightSearchedWord(const QString &p_text, uint p_options); bool wordInSearchedSelection(const QString &p_text); }; diff --git a/src/vmainwindow.cpp b/src/vmainwindow.cpp index b5a16b0f..d6c1784f 100644 --- a/src/vmainwindow.cpp +++ b/src/vmainwindow.cpp @@ -600,6 +600,13 @@ void VMainWindow::initEditMenu() connect(selectedWordAct, &QAction::triggered, this, &VMainWindow::changeHighlightSelectedWord); + // Highlight trailing space. + QAction *trailingSapceAct = new QAction(tr("Highlight Trailing Sapces"), this); + trailingSapceAct->setToolTip(tr("Highlight all the spaces at the end of a line")); + trailingSapceAct->setCheckable(true); + connect(trailingSapceAct, &QAction::triggered, + this, &VMainWindow::changeHighlightTrailingSapce); + editMenu->addAction(m_insertImageAct); editMenu->addSeparator(); m_insertImageAct->setEnabled(false); @@ -672,6 +679,9 @@ void VMainWindow::initEditMenu() editMenu->addAction(selectedWordAct); selectedWordAct->setChecked(vconfig.getHighlightSelectedWord()); + + editMenu->addAction(trailingSapceAct); + trailingSapceAct->setChecked(vconfig.getEnableTrailingSpaceHighlight()); } void VMainWindow::initDockWindows() @@ -784,6 +794,11 @@ void VMainWindow::changeHighlightSearchedWord(bool p_checked) vconfig.setHighlightSearchedWord(p_checked); } +void VMainWindow::changeHighlightTrailingSapce(bool p_checked) +{ + vconfig.setEnableTrailingSapceHighlight(p_checked); +} + void VMainWindow::setTabStopWidth(QAction *action) { if (!action) { diff --git a/src/vmainwindow.h b/src/vmainwindow.h index 097c3048..fa53c570 100644 --- a/src/vmainwindow.h +++ b/src/vmainwindow.h @@ -59,6 +59,7 @@ private slots: void changeHighlightCursorLine(bool p_checked); void changeHighlightSelectedWord(bool p_checked); void changeHighlightSearchedWord(bool p_checked); + void changeHighlightTrailingSapce(bool p_checked); void handleCurTabStatusChanged(const VFile *p_file, const VEditTab *p_editTab, bool p_editMode); void onePanelView(); void twoPanelView(); diff --git a/src/vstyleparser.cpp b/src/vstyleparser.cpp index 8d168ce1..5b2ded79 100644 --- a/src/vstyleparser.cpp +++ b/src/vstyleparser.cpp @@ -186,8 +186,10 @@ void VStyleParser::fetchMarkdownEditorStyles(QPalette &palette, QFont &font, QMap> &styles) const { QString ruleKey; + // editor pmh_style_attribute *editorStyles = markdownStyles->editor_styles; + ruleKey = "editor"; while (editorStyles) { switch (editorStyles->type) { case pmh_attr_type_foreground_color: @@ -210,6 +212,16 @@ void VStyleParser::fetchMarkdownEditorStyles(QPalette &palette, QFont &font, break; } + // Get custom styles: + // trailing-space. + case pmh_attr_type_other: + { + QString attrName(editorStyles->name); + QString value(editorStyles->value->string); + styles[ruleKey][attrName] = value; + break; + } + default: qWarning() << "unimplemented editor attr type:" << editorStyles->type; } @@ -229,6 +241,8 @@ void VStyleParser::fetchMarkdownEditorStyles(QPalette &palette, QFont &font, break; } + // Get custom styles: + // vim-background. case pmh_attr_type_other: { QString attrName(curLineStyles->name);