早速 Gtk のテキストウィジェットがどんな風に動いているのか見てみた。まず自分自身のソースをただ表示するだけの単純なプログラムを書いてみる。window があって、その中に scrolledWindow があって、その中に textView があるという構造だ。という事は、カーソルが移動した時に、どうやって外側の scrolledWindow は移動距離を知ってスクロールするのだろう。まず実験用の簡単なコードを書く。
#include <gtk/gtk.h> #define BUFSIZE 256 static void destroy(GtkWidget *widget, gpointer data) { gtk_main_quit(); } int main(int argc, char *argv[]) { GtkWidget *window; GtkWidget *scrolledWindow; GtkWidget *textView; gtk_init(&argc, &argv); window= gtk_window_new(GTK_WINDOW_TOPLEVEL); textView= gtk_text_view_new(); scrolledWindow= gtk_scrolled_window_new (NULL, NULL); { char buffer[BUFSIZE]; size_t length; FILE *in= fopen("test.c", "r"); GtkTextBuffer *textBuffer= gtk_text_view_get_buffer(GTK_TEXT_VIEW(textView)); while ((length= fread (buffer, 1, BUFSIZE, in)) > 0) { gtk_text_buffer_insert_at_cursor(textBuffer, buffer, length); } fclose(in); } gtk_container_add (GTK_CONTAINER(scrolledWindow), textView); gtk_container_add (GTK_CONTAINER(window), scrolledWindow); gtk_widget_show_all(window); g_signal_connect(G_OBJECT(window), "destroy", G_CALLBACK(destroy), NULL); gtk_main(); return 0; }
API を見ると gtk_text_view_scroll_to_mark が怪しいのでこの辺を見てみる。すると set_adjustment_clamped で実際のスクロールが行われてる事が分かった。GtkTextView 構造体には、タテヨコの位置調整を表す GtkAdjustment 構造体 hadjustment, vadjustment と言うメンバがあって、カーソルが画面の外に出るとこれを書き換えるようになっている。つまり、GtkScrolledWindow 自身はクリッピングを行わないという事なのかな?
と言うわけで今度は GtkScrolledWindow の関数にブレークポイントを仕込む。探すのかなり大変だったが、GtkScrollbar のスーパークラスの GtkRange にある gtk_range_update_mouse_location にそれらしき物が見つかった。ここでも先ほどの GtkAdjustment が鍵で、このオブジェクトが更新されると value-changed シグナルが送られ、関連しているスクロールバーが反応して位置あわせを行う。「シグナルを送る」と書くとあたかも非同期っぽいニュアンスだが、実際シグナルはただのコールバックのリストなので、同期していて普通にデバッグ出来る。普通のオブザーバーパターンだ。
まとめ
エディタのカーソルが動くと、GtkTextView と GtkScrollbar が同じ GtkAdjustment を参照していて、どっちかが動けば片方も動くようになっている。GtkScrolledWindow は何もやってないのかな。