前稿で QPlainTextEdit に(最小限の)vi コマンドモードを実装したが、
今が挿入モードなのかコマンドモードなのかが明示されないので、iコマンドを押してもモードが変化したことが直ぐに分からず、
何を言っても感情をまったく表情に表に出さない人と話しているようで、とてもストレスフルである。
そこで、本稿では以下の機能を実装してみることにする。
現在が、挿入・コマンドのいずれのモードであるかの情報を保持しているのは ViEditView であり、 モードを変えるのも ViEditView である。 モードの状態をステータスバーに表示するには、状態をなんらかの方法でステータスバーに伝達しなくてはいけない。 Qt にはオブジェクト間で通信を行うための便利な機構:signal-slot があるので、これを利用する。
ViEditView に modeChanged() シグナルを追加する。 このシグナルはモードが変化した時にエミット(発行)されるものとする。
1: class ViEditView : public QPlainTextEdit 2: { 3: ..... 4: signals: 5: void modeChanged(); 6: ..... 7: };
前稿では setMode() は単にメンバ変数の値を上書きしていたが、 モードが変化した場合は上記のシグナルを発行するように修正する。
1: void ViEditView::setMode(Mode mode) 2: { 3: if( mode != m_mode ) { 4: m_mode = mode; 5: emit modeChanged(); 6: } 7: }
※ modeChanged() にはモードを引数と付けないことにした。これはモードを enum にしているため、
このシグナルを受けるスロットの宣言部分で Mode を使うためには、Mode を定義しているヘッダファイルのインクルードが必要になる。
そうするとインクルード依存性が生じてしまうので、引数は付けないことにした。
インクルードしなくても enum Mode; と前方宣言すればいいのかと期待したがうまくコンパイルできなかった。
いい回避方法を知ってる人はご教授して欲しい。
次にモードをステータスバーに表示するようにしてみよう。これで i Esc でモードが変わったことの認識が可能になる。
ステータスバーを管理しているのは MainWindow なので、 ステータスバーへのモード表示は MainWindow の onModeChanged() で行うことにする。 このメソッドは先の ViEditView::modeChanged() シグナルとコネクトされるのでスロットにしておく。
1: class MainWindow : public QMainWindow 2: { 3: ..... 4: public slots: 5: void onModeChanged(); 6: ..... 7: }
onModeChanged() の定義は以下のようにすればおk。
1: void MainWindow::onModeChanged() 2: { 3: QString text; 4: switch( m_editor->mode() ) { 5: case ViEditView::CMD: 6: text = "CMD"; 7: break; 8: case ViEditView::INSERT: 9: text = "INSERT"; 10: break; 11: } 12: statusBar()->showMessage(text); 13: }
MainWindow コンストラクタで ViEditView::modeChanged() と onModeChanged() をコネクトする。
1: MainWindow::MainWindow(QWidget *parent, Qt::WFlags flags) 2: : QMainWindow(parent, flags) 3: { 4: ..... 5: connect(m_editor, SIGNAL(modeChanged()), this, SLOT(onModeChanged())); 6: onModeChanged(); 7: }
以上の修正で、下図の様に挿入モード・コマンドモードのどちらであるかをステータスバーに表示できるようになった。
通常、ユーザはカーソル位置を注視しているから、モードを確認するためにステータスバーを見るには視線を移動しなくてはいけない。
テニスではボールから視線を外さずに打つことが肝要である。しかしチャンスボールの様に余裕があるときは、
ついつい相手のポジションを確認するときに視線がボールから外れてしまい、ミスをすることが多い。
視線をボールから外さずに相手のポジションを確認するようにしないといけない。
編集中にカーソル位置から視線を外したからといって、カーソルを見失ったりなんらかのミスをするというわけではないが、
コマンドモードの場合はカーソル形状を変えるようにし、視線を移さなくてもモードを確認可能にしてみる。
だって、その方がやっぱり楽だもん。
Qt のテキストカーソル形状はあまり自由度がなく、QPlainTextEdit::setCursorWidth(int) で幅が指定出来るだけである。
挿入モードは幅最小なので、コマンドモードではカーソル位置文字幅に設定してみることにする。
テキストカーソル位置が変化すると、QPlainTextEdit::cursorPositionChanged() シグナルが発行されるので、
それを受けるスロット onCursorPositionChanged() を用意し、その中でカーソル幅をカーソル位置テキスト幅に設定する。
コードは以下のようになる。
1: void ViEditView::onCursorPositionChanged() 2: { 3: if( mode() == ViEditView::CMD ) { 4: QTextCursor cur = textCursor(); 5: cur.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor); 6: QString text = cur.selectedText(); 7: QChar ch = text.isEmpty() ? QChar(' ') : text[0]; 8: int wd = fontMetrics().width(ch); 9: if( !wd ) 10: wd = fontMetrics().width(QChar(' ')); 11: setCursorWidth(wd); 12: } else 13: setCursorWidth(1); 14: }
4行目で、テキストカーソルを取得し、アンカーを固定して右移動することでカーソル位置文字を選択する。 QTextCursor::selectedText() で選択された文字を取り出し、fontMetrics().width(ch) で文字幅を得る。 コントロールコードなどの場合文字幅がゼロになるので、fontMetrics().width(QChar(' ')) で半角空白幅を取得している。 最後に QPlainTextEdit::setCursorWidth(wd) で幅を指定して一丁上がりだ。
コマンドモードであるかどうかの視認性は向上されたが、 テキストカーソル幅を文字幅にしたために、カーソルがカーソル位置文字を覆い尽くしてしまい、文字の判別が困難になってしまった。 この問題の対処は後で画面描画を自前で行うようにするときに対処することとする。
本稿のソースコードは以下からDLできます。
http://vivi.dyndns.org/dist2/qvi-002.zip
※ VS2008 でプロジェクトを作成しています。