support Vim mode key bindings

After hitting `Ctrl+D` or keeping pressing `Ctrl+Alt`, VNote will enter Vim
normal pending mode.

1. Support <num>h,j,k,l to move cursor around;

TODO: Design a finite state machine to handle more Vim commands
elegantly.

Signed-off-by: Le Tan <tamlokveer@gmail.com>
This commit is contained in:
Le Tan 2016-12-19 21:43:35 +08:00
parent d2f61bc605
commit 26016dd44a
5 changed files with 241 additions and 61 deletions

View File

@ -18,15 +18,10 @@ VEdit::VEdit(VFile *p_file, QWidget *p_parent)
VEdit::~VEdit() VEdit::~VEdit()
{ {
qDebug() << "VEdit destruction";
if (m_file) { if (m_file) {
disconnect(document(), &QTextDocument::modificationChanged, disconnect(document(), &QTextDocument::modificationChanged,
(VFile *)m_file, &VFile::setModified); (VFile *)m_file, &VFile::setModified);
} }
if (m_editOps) {
delete m_editOps;
m_editOps = NULL;
}
} }
void VEdit::beginEdit() void VEdit::beginEdit()

View File

@ -8,8 +8,8 @@
extern VConfigManager vconfig; extern VConfigManager vconfig;
VEditOperations::VEditOperations(VEdit *p_editor, VFile *p_file) VEditOperations::VEditOperations(VEdit *p_editor, VFile *p_file)
: m_editor(p_editor), m_file(p_file), m_expandTab(false), : QObject(p_editor), m_editor(p_editor), m_file(p_file), m_expandTab(false),
m_keyState(KeyState::Normal) m_keyState(KeyState::Normal), m_pendingTime(2)
{ {
updateTabSettings(); updateTabSettings();
} }

View File

@ -3,6 +3,8 @@
#include <QPointer> #include <QPointer>
#include <QString> #include <QString>
#include <QObject>
#include <QList>
#include "vfile.h" #include "vfile.h"
class VEdit; class VEdit;
@ -11,8 +13,9 @@ class QKeyEvent;
enum class KeyState { Normal = 0, Vim }; enum class KeyState { Normal = 0, Vim };
class VEditOperations class VEditOperations: public QObject
{ {
Q_OBJECT
public: public:
VEditOperations(VEdit *p_editor, VFile *p_file); VEditOperations(VEdit *p_editor, VFile *p_file);
virtual ~VEditOperations(); virtual ~VEditOperations();
@ -32,6 +35,9 @@ protected:
bool m_expandTab; bool m_expandTab;
QString m_tabSpaces; QString m_tabSpaces;
KeyState m_keyState; KeyState m_keyState;
// Seconds for pending mode.
int m_pendingTime;
QList<QString> m_pendingKey;
}; };
#endif // VEDITOPERATIONS_H #endif // VEDITOPERATIONS_H

View File

