mirror of
https://gitee.com/vnotex/vnote.git
synced 2025-07-05 13:59:52 +08:00
Editor: support completion
This commit is contained in:
parent
284cba698f
commit
10a1e9c1a8
@ -66,7 +66,18 @@ Zoom in/out the page through the mouse scroll.
|
||||
Recover the page zoom factor to 100%.
|
||||
- `Ctrl+J/K`
|
||||
Scroll page down/up without changing cursor.
|
||||
|
||||
- `Ctrl+N/P`
|
||||
Activate auto-completion.
|
||||
- `Ctrl+N/P`
|
||||
Navigate through the completion list and insert current completion.
|
||||
- `Ctrl+J/K`
|
||||
Navigate through the completion list.
|
||||
- `Ctrl+E`
|
||||
Cancel completion.
|
||||
- `Enter`
|
||||
Insert current completion.
|
||||
- `Ctrl+[` or `Escape`
|
||||
Finish completion.
|
||||
|
||||
#### Text Editing
|
||||
- `Ctrl+B`
|
||||
|
@ -66,6 +66,18 @@
|
||||
恢复页面大小为100%。
|
||||
- `Ctrl+J/K`
|
||||
向下/向上滚动页面,不会改变光标。
|
||||
- `Ctrl+N/P`
|
||||
激活自动补全。
|
||||
- `Ctrl+N/P`
|
||||
浏览补全列表并插入当前补全。
|
||||
- `Ctrl+J/K`
|
||||
浏览补全列表。
|
||||
- `Ctrl+E`
|
||||
取消补全。
|
||||
- `Enter`
|
||||
插入补全。
|
||||
- `Ctrl+[` or `Escape`
|
||||
结束补全。
|
||||
|
||||
#### 文本编辑
|
||||
- `Ctrl+B`
|
||||
|
@ -9,7 +9,7 @@ mdhl_file=v_detorte.mdhl
|
||||
css_file=v_detorte.css
|
||||
codeblock_css_file=v_detorte_codeblock.css
|
||||
mermaid_css_file=v_detorte_mermaid.css
|
||||
version=7
|
||||
version=8
|
||||
|
||||
; This mapping will be used to translate colors when the content of HTML is copied
|
||||
; without background. You could just specify the foreground colors mapping here.
|
||||
@ -308,6 +308,18 @@ listview_item_selected_avtive_bg=@active_bg
|
||||
listview_item_selected_inactive_fg=@inactive_fg
|
||||
listview_item_selected_inactive_bg=@inactive_bg
|
||||
|
||||
; QAbstractItemView for TextEdit Completer.
|
||||
abstractitemview_textedit_fg=#000000
|
||||
abstractitemview_textedit_bg=#BCBCBC
|
||||
abstractitemview_textedit_item_hover_fg=@abstractitemview_textedit_fg
|
||||
abstractitemview_textedit_item_hover_bg=@master_hover_bg
|
||||
abstractitemview_textedit_item_selected_fg=@abstractitemview_textedit_fg
|
||||
abstractitemview_textedit_item_selected_bg=@master_light_bg
|
||||
abstractitemview_textedit_item_selected_avtive_fg=@abstractitemview_textedit_fg
|
||||
abstractitemview_textedit_item_selected_avtive_bg=@master_focus_bg
|
||||
abstractitemview_textedit_item_selected_inactive_fg=@inactive_fg
|
||||
abstractitemview_textedit_item_selected_inactive_bg=@inactive_bg
|
||||
|
||||
; Splitter.
|
||||
splitter_handle_bg=@border_bg
|
||||
splitter_handle_pressed_bg=@pressed_bg
|
||||
|
@ -930,6 +930,46 @@ QListView::item:disabled {
|
||||
}
|
||||
/* End QListView */
|
||||
|
||||
/* QAbstractItemView for TextEdit Completer popup*/
|
||||
QAbstractItemView[TextEdit="true"] {
|
||||
color: @abstractitemview_textedit_fg;
|
||||
background: @abstractitemview_textedit_bg;
|
||||
show-decoration-selected: 0;
|
||||
border: none;
|
||||
selection-background-color: transparent;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
QAbstractItemView[TextEdit="true"]::item {
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
QAbstractItemView[TextEdit="true"]::item:hover {
|
||||
color: @abstractitemview_textedit_item_hover_fg;
|
||||
background: @abstractitemview_textedit_item_hover_bg;
|
||||
}
|
||||
|
||||
QAbstractItemView[TextEdit="true"]::item:selected {
|
||||
color: @abstractitemview_textedit_item_selected_fg;
|
||||
background: @abstractitemview_textedit_item_selected_bg;
|
||||
}
|
||||
|
||||
QAbstractItemView[TextEdit="true"]::item:selected:active {
|
||||
color: @abstractitemview_textedit_item_selected_active_fg;
|
||||
background: @abstractitemview_textedit_item_selected_active_bg;
|
||||
}
|
||||
|
||||
QAbstractItemView[TextEdit="true"]::item:selected:!active {
|
||||
color: @abstractitemview_textedit_item_selected_inactive_fg;
|
||||
background: @abstractitemview_textedit_item_selected_inactive_bg;
|
||||
}
|
||||
|
||||
QAbstractItemView[TextEdit="true"]::item:disabled {
|
||||
background: transparent;
|
||||
}
|
||||
/* End QAbstractItemView */
|
||||
|
||||
/* QSplitter */
|
||||
QSplitter#MainSplitter {
|
||||
border: none;
|
||||
|
@ -7,7 +7,7 @@ mdhl_file=v_moonlight.mdhl
|
||||
css_file=v_moonlight.css
|
||||
codeblock_css_file=v_moonlight_codeblock.css
|
||||
mermaid_css_file=v_moonlight_mermaid.css
|
||||
version=18
|
||||
version=19
|
||||
|
||||
; This mapping will be used to translate colors when the content of HTML is copied
|
||||
; without background. You could just specify the foreground colors mapping here.
|
||||
@ -306,6 +306,18 @@ listview_item_selected_avtive_bg=@active_bg
|
||||
listview_item_selected_inactive_fg=@inactive_fg
|
||||
listview_item_selected_inactive_bg=@inactive_bg
|
||||
|
||||
; QAbstractItemView for TextEdit Completer.
|
||||
abstractitemview_textedit_fg=@content_fg
|
||||
abstractitemview_textedit_bg=#323841
|
||||
abstractitemview_textedit_item_hover_fg=@master_fg
|
||||
abstractitemview_textedit_item_hover_bg=@master_hover_bg
|
||||
abstractitemview_textedit_item_selected_fg=@master_fg
|
||||
abstractitemview_textedit_item_selected_bg=@master_bg
|
||||
abstractitemview_textedit_item_selected_avtive_fg=@master_fg
|
||||
abstractitemview_textedit_item_selected_avtive_bg=@master_focus_bg
|
||||
abstractitemview_textedit_item_selected_inactive_fg=@inactive_fg
|
||||
abstractitemview_textedit_item_selected_inactive_bg=@inactive_bg
|
||||
|
||||
; Splitter.
|
||||
splitter_handle_bg=@border_bg
|
||||
splitter_handle_pressed_bg=@pressed_bg
|
||||
|
@ -930,6 +930,46 @@ QListView::item:disabled {
|
||||
}
|
||||
/* End QListView */
|
||||
|
||||
/* QAbstractItemView for TextEdit Completer popup*/
|
||||
QAbstractItemView[TextEdit="true"] {
|
||||
color: @abstractitemview_textedit_fg;
|
||||
background: @abstractitemview_textedit_bg;
|
||||
show-decoration-selected: 0;
|
||||
border: none;
|
||||
selection-background-color: transparent;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
QAbstractItemView[TextEdit="true"]::item {
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
QAbstractItemView[TextEdit="true"]::item:hover {
|
||||
color: @abstractitemview_textedit_item_hover_fg;
|
||||
background: @abstractitemview_textedit_item_hover_bg;
|
||||
}
|
||||
|
||||
QAbstractItemView[TextEdit="true"]::item:selected {
|
||||
color: @abstractitemview_textedit_item_selected_fg;
|
||||
background: @abstractitemview_textedit_item_selected_bg;
|
||||
}
|
||||
|
||||
QAbstractItemView[TextEdit="true"]::item:selected:active {
|
||||
color: @abstractitemview_textedit_item_selected_active_fg;
|
||||
background: @abstractitemview_textedit_item_selected_active_bg;
|
||||
}
|
||||
|
||||
QAbstractItemView[TextEdit="true"]::item:selected:!active {
|
||||
color: @abstractitemview_textedit_item_selected_inactive_fg;
|
||||
background: @abstractitemview_textedit_item_selected_inactive_bg;
|
||||
}
|
||||
|
||||
QAbstractItemView[TextEdit="true"]::item:disabled {
|
||||
background: transparent;
|
||||
}
|
||||
/* End QAbstractItemView */
|
||||
|
||||
/* QSplitter */
|
||||
QSplitter#MainSplitter {
|
||||
border: none;
|
||||
|
@ -7,7 +7,7 @@ mdhl_file=v_native.mdhl
|
||||
css_file=v_native.css
|
||||
codeblock_css_file=v_native_codeblock.css
|
||||
mermaid_css_file=v_native_mermaid.css
|
||||
version=15
|
||||
version=16
|
||||
|
||||
[phony]
|
||||
; Abstract color attributes.
|
||||
|
@ -510,6 +510,12 @@ QListView::item {
|
||||
}
|
||||
/* End QListView */
|
||||
|
||||
/* QAbstractItemView for TextEdit Completer popup*/
|
||||
QAbstractItemView[TextEdit="true"] {
|
||||
border: 1px solid @border_bg;
|
||||
}
|
||||
/* End QAbstractItemView */
|
||||
|
||||
/* QSplitter */
|
||||
QSplitter {
|
||||
border: none;
|
||||
|
@ -7,7 +7,7 @@ mdhl_file=v_pure.mdhl
|
||||
css_file=v_pure.css
|
||||
codeblock_css_file=v_pure_codeblock.css
|
||||
mermaid_css_file=v_pure_mermaid.css
|
||||
version=16
|
||||
version=17
|
||||
|
||||
[phony]
|
||||
; Abstract color attributes.
|
||||
@ -300,6 +300,18 @@ listview_item_selected_avtive_bg=@active_bg
|
||||
listview_item_selected_inactive_fg=@inactive_fg
|
||||
listview_item_selected_inactive_bg=@inactive_bg
|
||||
|
||||
; QAbstractItemView for TextEdit Completer.
|
||||
abstractitemview_textedit_fg=#content_fg
|
||||
abstractitemview_textedit_bg=#DADADA
|
||||
abstractitemview_textedit_item_hover_fg=@abstractitemview_textedit_fg
|
||||
abstractitemview_textedit_item_hover_bg=@master_hover_bg
|
||||
abstractitemview_textedit_item_selected_fg=@abstractitemview_textedit_fg
|
||||
abstractitemview_textedit_item_selected_bg=@master_light_bg
|
||||
abstractitemview_textedit_item_selected_avtive_fg=@abstractitemview_textedit_fg
|
||||
abstractitemview_textedit_item_selected_avtive_bg=@master_focus_bg
|
||||
abstractitemview_textedit_item_selected_inactive_fg=@inactive_fg
|
||||
abstractitemview_textedit_item_selected_inactive_bg=@inactive_bg
|
||||
|
||||
; Splitter.
|
||||
splitter_handle_bg=@border_bg
|
||||
splitter_handle_pressed_bg=@pressed_bg
|
||||
|
@ -930,6 +930,46 @@ QListView::item:disabled {
|
||||
}
|
||||
/* End QListView */
|
||||
|
||||
/* QAbstractItemView for TextEdit Completer popup*/
|
||||
QAbstractItemView[TextEdit="true"] {
|
||||
color: @abstractitemview_textedit_fg;
|
||||
background: @abstractitemview_textedit_bg;
|
||||
show-decoration-selected: 0;
|
||||
border: none;
|
||||
selection-background-color: transparent;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
QAbstractItemView[TextEdit="true"]::item {
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
QAbstractItemView[TextEdit="true"]::item:hover {
|
||||
color: @abstractitemview_textedit_item_hover_fg;
|
||||
background: @abstractitemview_textedit_item_hover_bg;
|
||||
}
|
||||
|
||||
QAbstractItemView[TextEdit="true"]::item:selected {
|
||||
color: @abstractitemview_textedit_item_selected_fg;
|
||||
background: @abstractitemview_textedit_item_selected_bg;
|
||||
}
|
||||
|
||||
QAbstractItemView[TextEdit="true"]::item:selected:active {
|
||||
color: @abstractitemview_textedit_item_selected_active_fg;
|
||||
background: @abstractitemview_textedit_item_selected_active_bg;
|
||||
}
|
||||
|
||||
QAbstractItemView[TextEdit="true"]::item:selected:!active {
|
||||
color: @abstractitemview_textedit_item_selected_inactive_fg;
|
||||
background: @abstractitemview_textedit_item_selected_inactive_bg;
|
||||
}
|
||||
|
||||
QAbstractItemView[TextEdit="true"]::item:disabled {
|
||||
background: transparent;
|
||||
}
|
||||
/* End QAbstractItemView */
|
||||
|
||||
/* QSplitter */
|
||||
QSplitter#MainSplitter {
|
||||
border: none;
|
||||
|
@ -142,7 +142,8 @@ SOURCES += main.cpp\
|
||||
vtagexplorer.cpp \
|
||||
pegmarkdownhighlighter.cpp \
|
||||
pegparser.cpp \
|
||||
peghighlighterresult.cpp
|
||||
peghighlighterresult.cpp \
|
||||
vtexteditcompleter.cpp
|
||||
|
||||
HEADERS += vmainwindow.h \
|
||||
vdirectorytree.h \
|
||||
@ -279,7 +280,8 @@ HEADERS += vmainwindow.h \
|
||||
markdownhighlighterdata.h \
|
||||
pegmarkdownhighlighter.h \
|
||||
pegparser.h \
|
||||
peghighlighterresult.h
|
||||
peghighlighterresult.h \
|
||||
vtexteditcompleter.h
|
||||
|
||||
RESOURCES += \
|
||||
vnote.qrc \
|
||||
|
@ -10,10 +10,13 @@
|
||||
#include <QPair>
|
||||
#include <QSplitter>
|
||||
#include <QStack>
|
||||
#include <QSharedPointer>
|
||||
|
||||
#include "vnotebook.h"
|
||||
#include "veditwindow.h"
|
||||
#include "vnavigationmode.h"
|
||||
#include "vfilesessioninfo.h"
|
||||
#include "vtexteditcompleter.h"
|
||||
|
||||
class VFile;
|
||||
class VDirectory;
|
||||
@ -95,6 +98,8 @@ public:
|
||||
// Distribute all the splits evenly.
|
||||
void distributeSplits();
|
||||
|
||||
QSharedPointer<VTextEditCompleter> getCompleter() const;
|
||||
|
||||
signals:
|
||||
// Emit when current window's tab status updated.
|
||||
void tabStatusUpdated(const VEditTabInfo &p_info);
|
||||
@ -244,6 +249,8 @@ private:
|
||||
bool m_autoSave;
|
||||
|
||||
VMathJaxPreviewHelper *m_mathPreviewHelper;
|
||||
|
||||
QSharedPointer<VTextEditCompleter> m_completer;
|
||||
};
|
||||
|
||||
inline VEditWindow* VEditArea::getWindow(int windowIndex) const
|
||||
@ -266,4 +273,14 @@ inline VMathJaxPreviewHelper *VEditArea::getMathJaxPreviewHelper() const
|
||||
{
|
||||
return m_mathPreviewHelper;
|
||||
}
|
||||
|
||||
inline QSharedPointer<VTextEditCompleter> VEditArea::getCompleter() const
|
||||
{
|
||||
if (m_completer.isNull()) {
|
||||
VEditArea *ea = const_cast<VEditArea *>(this);
|
||||
ea->m_completer.reset(new VTextEditCompleter(ea));
|
||||
}
|
||||
|
||||
return m_completer;
|
||||
}
|
||||
#endif // VEDITAREA_H
|
||||
|
128
src/veditor.cpp
128
src/veditor.cpp
@ -15,7 +15,9 @@ extern VConfigManager *g_config;
|
||||
|
||||
extern VMetaWordManager *g_mwMgr;
|
||||
|
||||
VEditor::VEditor(VFile *p_file, QWidget *p_editor)
|
||||
VEditor::VEditor(VFile *p_file,
|
||||
QWidget *p_editor,
|
||||
const QSharedPointer<VTextEditCompleter> &p_completer)
|
||||
: m_editor(p_editor),
|
||||
m_object(new VEditorObject(this, p_editor)),
|
||||
m_file(p_file),
|
||||
@ -23,12 +25,16 @@ VEditor::VEditor(VFile *p_file, QWidget *p_editor)
|
||||
m_document(nullptr),
|
||||
m_enableInputMethod(true),
|
||||
m_timeStamp(0),
|
||||
m_trailingSpaceSelectionTS(0)
|
||||
m_trailingSpaceSelectionTS(0),
|
||||
m_completer(p_completer)
|
||||
{
|
||||
}
|
||||
|
||||
VEditor::~VEditor()
|
||||
{
|
||||
if (m_completer->widget() == m_editor) {
|
||||
m_completer->setWidget(NULL);
|
||||
}
|
||||
}
|
||||
|
||||
void VEditor::init()
|
||||
@ -161,8 +167,19 @@ void VEditor::highlightOnCursorPositionChanged()
|
||||
} else {
|
||||
// Judge whether we have trailing space at current line.
|
||||
QString text = cursor.block().text();
|
||||
if (text.rbegin()->isSpace()) {
|
||||
updateTrailingSpaceHighlights();
|
||||
if (!text.isEmpty()) {
|
||||
auto it = text.rbegin();
|
||||
bool needUpdate = it->isSpace();
|
||||
if (!needUpdate
|
||||
&& cursor.atBlockEnd()
|
||||
&& text.size() > 1) {
|
||||
++it;
|
||||
needUpdate = it->isSpace();
|
||||
}
|
||||
|
||||
if (needUpdate) {
|
||||
updateTrailingSpaceHighlights();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle word-wrap in one block.
|
||||
@ -228,14 +245,15 @@ void VEditor::filterTrailingSpace(QList<QTextEdit::ExtraSelection> &p_selects,
|
||||
const QList<QTextEdit::ExtraSelection> &p_src)
|
||||
{
|
||||
QTextCursor cursor = textCursorW();
|
||||
if (!cursor.atBlockEnd()) {
|
||||
p_selects.append(p_src);
|
||||
return;
|
||||
}
|
||||
|
||||
int cursorPos = cursor.position();
|
||||
bool blockEnd = cursor.atBlockEnd();
|
||||
int blockNum = cursor.blockNumber();
|
||||
for (auto it = p_src.begin(); it != p_src.end(); ++it) {
|
||||
if (it->cursor.selectionEnd() == cursorPos) {
|
||||
if (blockEnd && it->cursor.blockNumber() == blockNum) {
|
||||
// When cursor is at block end, we do not display any trailing space
|
||||
// at current line.
|
||||
continue;
|
||||
} else if (!it->cursor.atBlockEnd()) {
|
||||
// Obsolete trailing space.
|
||||
continue;
|
||||
} else {
|
||||
p_selects.append(*it);
|
||||
@ -1100,3 +1118,91 @@ bool VEditor::setCursorPosition(int p_blockNumber, int p_posInBlock)
|
||||
setTextCursorW(cursor);
|
||||
return true;
|
||||
}
|
||||
|
||||
static Qt::CaseSensitivity completionCaseSensitivity(const QString &p_text)
|
||||
{
|
||||
bool upperCase = false;
|
||||
for (int i = 0; i < p_text.size(); ++i) {
|
||||
if (p_text[i].isUpper()) {
|
||||
upperCase = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return upperCase ? Qt::CaseSensitive : Qt::CaseInsensitive;
|
||||
}
|
||||
|
||||
void VEditor::requestCompletion(bool p_reversed)
|
||||
{
|
||||
QTextCursor cursor = textCursorW();
|
||||
cursor.clearSelection();
|
||||
setTextCursorW(cursor);
|
||||
|
||||
QStringList words = generateCompletionCandidates();
|
||||
QString prefix = fetchCompletionPrefix();
|
||||
// Smart case.
|
||||
Qt::CaseSensitivity cs = completionCaseSensitivity(prefix);
|
||||
|
||||
cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::MoveAnchor, prefix.size());
|
||||
QRect popupRect = cursorRectW(cursor);
|
||||
popupRect.setLeft(popupRect.left() + lineNumberAreaWidth());
|
||||
|
||||
m_completer->performCompletion(words, prefix, cs, p_reversed, popupRect, this);
|
||||
}
|
||||
|
||||
bool VEditor::isCompletionActivated() const
|
||||
{
|
||||
if (m_completer->widget() == m_editor
|
||||
&& m_completer->isPopupVisible()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
QStringList VEditor::generateCompletionCandidates() const
|
||||
{
|
||||
QString content = getContent();
|
||||
QTextCursor cursor = textCursorW();
|
||||
int start, end;
|
||||
VEditUtils::findCurrentWord(cursor, start, end);
|
||||
|
||||
QRegExp reg("\\W+");
|
||||
|
||||
QStringList ret = content.mid(end).split(reg, QString::SkipEmptyParts);
|
||||
ret.append(content.left(start).split(reg, QString::SkipEmptyParts));
|
||||
ret.removeDuplicates();
|
||||
return ret;
|
||||
}
|
||||
|
||||
QString VEditor::fetchCompletionPrefix() const
|
||||
{
|
||||
QTextCursor cursor = textCursorW();
|
||||
if (cursor.atBlockStart()) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
int pos = cursor.position() - 1;
|
||||
int blockPos = cursor.block().position();
|
||||
QString prefix;
|
||||
while (pos >= blockPos) {
|
||||
QChar ch = m_document->characterAt(pos);
|
||||
if (ch.isSpace()) {
|
||||
break;
|
||||
}
|
||||
|
||||
prefix.prepend(ch);
|
||||
--pos;
|
||||
}
|
||||
|
||||
return prefix;
|
||||
}
|
||||
|
||||
// @p_prefix may be longer than @p_completion.
|
||||
void VEditor::insertCompletion(const QString &p_prefix, const QString &p_completion)
|
||||
{
|
||||
QTextCursor cursor = textCursorW();
|
||||
cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor, p_prefix.size());
|
||||
cursor.insertText(p_completion);
|
||||
setTextCursorW(cursor);
|
||||
}
|
||||
|
@ -6,11 +6,13 @@
|
||||
#include <QList>
|
||||
#include <QTextEdit>
|
||||
#include <QColor>
|
||||
#include <QSharedPointer>
|
||||
|
||||
#include "veditconfig.h"
|
||||
#include "vconstants.h"
|
||||
#include "vfile.h"
|
||||
#include "vwordcountinfo.h"
|
||||
#include "vtexteditcompleter.h"
|
||||
|
||||
class QWidget;
|
||||
class VEditorObject;
|
||||
@ -39,7 +41,9 @@ enum class SelectionId {
|
||||
class VEditor
|
||||
{
|
||||
public:
|
||||
explicit VEditor(VFile *p_file, QWidget *p_editor);
|
||||
explicit VEditor(VFile *p_file,
|
||||
QWidget *p_editor,
|
||||
const QSharedPointer<VTextEditCompleter> &p_completer);
|
||||
|
||||
virtual ~VEditor();
|
||||
|
||||
@ -154,6 +158,15 @@ public:
|
||||
|
||||
virtual bool setCursorPosition(int p_blockNumber, int p_posInBlock);
|
||||
|
||||
QString fetchCompletionPrefix() const;
|
||||
|
||||
// Request text completion.
|
||||
virtual void requestCompletion(bool p_reversed);
|
||||
|
||||
virtual bool isCompletionActivated() const;
|
||||
|
||||
virtual void insertCompletion(const QString &p_prefix, const QString &p_completion);
|
||||
|
||||
// Wrapper functions for QPlainTextEdit/QTextEdit.
|
||||
// Ends with W to distinguish it from the original interfaces.
|
||||
public:
|
||||
@ -201,6 +214,10 @@ public:
|
||||
|
||||
virtual void ensureCursorVisibleW() = 0;
|
||||
|
||||
virtual QRect cursorRectW() = 0;
|
||||
|
||||
virtual QRect cursorRectW(const QTextCursor &p_cursor) = 0;
|
||||
|
||||
protected:
|
||||
void init();
|
||||
|
||||
@ -235,6 +252,8 @@ protected:
|
||||
|
||||
bool handleWheelEvent(QWheelEvent *p_event);
|
||||
|
||||
virtual int lineNumberAreaWidth() const = 0;
|
||||
|
||||
QWidget *m_editor;
|
||||
|
||||
VEditorObject *m_object;
|
||||
@ -288,6 +307,8 @@ private:
|
||||
// Highlight @p_cursor as the searched keyword under cursor.
|
||||
void highlightSearchedWordUnderCursor(const QTextCursor &p_cursor);
|
||||
|
||||
QStringList generateCompletionCandidates() const;
|
||||
|
||||
QLabel *m_wrapLabel;
|
||||
QTimer *m_labelTimer;
|
||||
|
||||
@ -329,6 +350,8 @@ private:
|
||||
|
||||
TimeStamp m_trailingSpaceSelectionTS;
|
||||
|
||||
QSharedPointer<VTextEditCompleter> m_completer;
|
||||
|
||||
// Functions for private slots.
|
||||
private:
|
||||
void labelTimerTimeout();
|
||||
|
@ -3343,3 +3343,8 @@ void VMainWindow::focusEditArea() const
|
||||
|
||||
widget->setFocus();
|
||||
}
|
||||
|
||||
void VMainWindow::setCaptainModeEnabled(bool p_enabled)
|
||||
{
|
||||
m_captain->setCaptainModeEnabled(p_enabled);
|
||||
}
|
||||
|
@ -128,6 +128,8 @@ public:
|
||||
|
||||
VExplorer *getExplorer() const;
|
||||
|
||||
void setCaptainModeEnabled(bool p_enabled);
|
||||
|
||||
signals:
|
||||
// Emit when editor related configurations were changed by user.
|
||||
void editorConfigUpdated();
|
||||
|
@ -431,6 +431,30 @@ bool VMdEditOperations::handleKeyPressEvent(QKeyEvent *p_event)
|
||||
break;
|
||||
}
|
||||
|
||||
case Qt::Key_N:
|
||||
{
|
||||
if (VUtils::isControlModifierForVim(modifiers)) {
|
||||
// Completion.
|
||||
if (!m_editor->isCompletionActivated()) {
|
||||
m_editor->requestCompletion(false);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case Qt::Key_P:
|
||||
{
|
||||
if (VUtils::isControlModifierForVim(modifiers)) {
|
||||
// Completion.
|
||||
if (!m_editor->isCompletionActivated()) {
|
||||
m_editor->requestCompletion(true);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
@ -32,9 +32,10 @@ extern VConfigManager *g_config;
|
||||
VMdEditor::VMdEditor(VFile *p_file,
|
||||
VDocument *p_doc,
|
||||
MarkdownConverterType p_type,
|
||||
const QSharedPointer<VTextEditCompleter> &p_completer,
|
||||
QWidget *p_parent)
|
||||
: VTextEdit(p_parent),
|
||||
VEditor(p_file, this),
|
||||
VEditor(p_file, this, p_completer),
|
||||
m_pegHighlighter(NULL),
|
||||
m_freshEdit(true),
|
||||
m_textToHtmlDialog(NULL),
|
||||
@ -1411,3 +1412,8 @@ void VMdEditor::setFontAndPaletteByStyleSheet(const QFont &p_font, const QPalett
|
||||
.arg(p_palette.color(QPalette::Text).name())
|
||||
.arg(p_palette.color(QPalette::Base).name()));
|
||||
}
|
||||
|
||||
int VMdEditor::lineNumberAreaWidth() const
|
||||
{
|
||||
return VTextEdit::lineNumberAreaWidth();
|
||||
}
|
||||
|
@ -28,6 +28,7 @@ public:
|
||||
VMdEditor(VFile *p_file,
|
||||
VDocument *p_doc,
|
||||
MarkdownConverterType p_type,
|
||||
const QSharedPointer<VTextEditCompleter> &p_completer,
|
||||
QWidget *p_parent = nullptr);
|
||||
|
||||
void beginEdit() Q_DECL_OVERRIDE;
|
||||
@ -189,6 +190,16 @@ public:
|
||||
ensureCursorVisible();
|
||||
}
|
||||
|
||||
QRect cursorRectW() Q_DECL_OVERRIDE
|
||||
{
|
||||
return cursorRect();
|
||||
}
|
||||
|
||||
QRect cursorRectW(const QTextCursor &p_cursor) Q_DECL_OVERRIDE
|
||||
{
|
||||
return cursorRect(p_cursor);
|
||||
}
|
||||
|
||||
signals:
|
||||
// Signal when headers change.
|
||||
void headersChanged(const QVector<VTableOfContentItem> &p_headers);
|
||||
@ -223,6 +234,8 @@ protected:
|
||||
|
||||
void wheelEvent(QWheelEvent *p_event) Q_DECL_OVERRIDE;
|
||||
|
||||
int lineNumberAreaWidth() const Q_DECL_OVERRIDE;
|
||||
|
||||
private slots:
|
||||
// Update m_headers according to elements.
|
||||
void updateHeaders(const QVector<VElementRegion> &p_headerRegions);
|
||||
|
@ -473,7 +473,11 @@ void VMdTab::setupMarkdownEditor()
|
||||
{
|
||||
Q_ASSERT(!m_editor);
|
||||
|
||||
m_editor = new VMdEditor(m_file, m_document, m_mdConType, this);
|
||||
m_editor = new VMdEditor(m_file,
|
||||
m_document,
|
||||
m_mdConType,
|
||||
m_editArea->getCompleter(),
|
||||
this);
|
||||
m_editor->setProperty("MainEditor", true);
|
||||
m_editor->setEditTab(this);
|
||||
int delta = g_config->getEditorZoomDelta();
|
||||
|
@ -459,3 +459,8 @@ void VTextEdit::dragMoveEvent(QDragMoveEvent *p_event)
|
||||
// TODO: find out the rect of current cursor to update that rect only.
|
||||
update();
|
||||
}
|
||||
|
||||
int VTextEdit::lineNumberAreaWidth() const
|
||||
{
|
||||
return m_lineNumberArea->width();
|
||||
}
|
||||
|
@ -81,6 +81,8 @@ protected:
|
||||
// Return the Y offset of the content via the scrollbar.
|
||||
int contentOffsetY() const;
|
||||
|
||||
int lineNumberAreaWidth() const;
|
||||
|
||||
void updateLineNumberAreaWidth(const QFontMetrics &p_metrics);
|
||||
|
||||
void dragMoveEvent(QDragMoveEvent *p_event) Q_DECL_OVERRIDE;
|
||||
|
321
src/vtexteditcompleter.cpp
Normal file
321
src/vtexteditcompleter.cpp
Normal file
@ -0,0 +1,321 @@
|
||||
#include "vtexteditcompleter.h"
|
||||
|
||||
#include <QStringListModel>
|
||||
#include <QStyledItemDelegate>
|
||||
#include <QScrollBar>
|
||||
#include <QDebug>
|
||||
#include <QEvent>
|
||||
#include <QKeyEvent>
|
||||
|
||||
#include "utils/vutils.h"
|
||||
#include "veditor.h"
|
||||
#include "vmainwindow.h"
|
||||
|
||||
extern VMainWindow *g_mainWin;
|
||||
|
||||
VTextEditCompleter::VTextEditCompleter(QObject *p_parent)
|
||||
: QCompleter(p_parent),
|
||||
m_initialized(false)
|
||||
{
|
||||
}
|
||||
|
||||
void VTextEditCompleter::performCompletion(const QStringList &p_words,
|
||||
const QString &p_prefix,
|
||||
Qt::CaseSensitivity p_cs,
|
||||
bool p_reversed,
|
||||
const QRect &p_rect,
|
||||
VEditor *p_editor)
|
||||
{
|
||||
init();
|
||||
|
||||
m_editor = p_editor;
|
||||
|
||||
setWidget(m_editor->getEditor());
|
||||
|
||||
m_model->setStringList(p_words);
|
||||
setCaseSensitivity(p_cs);
|
||||
setCompletionPrefix(p_prefix);
|
||||
|
||||
int cnt = completionCount();
|
||||
if (cnt == 0) {
|
||||
finishCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
selectRow(p_reversed ? cnt - 1 : 0);
|
||||
|
||||
if (cnt == 1 && currentCompletion() == p_prefix) {
|
||||
finishCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
g_mainWin->setCaptainModeEnabled(false);
|
||||
|
||||
m_insertedCompletion = p_prefix;
|
||||
insertCurrentCompletion();
|
||||
|
||||
auto pu = popup();
|
||||
QRect rt(p_rect);
|
||||
rt.setWidth(pu->sizeHintForColumn(0) + pu->verticalScrollBar()->sizeHint().width());
|
||||
complete(rt);
|
||||
}
|
||||
|
||||
void VTextEditCompleter::init()
|
||||
{
|
||||
if (m_initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_initialized = true;
|
||||
|
||||
m_model = new QStringListModel(this);
|
||||
setModel(m_model);
|
||||
|
||||
popup()->setProperty("TextEdit", true);
|
||||
popup()->setItemDelegate(new QStyledItemDelegate(this));
|
||||
|
||||
connect(this, static_cast<void(QCompleter::*)(const QString &)>(&QCompleter::activated),
|
||||
this, [this](const QString &p_text) {
|
||||
insertCompletion(p_text);
|
||||
finishCompletion();
|
||||
});
|
||||
}
|
||||
|
||||
void VTextEditCompleter::selectNextCompletion(bool p_reversed)
|
||||
{
|
||||
QModelIndex curIndex = popup()->currentIndex();
|
||||
if (p_reversed) {
|
||||
if (curIndex.isValid()) {
|
||||
int row = curIndex.row();
|
||||
if (row == 0) {
|
||||
setCurrentIndex(QModelIndex());
|
||||
} else {
|
||||
selectRow(row - 1);
|
||||
}
|
||||
} else {
|
||||
selectRow(completionCount() - 1);
|
||||
}
|
||||
} else {
|
||||
if (curIndex.isValid()) {
|
||||
int row = curIndex.row();
|
||||
if (!selectRow(row + 1)) {
|
||||
setCurrentIndex(QModelIndex());
|
||||
}
|
||||
} else {
|
||||
selectRow(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool VTextEditCompleter::selectRow(int p_row)
|
||||
{
|
||||
if (setCurrentRow(p_row)) {
|
||||
setCurrentIndex(currentIndex());
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void VTextEditCompleter::setCurrentIndex(QModelIndex p_index, bool p_select)
|
||||
{
|
||||
auto pu = popup();
|
||||
if (!pu) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!p_select) {
|
||||
pu->selectionModel()->setCurrentIndex(p_index, QItemSelectionModel::NoUpdate);
|
||||
} else {
|
||||
if (!p_index.isValid()) {
|
||||
pu->selectionModel()->clear();
|
||||
} else {
|
||||
pu->selectionModel()->setCurrentIndex(p_index, QItemSelectionModel::ClearAndSelect);
|
||||
}
|
||||
}
|
||||
|
||||
p_index = pu->selectionModel()->currentIndex();
|
||||
if (!p_index.isValid()) {
|
||||
pu->scrollToTop();
|
||||
} else {
|
||||
pu->scrollTo(p_index);
|
||||
}
|
||||
}
|
||||
|
||||
bool VTextEditCompleter::eventFilter(QObject *p_obj, QEvent *p_eve)
|
||||
{
|
||||
switch (p_eve->type()) {
|
||||
case QEvent::KeyPress:
|
||||
{
|
||||
if (p_obj != popup() || !m_editor) {
|
||||
break;
|
||||
}
|
||||
|
||||
bool exited = false;
|
||||
|
||||
QKeyEvent *ke = static_cast<QKeyEvent *>(p_eve);
|
||||
const int key = ke->key();
|
||||
const int modifiers = ke->modifiers();
|
||||
switch (key) {
|
||||
case Qt::Key_N:
|
||||
V_FALLTHROUGH;
|
||||
case Qt::Key_P:
|
||||
if (VUtils::isControlModifierForVim(modifiers)) {
|
||||
selectNextCompletion(key == Qt::Key_P);
|
||||
insertCurrentCompletion();
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
|
||||
case Qt::Key_J:
|
||||
V_FALLTHROUGH;
|
||||
case Qt::Key_K:
|
||||
if (VUtils::isControlModifierForVim(modifiers)) {
|
||||
selectNextCompletion(key == Qt::Key_K);
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
|
||||
case Qt::Key_BracketLeft:
|
||||
if (!VUtils::isControlModifierForVim(modifiers)) {
|
||||
break;
|
||||
}
|
||||
V_FALLTHROUGH;
|
||||
case Qt::Key_Escape:
|
||||
exited = true;
|
||||
// Propogate this event to the editor widget for Vim mode.
|
||||
if (!m_editor->getVim()) {
|
||||
finishCompletion();
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
|
||||
case Qt::Key_E:
|
||||
if (VUtils::isControlModifierForVim(modifiers)) {
|
||||
cancelCompletion();
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
|
||||
case Qt::Key_Enter:
|
||||
case Qt::Key_Return:
|
||||
{
|
||||
if (m_insertedCompletion != currentCompletion()) {
|
||||
insertCurrentCompletion();
|
||||
finishCompletion();
|
||||
return true;
|
||||
} else {
|
||||
exited = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
int cursorPos = -1;
|
||||
if (!exited) {
|
||||
cursorPos = m_editor->textCursorW().position();
|
||||
}
|
||||
|
||||
bool ret = QCompleter::eventFilter(p_obj, p_eve);
|
||||
if (!exited) {
|
||||
// Detect if cursor position changed after key press.
|
||||
int pos = m_editor->textCursorW().position();
|
||||
if (pos == cursorPos - 1) {
|
||||
// Deleted one char.
|
||||
if (m_insertedCompletion.size() > 1) {
|
||||
updatePrefix(m_editor->fetchCompletionPrefix());
|
||||
} else {
|
||||
exited = true;
|
||||
}
|
||||
} else if (pos == cursorPos + 1) {
|
||||
// Added one char.
|
||||
QString prefix = m_editor->fetchCompletionPrefix();
|
||||
if (prefix.size() == m_insertedCompletion.size() + 1
|
||||
&& prefix.startsWith(m_insertedCompletion)) {
|
||||
updatePrefix(prefix);
|
||||
} else {
|
||||
exited = true;
|
||||
}
|
||||
} else if (pos != cursorPos) {
|
||||
exited = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (exited) {
|
||||
// finishCompletion() will do clean up. Must be called after QCompleter::eventFilter().
|
||||
finishCompletion();
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
case QEvent::Hide:
|
||||
{
|
||||
// Completion exited.
|
||||
cleanUp();
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return QCompleter::eventFilter(p_obj, p_eve);
|
||||
}
|
||||
|
||||
void VTextEditCompleter::insertCurrentCompletion()
|
||||
{
|
||||
QString completion;
|
||||
QModelIndex curIndex = popup()->currentIndex();
|
||||
if (curIndex.isValid()) {
|
||||
completion = currentCompletion();
|
||||
} else {
|
||||
completion = completionPrefix();
|
||||
}
|
||||
|
||||
insertCompletion(completion);
|
||||
}
|
||||
|
||||
void VTextEditCompleter::insertCompletion(const QString &p_completion)
|
||||
{
|
||||
if (m_insertedCompletion == p_completion) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_editor->insertCompletion(m_insertedCompletion, p_completion);
|
||||
m_insertedCompletion = p_completion;
|
||||
}
|
||||
|
||||
void VTextEditCompleter::cancelCompletion()
|
||||
{
|
||||
insertCompletion(completionPrefix());
|
||||
|
||||
finishCompletion();
|
||||
}
|
||||
|
||||
void VTextEditCompleter::cleanUp()
|
||||
{
|
||||
// Do not clean up m_editor and m_insertedCompletion, since activated()
|
||||
// signal is after the HideEvent.
|
||||
setWidget(NULL);
|
||||
g_mainWin->setCaptainModeEnabled(true);
|
||||
}
|
||||
|
||||
void VTextEditCompleter::updatePrefix(const QString &p_prefix)
|
||||
{
|
||||
m_insertedCompletion = p_prefix;
|
||||
setCompletionPrefix(p_prefix);
|
||||
|
||||
int cnt = completionCount();
|
||||
if (cnt == 0) {
|
||||
finishCompletion();
|
||||
} else if (cnt == 1) {
|
||||
setCurrentRow(0);
|
||||
if (currentCompletion() == p_prefix) {
|
||||
finishCompletion();
|
||||
}
|
||||
}
|
||||
}
|
68
src/vtexteditcompleter.h
Normal file
68
src/vtexteditcompleter.h
Normal file
@ -0,0 +1,68 @@
|
||||
#ifndef VTEXTEDITCOMPLETER_H
|
||||
#define VTEXTEDITCOMPLETER_H
|
||||
|
||||
#include <QCompleter>
|
||||
#include <QAbstractItemView>
|
||||
|
||||
class QStringListModel;
|
||||
class VEditor;
|
||||
|
||||
class VTextEditCompleter : public QCompleter
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit VTextEditCompleter(QObject *p_parent = nullptr);
|
||||
|
||||
bool isPopupVisible() const;
|
||||
|
||||
void performCompletion(const QStringList &p_words,
|
||||
const QString &p_prefix,
|
||||
Qt::CaseSensitivity p_cs,
|
||||
bool p_reversed,
|
||||
const QRect &p_rect,
|
||||
VEditor *p_editor);
|
||||
|
||||
protected:
|
||||
bool eventFilter(QObject *p_obj, QEvent *p_eve) Q_DECL_OVERRIDE;
|
||||
|
||||
private:
|
||||
void init();
|
||||
|
||||
bool selectRow(int p_row);
|
||||
|
||||
void setCurrentIndex(QModelIndex p_index, bool p_select = true);
|
||||
|
||||
void selectNextCompletion(bool p_reversed);
|
||||
|
||||
void insertCurrentCompletion();
|
||||
|
||||
void insertCompletion(const QString &p_completion);
|
||||
|
||||
void finishCompletion();
|
||||
|
||||
// Revert inserted completion to prefix and finish completion.
|
||||
void cancelCompletion();
|
||||
|
||||
void cleanUp();
|
||||
|
||||
void updatePrefix(const QString &p_prefix);
|
||||
|
||||
bool m_initialized;
|
||||
|
||||
QStringListModel *m_model;
|
||||
|
||||
VEditor *m_editor;
|
||||
|
||||
QString m_insertedCompletion;
|
||||
};
|
||||
|
||||
inline bool VTextEditCompleter::isPopupVisible() const
|
||||
{
|
||||
return popup()->isVisible();
|
||||
}
|
||||
|
||||
inline void VTextEditCompleter::finishCompletion()
|
||||
{
|
||||
popup()->hide();
|
||||
}
|
||||
#endif // VTEXTEDITCOMPLETER_H
|
Loading…
x
Reference in New Issue
Block a user