@ -2,13 +2,13 @@
#include <QImage> #include <QImage>
#include <QVariant> #include <QVariant>
#include <QMimeData> #include <QMimeData>
#include <QObject>
#include <QWidget> #include <QWidget>
#include <QImageReader> #include <QImageReader>
#include <QDir> #include <QDir>
#include <QMessageBox> #include <QMessageBox>
#include <QKeyEvent> #include <QKeyEvent>
#include <QTextCursor> #include <QTextCursor>
#include <QTimer>
#include "vmdeditoperations.h" #include "vmdeditoperations.h"
#include "dialog/vinsertimagedialog.h" #include "dialog/vinsertimagedialog.h"
#include "utils/vutils.h" #include "utils/vutils.h"
@ -20,6 +20,10 @@
VMdEditOperations::VMdEditOperations(VEdit *p_editor, VFile *p_file) VMdEditOperations::VMdEditOperations(VEdit *p_editor, VFile *p_file)
: VEditOperations(p_editor, p_file) : VEditOperations(p_editor, p_file)
{ {
m_pendingTimer = new QTimer(this);
m_pendingTimer->setSingleShot(true);
m_pendingTimer->setInterval(m_pendingTime * 1000); // milliseconds
connect(m_pendingTimer, &QTimer::timeout, this, &VMdEditOperations::pendingTimerTimeout);
} }
bool VMdEditOperations::insertImageFromMimeData(const QMimeData *source) bool VMdEditOperations::insertImageFromMimeData(const QMimeData *source)
@ -28,7 +32,7 @@ bool VMdEditOperations::insertImageFromMimeData(const QMimeData *source)
if (image.isNull()) { if (image.isNull()) {
return false; return false;
} }
VInsertImageDialog dialog(QObject::tr("Insert image from clipboard"), QObject::tr("image_title"), VInsertImageDialog dialog(tr("Insert image from clipboard"), tr("image_title"),
"", (QWidget *)m_editor); "", (QWidget *)m_editor);
dialog.setBrowseable(false); dialog.setBrowseable(false);
dialog.setImage(image); dialog.setImage(image);
@ -48,7 +52,7 @@ void VMdEditOperations::insertImageFromQImage(const QString &title, const QStrin
VUtils::makeDirectory(path); VUtils::makeDirectory(path);
bool ret = image.save(filePath); bool ret = image.save(filePath);
if (!ret) { if (!ret) {
QMessageBox msgBox(QMessageBox::Warning, QObject::tr("Warning"), QString("Fail to save image %1").arg(filePath), QMessageBox msgBox(QMessageBox::Warning, tr("Warning"), QString("Fail to save image %1").arg(filePath),
QMessageBox::Ok, (QWidget *)m_editor); QMessageBox::Ok, (QWidget *)m_editor);
msgBox.exec(); msgBox.exec();
return; return;
@ -73,7 +77,7 @@ void VMdEditOperations::insertImageFromPath(const QString &title,
bool ret = QFile::copy(oriImagePath, filePath); bool ret = QFile::copy(oriImagePath, filePath);
if (!ret) { if (!ret) {
qWarning() << "error: fail to copy" << oriImagePath << "to" << filePath; qWarning() << "error: fail to copy" << oriImagePath << "to" << filePath;
QMessageBox msgBox(QMessageBox::Warning, QObject::tr("Warning"), QString("Fail to save image %1").arg(filePath), QMessageBox msgBox(QMessageBox::Warning, tr("Warning"), QString("Fail to save image %1").arg(filePath),
QMessageBox::Ok, (QWidget *)m_editor); QMessageBox::Ok, (QWidget *)m_editor);
msgBox.exec(); msgBox.exec();
return; return;
@ -154,7 +158,7 @@ bool VMdEditOperations::insertURLFromMimeData(const QMimeData *source)
bool VMdEditOperations::insertImage() bool VMdEditOperations::insertImage()
{ {
VInsertImageDialog dialog(QObject::tr("Insert Image From File"), QObject::tr("image_title"), VInsertImageDialog dialog(tr("Insert Image From File"), tr("image_title"),
"", (QWidget *)m_editor); "", (QWidget *)m_editor);
if (dialog.exec() == QDialog::Accepted) { if (dialog.exec() == QDialog::Accepted) {
QString title = dialog.getImageTitleInput(); QString title = dialog.getImageTitleInput();
@ -165,8 +169,44 @@ bool VMdEditOperations::insertImage()
return true; return true;
} }
bool VMdEditOperations::shouldTriggerVimMode(QKeyEvent *p_event)
{
if (m_keyState == KeyState::Vim) {
return true;
} else {
if (p_event->modifiers() == (Qt::ControlModifier | Qt::AltModifier)) {
switch (p_event->key()) {
// Should add one item for each supported Ctrl+ALT+<Key> Vim binding.
case Qt::Key_H:
case Qt::Key_J:
case Qt::Key_K:
case Qt::Key_L:
case Qt::Key_0:
case Qt::Key_1:
case Qt::Key_2:
case Qt::Key_3:
case Qt::Key_4:
case Qt::Key_5:
case Qt::Key_6:
case Qt::Key_7:
case Qt::Key_8:
case Qt::Key_9:
return true;
default:
break;
}
}
}
return false;
}
bool VMdEditOperations::handleKeyPressEvent(QKeyEvent *p_event) bool VMdEditOperations::handleKeyPressEvent(QKeyEvent *p_event)
{ {
if (shouldTriggerVimMode(p_event)) {
if (handleKeyPressVim(p_event)) {
return true;
}
} else {
switch (p_event->key()) { switch (p_event->key()) {
case Qt::Key_Tab: case Qt::Key_Tab:
{ {
@ -184,30 +224,6 @@ bool VMdEditOperations::handleKeyPressEvent(QKeyEvent *p_event)
break; break;
} }
case Qt::Key_H:
{
if (handleKeyH(p_event)) {
return true;
}
break;
}
case Qt::Key_W:
{
if (handleKeyW(p_event)) {
return true;
}
break;
}
case Qt::Key_U:
{
if (handleKeyU(p_event)) {
return true;
}
break;
}
case Qt::Key_B: case Qt::Key_B:
{ {
if (handleKeyB(p_event)) { if (handleKeyB(p_event)) {
@ -216,6 +232,22 @@ bool VMdEditOperations::handleKeyPressEvent(QKeyEvent *p_event)
break; break;
} }
case Qt::Key_D:
{
if (handleKeyD(p_event)) {
return true;
}
break;
}
case Qt::Key_H:
{
if (handleKeyH(p_event)) {
return true;
}
break;
}
case Qt::Key_I: case Qt::Key_I:
{ {
if (handleKeyI(p_event)) { if (handleKeyI(p_event)) {
@ -224,12 +256,27 @@ bool VMdEditOperations::handleKeyPressEvent(QKeyEvent *p_event)
break; break;
} }
default: case Qt::Key_U:
// Fall through. {
if (handleKeyU(p_event)) {
return true;
}
break; break;
} }
m_keyState = KeyState::Normal; case Qt::Key_W:
{
if (handleKeyW(p_event)) {
return true;
}
break;
}
default:
break;
}
}
return false; return false;
} }
@ -273,6 +320,9 @@ bool VMdEditOperations::handleKeyTab(QKeyEvent *p_event)
bool VMdEditOperations::handleKeyBackTab(QKeyEvent *p_event) bool VMdEditOperations::handleKeyBackTab(QKeyEvent *p_event)
{ {
if (p_event->modifiers() != Qt::ShiftModifier) {
return false;
}
QTextDocument *doc = m_editor->document(); QTextDocument *doc = m_editor->document();
QTextCursor cursor = m_editor->textCursor(); QTextCursor cursor = m_editor->textCursor();
QTextBlock block = doc->findBlock(cursor.selectionStart()); QTextBlock block = doc->findBlock(cursor.selectionStart());
@ -345,6 +395,20 @@ bool VMdEditOperations::handleKeyB(QKeyEvent *p_event)
return false; return false;
} }
bool VMdEditOperations::handleKeyD(QKeyEvent *p_event)
{
if (p_event->modifiers() == Qt::ControlModifier) {
// Ctrl+D, enter Vim-pending mode.
// Will accept the key stroke in m_pendingTime as Vim normal command.
m_keyState = KeyState::Vim;
m_pendingTimer->stop();
m_pendingTimer->start();
p_event->accept();
return true;
}
return false;
}
bool VMdEditOperations::handleKeyH(QKeyEvent *p_event) bool VMdEditOperations::handleKeyH(QKeyEvent *p_event)
{ {
if (p_event->modifiers() == Qt::ControlModifier) { if (p_event->modifiers() == Qt::ControlModifier) {
@ -427,3 +491,106 @@ bool VMdEditOperations::handleKeyW(QKeyEvent *p_event)
return false; return false;
} }
bool VMdEditOperations::handleKeyPressVim(QKeyEvent *p_event)
{
int modifiers = p_event->modifiers();
bool ctrlAlt = modifiers == (Qt::ControlModifier | Qt::AltModifier);
switch (p_event->key()) {
// Ctrl may be sent out first.
case Qt::Key_Control:
{
goto pending;
break;
}
case Qt::Key_H:
case Qt::Key_J:
case Qt::Key_K:
case Qt::Key_L:
{
if (modifiers == Qt::NoModifier || ctrlAlt) {
QTextCursor::MoveOperation op;
switch (p_event->key()) {
case Qt::Key_H:
op = QTextCursor::Left;
break;
case Qt::Key_J:
op = QTextCursor::Down;
break;
case Qt::Key_K:
op = QTextCursor::Up;
break;
case Qt::Key_L:
op = QTextCursor::Right;
}
// Move cursor <repeat> characters left/Down/Up/Right.
int repeat = keySeqToNumber(m_pendingKey);
QTextCursor cursor = m_editor->textCursor();
cursor.movePosition(op, QTextCursor::MoveAnchor,
repeat == 0 ? 1 : repeat);
m_editor->setTextCursor(cursor);
}
break;
}
case Qt::Key_0:
case Qt::Key_1:
case Qt::Key_2:
case Qt::Key_3:
case Qt::Key_4:
case Qt::Key_5:
case Qt::Key_6:
case Qt::Key_7:
case Qt::Key_8:
case Qt::Key_9:
{
if (modifiers == Qt::NoModifier || ctrlAlt) {
int num = p_event->key() - Qt::Key_0;
m_pendingKey.append(QString::number(num));
goto pending;
}
break;
}
default:
// Unknown key. End Vim mode.
break;
}
m_keyState = KeyState::Normal;
m_pendingKey.clear();
m_pendingTimer->stop();
p_event->accept();
return true;
pending:
if (m_pendingTimer->isActive()) {
m_pendingTimer->stop();
m_pendingTimer->start();
}
p_event->accept();
return true;
}
int VMdEditOperations::keySeqToNumber(const QList<QString> &p_seq)
{
int num = 0;
for (int i = 0; i < p_seq.size(); ++i) {
QString tmp = p_seq.at(i);
bool ok;
int tmpInt = tmp.toInt(&ok);
if (!ok) {
return 0;
}
num = num * 10 + tmpInt;
}
return num;
}
void VMdEditOperations::pendingTimerTimeout()
{
qDebug() << "key pending timer timeout";
m_keyState = KeyState::Normal;
m_pendingKey.clear();
}

View File

@ -7,9 +7,12 @@
#include <QImage> #include <QImage>
#include "veditoperations.h" #include "veditoperations.h"
class QTimer;
// Editor operations for Markdown // Editor operations for Markdown
class VMdEditOperations : public VEditOperations class VMdEditOperations : public VEditOperations
{ {
Q_OBJECT
public: public:
VMdEditOperations(VEdit *p_editor, VFile *p_file); VMdEditOperations(VEdit *p_editor, VFile *p_file);
bool insertImageFromMimeData(const QMimeData *source) Q_DECL_OVERRIDE; bool insertImageFromMimeData(const QMimeData *source) Q_DECL_OVERRIDE;
@ -17,6 +20,9 @@ public:
bool insertImage() Q_DECL_OVERRIDE; bool insertImage() Q_DECL_OVERRIDE;
bool handleKeyPressEvent(QKeyEvent *p_event) Q_DECL_OVERRIDE; bool handleKeyPressEvent(QKeyEvent *p_event) Q_DECL_OVERRIDE;
private slots:
void pendingTimerTimeout();
private: private:
bool insertImageFromURL(const QUrl &imageUrl); bool insertImageFromURL(const QUrl &imageUrl);
void insertImageFromPath(const QString &title, const QString &path, const QString &oriImagePath); void insertImageFromPath(const QString &title, const QString &path, const QString &oriImagePath);
@ -26,10 +32,16 @@ private:
bool handleKeyTab(QKeyEvent *p_event); bool handleKeyTab(QKeyEvent *p_event);
bool handleKeyBackTab(QKeyEvent *p_event); bool handleKeyBackTab(QKeyEvent *p_event);
bool handleKeyB(QKeyEvent *p_event); bool handleKeyB(QKeyEvent *p_event);
bool handleKeyD(QKeyEvent *p_event);
bool handleKeyH(QKeyEvent *p_event); bool handleKeyH(QKeyEvent *p_event);
bool handleKeyI(QKeyEvent *p_event); bool handleKeyI(QKeyEvent *p_event);
bool handleKeyU(QKeyEvent *p_event); bool handleKeyU(QKeyEvent *p_event);
bool handleKeyW(QKeyEvent *p_event); bool handleKeyW(QKeyEvent *p_event);
bool handleKeyPressVim(QKeyEvent *p_event);
bool shouldTriggerVimMode(QKeyEvent *p_event);
int keySeqToNumber(const QList<QString> &p_seq);
QTimer *m_pendingTimer;
}; };
#endif // VMDEDITOPERATIONS_H #endif // VMDEDITOPERATIONS_H