CUIはcharacter user interfaceなのですが、 キャラクタ・ベースの端末上でGUIのwidget風な動作をするプログラムを作ってみようと思います
マウスは無しで入力はキーボードだけ。 BIOSの設定画面とか、ガラケーの画面のような感じにします
カラーごてごてにすると下品になってしまいがちなので、 モノクロでアンダーラインと反転の装飾だけにします
20年以上前に、Turbo C++についてきた、お試し版の統合環境の画面が記憶の片隅にあります (古過ぎてもう誰も判ってもらえないかも)
でも、いきなり大風呂敷を広げて、途中で放り出すのも情けないので、 簡単な目標にして、徐々に拡張していくようにします
という事で、最初の目標は低く
+---------------------+ | | | Are you sure ? | | | | (OK) (Cancel) | | | +---------------------+
こんな画面だけから始めてみます
古来のCUIでは
int ch; : <何か処理があって> : printf("Are you sure ? [Y/n] : "); fflush(stdout); ch = getchar(); if(ch == 'Y' || ch == 'y' || ch == '\n'){ : <続きの処理> : }else{ : <やっぱりヤメ> : }
的な場面ですが、ここでダイアログ風の画面を表示して、 キー操作でボタンを押すようにしてみます
char *s; : <何か処理があって> : s = cui_simple_dialog("Are you sure ?", "OK", "Cancel"); if(strcmp(s, "OK") == 0){ : <続きの処理> : }else{ /* "Cancel" */ : <やっぱりヤメ> : } char * cui_simple_dialog(char *s, char *s_btn1, char *s_btn2) { : <さて、どうしようか...> : }
動作の概要を考えてみると次のような流れでしょうか
画面を構成する部品としては
さらに、ライブラリとして呼び出す使い方を考えると、cui_sample_dialog() は次の疑似コードな感じでしょうか...
char * cui_simple_dialog(char *s, char *s_btn1, char *s_btn2) { void *panel, *btn1, *btn2; char *btn_result; panel = cui_panel_new(NULL, ...); cui_label_new(panel, s, ...); btn1 = cui_button_new(panel, s_btn1, ...); btn2 = cui_button_new(panel, s_btn2, ...); cui_bind(btn1, cui_simple_dialog_handler, &btn_result); cui_bind(btn2, cui_simple_dialog_handler, &btn_result); cui_main(panel); return btn_result; } void cui_simple_dialog_handler(int evt_type, void *obj, void *prm) { char **result_ptr = (char **)prm; if(evt_type == CUI_EVT_BUTTON){ *result_ptr = cui_button_str_get(obj); cui_quit(); } }
あー、だらだらと文章にすると、ややこしや。
なんか仮定だらけで、果たしてうまくいくのやら...
ここで、上位側の構想から気分を変えて、少々基礎工事をしておきます
入力と出力、つまり、キー入力と画面への表示です
まずはキー入力から
古来から入門書でおなじみのgetchar()の動作では、 末尾にENTERキーの入力が必要なので、そのままでは使えません
世の中に末尾にENTERキーの入力なしで操作するプログラムは、 いくらでも存在するわけで、調べて試してみたところ、 次の処理でなんとかいけそうです
という事で、キー入力用の処理を用意して、試してみます
$ make gcc -Wall -c -o key_test.o key_test.c gcc -Wall -c -o key.o key.c gcc -Wall -o key_test key_test.o key.o $
$ ./key_test .....61 (a) ..62 (b) ..63 (c) ........41 (A) .........31 (1) ...0d ....08 ......\ ............1b 5b ([) 41 (A) ...........1b 5b ([) 44 (D) ............1b 5b ([) 43 (C) ..........1b 5b ([) 42 (B) ...........
入力したキーは、a, b, c, A, 1, ENTER, BS, 上矢印, 左矢印, 右矢印, 下矢印です
ENTER は 0x0d で '\r'が、BS は 0x08 '\b' が使えそうです
矢印キーは1度押すと、3バイト返るようです
ENTER, BS, 矢印は環境の設定により変わる気がします。これが全てと思わない方がいいでしょう。
別の環境でも、同じキー入力を試した結果です
$ ./key_test .........61 (a) ....62 (b) .........63 (c) .........31 (1) ................0d ........7f .....\ .........1b 5b ([) 41 (A) .......1b 5b ([) 44 (D) .......1b 5b ([) 43 (C) ......1b 5b ([) 42 (B) .......... 0d 08 $
やはり、この結果では BS が 0x08 から 0x7f に変わってました
環境によりcui_key_get()の返す値が変わる可能性があり、 矢印キーではcui_key_get()を3回呼ばないと、どの矢印キーか判断できません
上位側には同じキーで同じ値を返したいし、 矢印キーも1回の呼び出しで判定出来るようにしたいです
という事で、より上位な関数を用意してみます
get2() というネーミングは、いかがなものか、ですが...
上位に返す矢印キーのコードは man asciiを見て、 とりあえず、使ってなさそうなコントロール・コードを適当に割り振ってみました
Oct Dec Hex Char ---------------------------------------- : 021 17 11 DC1 (device control 1) 022 18 12 DC2 (device control 2) 023 19 13 DC3 (device control 3) 024 20 14 DC4 (device control 4) :
$ cat cui0_1.patch | patch -p1 $ make $ ./key_test :
これで先の別の環境で試してみても、 a, b, c, 1, ENTER, BS, 上矢印, 左矢印, 右矢印, 下矢印 については同じ結果になりました
.....61 (a) ...62 (b) ...63 (c) .....31 (1) ......0d .....08 .....11 ...13 ....14 ...12 .......... : .....61 (a) ..62 (b) ....63 (c) ....31 (1) ...0d ..........08 .............11 ...13 ....14 ..12 ......... :
続いて、出力側の画面表示
指定の位置に指定の装飾で文字列を表示させたいのですが、 そんなときは昔から、エスケープ・シーケンスの出番です
esc.c , esc.h , esc_test.c として用意してみます
$ cat cui1_2.patch | patch -p1 $ make $ ./esc_test
実行すると foo bar hoge の文字列が、指定の位置に、指定の装飾で表示されます
foo bar hoge
基礎工事から、上位側の cui_simple_dialog()とcui_simple_dialog_handler()の構想に戻ります
先述の疑似コードで、呼び出そうとしてる関数を抜き出すと
+--------------------+----------------------------------+ |cui_panel_new() |パネル生成 | +--------------------+----------------------------------+ |cui_label_new() |ラベル生成 | +--------------------+----------------------------------+ |cui_button_new() |ボタン生成 | +--------------------+----------------------------------+ |cui_bind() |部品にハンドラを登録 | +--------------------+----------------------------------+ |cui_main() |メイン処理 | +--------------------+----------------------------------+ |cui_button_str_get()|ボタンの文字列を取得 | +--------------------+----------------------------------+ |cui_quit() |メイン処理を終了するように指示する| +--------------------+----------------------------------+
順にパネル、ラベル、ボタンの部品の生成から考えてみます
疑似コードでは void * として部品の構造体のアドレスを保持するように考えてましたが、 ここはオブジェクト指向っぽく、元になる部品から継承するようにしてみます
typedef struct cui_base *cui; struct cui_base{ cui parent, children, next; int x, y, w, h; };
元になる構造体へのポインタをcui型とします
構造体のメンバは、とりあえず今思いついた分だけ並べてみました。
parentで親の入れものを、
childrenで保持してる子供の部品群の先頭を、
nextで同じ親を持つ兄弟を指すようにします
とりあえず位置 x, y とサイズ w, h を用意。 位置 x, y は、親の入れものからの相対座標系で考えます
このベースからパネル、ラベルを継承すると
typedef struct cui_panel{ struct cui_base base; } *cui_panel; typedef struct cui_label{ struct cui_base base; char *s; } *cui_label;
struct cui_xxx は構造体を、cui_xxx という型はポインタとします
ラベルは元になる構造体から、ほとんどそのままで、保持する文字列を追加しただけです
typedef struct cui_button{ struct cui_label label; } *cui_button;
ボタンはラベルから継承するようにしてみます
では疑似コードのcui_xxx_new()の生成用の関数
mallocで生成すればいいでしょうが、 メモリ不足のエラー判定を一々するのも面倒です
きょう日のメモリ満載な環境では、まず失敗する事が稀な上に、 別にマスプロな製品でもなんでもないので、 メモリが足りなかったら、動かなくても問題ないです
万一mallocが失敗したら、メッセージ出してプログラム全体が終了すれば、それでいいです
なので cui cui_alloc(int size) な関数を用意して、 この関数から返ったという事は、絶対に成功してるという事にします
失敗したら、プログラム全体が終了するので、 この関数からも戻りません
#define MSG(s) fprintf(stderr, "%s() %s L%d : %s\n", __func__, __FILE__, __LINE__, s) #define ERR(s) do{ fprintf(stderr, "ERR "); MSG(s); exit(1); }while(0) cui cui_alloc(int size) { cui obj; if((obj = malloc(size)) == NULL) ERR("No Mem"); return obj; }
そしてベースの構造体の初期化用の関数
void cui_base_init(cui obj, cui parent, int x, int y, int w, int h) { obj->parent = parent; obj->children = NULL; obj->next = NULL; obj->x = x; obj->y = y; obj->w = w; obj->h = h; }
children は初期化の設定の段階では、子供の部品は未だ無いと考えて NULL を設定します
逆に第1引数の parent 側からしたら、子供の1つに初期化したばかりのobjを追加したいので、 その処理を考えます
void cui_base_child_add(cui obj, cui child) { child->next = obj->children; obj->children = child; }
この子供の追加処理を cui_base_init() の中で、 parent に対して呼び出せばよさそうです
void cui_base_init(cui obj, cui parent, int x, int y, int w, int h) { obj->parent = parent; obj->children = NULL; obj->next = NULL; obj->x = x; obj->y = y; obj->w = w; obj->h = h; if(parent) cui_base_child_add(parent, obj); }
ではパネルの生成
cui cui_panel_new(cui parent, int x, int y, int w, int h) { cui obj = cui_alloc(sizeof(struct cui_panel)); cui_panel_init(obj, parent, x, y, w, h); return obj; } void cui_panel_init(cui obj, cui parent, int x, int y, int w, int h) { cui_base_init(obj, parent, x, y, w, h); }
続いてラベルの生成
ラベルのサイズ w, h について、hは常に1行固定と考えて、 wは指定の文字列sの長さで自動的に決まるものとします
cui cui_label_new(cui parent, int x, int y, char *s) { cui obj = cui_alloc(sizeof(struct cui_label)); cui_label_init(obj, parent, x, y, s); return obj; } void cui_label_init(cui obj, cui parent, int x, int y, char *s) { cui_label p = (cui_label)obj; cui_base_init(obj, parent, x, y, strlen(s), 1); p->s = s; }
続いてボタンの生成
一旦ラベルとして初期化して、ボタン独自の初期化を追加します
サイズの w を、'(' と ')' の 2 だけ増やします
cui cui_button_new(cui parent, int x, int y, char *s) { cui obj = cui_alloc(sizeof(struct cui_button)); cui_button_init(obj, parent, x, y, s); return obj; } void cui_button_init(cui obj, cui parent, int x, int y, char *s) { cui_label_init(obj, parent, x, y, s); obj->w += 2; }
ここまでの分を cui.c , cui.h としてまとめてみて、 コンパイルが通るかだけみておきます
$ make cui.o gcc -Wall -c -o cui.o cui.c $
OK
ここまでのソースファイル cui3.tgz
次は、cui_bind() , cui_main(), cui_quit() で、イベント関係をば...
疑似コードでは仮に
: cui_bind(btn1, cui_simple_dialog_handler, &btn_result); void cui_simple_dialog_handler(int evt_type, void *obj, void *prm) :
などとしてました
イベントの処理について考えてみます。
今のところ、思いつくイベントの種類は
くらいでしょうか
基本的には部品ごとに、対応するハンドラを登録出来るようにしておいて、 イベントの処理としてハンドラを呼び出せばよさそうです
キー入力のイベントと描画要求は、システムとして内部的に処理して、 ボタン押したイベントは、アプリケーションから登録して使うと考えます
このプログラムでは、操作のイベントとしては、マウス無しなので、キーしかありえません。
キーのイベントがトリガとなって、描画要求のイベント、ボタン押したイベントの処理に連携します。
でも、処理としては一元化して、単純に考えても、大丈夫そうですね
ハンドラの引数と型を考えてみます
typedef void (*cui_handler)(cui obj, int evt, void *prm); typedef struct cui_handler_list *cui_handler_list; struct cui_handler_list{ cui_handler hdr; int evt; void *prm; cui_handler_list next; };
ハンドラの引数は
部品に複数のハンドラを登録できるようにしたいので、 リスト用の構造体も用意しておきます
イベントの種類の定義は、とりあえず複数の指定が出来るように、 ビットの論理和(OR)で指定できるようにしておきます
では、部品にハンドラを登録するための cui_bind() を実装してみます
続いて、cui_main() , cui_quit() の処理
キーを受け付けて処理しますが、どの部品を対象として処理すべきか?
フォーカスがどの部品にあるかという「状態」を管理せねばなりません。
とりあえず安直に、グローバル変数 cui cui_focus を用意してみます
フォーカスは矢印キーで移動させたいですが、 今のところフォーカスを受けるのはボタンだけで、 パネルとラベルはキー操作不要の部品です
部品側の設定として、フォーカス可能の指定を追加します。
struct cui_base にメンバ int flags 追加して、
マクロ CUI_FLG_CAN_FOCUS 値 (1<<0) としておきます
さて、まず初めはフォーカスの無い状態から、矢印キーが押されて、
どこかの部品にフォーカスされます。
そして、さらに矢印キーで、フォーカスが移動します。
どうすべきか?
これまた安直に、フォーカス可能部品を総当たりで調べる方法にしてみます
フォーカスが無ければ
+------+----------------------------+ |上矢印|一番上にある部品にフォーカス| +------+----------------------------+ |下矢印|一番下にある部品にフォーカス| +------+----------------------------+ |左矢印|一番左にある部品にフォーカス| +------+----------------------------+ |右矢印|一番右にある部品にフォーカス| +------+----------------------------+
フォーカスがあれば
+------+------------------------------------+ |上矢印|上にある一番近い部品にフォーカス移動| +------+------------------------------------+ |下矢印|下にある一番近い部品にフォーカス移動| +------+------------------------------------+ |左矢印|左にある一番近い部品にフォーカス移動| +------+------------------------------------+ |右矢印|右にある一番近い部品にフォーカス移動| +------+------------------------------------+
部品のもつ x, y は、親の入れものからの相対座標なので、 この場合の座標は、グローバルな座標で比較せねばならんです
+------------------------+-----------------------------------+ |「一番上にある部品」とは|グローバルなy座標の値が一番小さい | +------------------------+-----------------------------------+ |「一番下にある部品」とは|グローバルなy座標+hの値が一番大きい| +------------------------+-----------------------------------+ |「一番左にある部品」とは|グローバルなx座標の値が一番小さい | +------------------------+-----------------------------------+ |「一番右にある部品」とは|グローバルなx座標+wの値が一番大きい| +------------------------+-----------------------------------+
+----------------------------+--------------------------------------------+ |「上にある一番近い部品」とは|グローバルなy座標の値が自分以下であり、 | | |部品の中心からの距離が最小で、自分以外の部品| +----------------------------+--------------------------------------------+ |「下にある一番近い部品」とは|グローバルなy座標+hの値が自分以上であり、 | | |部品の中心からの距離が最小で、自分以外の部品| +----------------------------+--------------------------------------------+ |「左にある一番近い部品」とは|グローバルなx座標の値が自分以下であり、 | | |部品の中心からの距離が最小で、自分以外の部品| +----------------------------+--------------------------------------------+ |「右にある一番近い部品」とは|グローバルなx座標+wの値が自分以上であり、 | | |部品の中心からの距離が最小で、自分以外の部品| +----------------------------+--------------------------------------------+
とりあえず、このような判定で試してみます
ここでふと...
部品がキーイベントを処理したい時もあるし、メインループがフォーカス移動のために、
キーイベントを処理したい時もあるなぁ...
ハンドラが複数登録されていて、その種類のイベントが発生したら、合致するハンドラを順に呼ぼうとしてます。
フォーカスがきてる部品へ登録したハンドラで、複数のものが合致したら、登録順に呼べばいいでしょう。
例えばボタンなら、ENTERキーは処理したいけど、矢印キーは処理したくなくて、
システムがフォーカスの移動として扱いたいところです。
ここはよくある方法として、ハンドラの返り値で、そのイベントを処理したか、無視したかをシステムに伝えるようにしてみます。
フォーカスされてる部品が登録順に、ハンドラ呼び出しを試し、
どこかのハンドラで処理されたら、そこで連鎖は終了。
どのハンドラも処理しなかったら、最後にシステム(cui_main関数のループ)が、
フォーカスの移動として処理します。
これで、いけそうです
そしてさらに、次から次と問題が...
イベントのパラメータは、ユーザが指定するデータを渡すつもりで用意してましたが、
キーのイベントの場合、キーの値を渡すかなぁと、ぼんやり考えてました。
そうすると、ユーザのデータが指定できないです。
イベントのユーザ指定のパラメータと、イベントの値とは、別にしないとダメです
typedef void (*cui_handler)(cui obj, int evt, void *prm);
のハンドラの型は
typedef int (*cui_handler)(cui obj, int evt, int val, void *prm);
として、値 val を追加しておきます
cui_main() を終了するための cui_quit() の処理は、 終了イベントとして処理するのも、まわりくどい気がするので、 グローバル変数 int cui_running だけで判定するようにします
では、キーのイベントから、フォーカス移動の処理を実装してみます
$ make cui.o gcc -Wall -c -o cui.o cui.c $
さて、コンパイルは通したものの、どうやって動作確認すべしか...
ここまでの動作確認として、 先述の疑似コード cui_simple_dialog() 的なやつで試してみます
+----------+-----------------------------------------+ |cui.h |マクロをヘッダファイルへ移動 | |cui.c | | +----------+-----------------------------------------+ |cui.h |プロトタイプ宣言追加 | +----------+-----------------------------------------+ |cui.c |label, button の文字列取得関数追加 | | +-----------------------------------------+ | |cui_main()にescの初期化処理と終了処理追加| +----------+-----------------------------------------+ |cui_test.c|新規追加 | +----------+-----------------------------------------+ |Makefile |cui_test追加 | +----------+-----------------------------------------+
$ make gcc -Wall -c -o cui_test.o cui_test.c gcc -Wall -c -o cui.o cui.c gcc -Wall -c -o esc.o esc.c gcc -Wall -c -o key.o key.c gcc -Wall -o cui_test cui_test.o cui.o esc.o key.o $ $ ./cui_test :
まず、矢印キーを押してみて
hdr: Cancel, 00
の表示 !
あら? テストしてみるものです。
cui_main() の処理で、キー入力なしで 0 で返っても、ハンドラを呼び出してしまってました (>_<)
'q' キーで終了は、できました。
では修正
$ make gcc -Wall -c -o cui.o cui.c gcc -Wall -o cui_test cui_test.o cui.o esc.o key.o $ $ ./cui_test
右矢印押して、'a'
hdr: Cancel, 61
左矢印で
hdr: Cancel, 13
これで、OKボタンにフォーカス移動してるか?
'b' 押してみて
hdr: Cancel, 62
ダメでした(T_T)
フォーカスはCancelボタンのまま移動してないようです。
さて、どこが悪いのか...
そうか! 既にフォーカスがきてるCancelボタンも含めて、
フォーカスの移動先として処理してました orz
修正
$ make $ ./cui_test
右矢印押して、'a'
hdr: Cancel, 61
左矢印で
hdr: Cancel, 13
'b'
hdr: OK, 62
おーし(^_^) そして'q'で終了
それらしく動いてるようです
ここまでのソースファイル cui8.tgz
ここまで、部品の表示が何も出ないので、なんとも寂しいです
表示イベントの処理を実装してみましょう
内部ではイベントの種類を CUI_EVT_DRAW でハンドラを呼び出して、
扱おうと考えてますが、表示要求自体はキーイベントをトリガとして起きます。
例えば、キーイベントで部品にフォーカスがきたら、
表示をフォーカスがきた状態に更新するため、
種類 CUI_EVT_DRAW として登録されてるハンドラを呼び出すようにします
そう考えると、表示用のハンドラでは、イベントの値 val は、 表示の状態を指定すると考えるのが自然です
+---------------+------------------------+ | 値 val | 状態 | +---------------+------------------------+ |CUI_DRAW_NORMAL|通常の表示 | +---------------+------------------------+ |CUI_DRAW_FOCUS |フォーカスがきてるとき | +---------------+------------------------+ |CUI_DRAW_ACTIVE|ボタンで押された状態など| +---------------+------------------------+
こんな感じで用意してみます
そして、まずはラベルの表示を試してみます。
ラベルの初期化処理 cui_label_init() で、
ラベル表示用のハンドラを登録します。
ラベルはフォーカスもこないし、アクティブにもならないので、
ハンドラでは、値 val は無視して、
グローバル座標の位置に文字列を表示します
そして最初の表示のきっかけをどうするか?
cui_main(top_obj) として呼び出されたら、
とりあえず、top_obj以下の全部の部品に、表示要求を出すようにしてみます
$ make $ ./cui_test
Are you sure ? OK Cancel
なにやら、それらしい表示が出ました。
とりあえず、右矢印で Cancel ボタンにフォーカスあてて、'q' で終了
ボタンはラベルから継承してるので、
そのままラベルの表示ハンドラが呼び出されてます。
ボタン用のハンドラを追加して、登録するようにしてみます
ですが、ただ追加登録しただけでは、ダメです。
ハンドラは登録順に呼ばれて、TRUEが返るとそこで終了するので、
結局、先に登録されたラベル用のハンドラだけの処理になります。
登録されたハンドラを削除する関数 cui_unbind() を追加してみます
そうこうしてると、イベントの種類 CUI_EVT_xxx を、
せっかくORで指定できるようにしてるのに、
一致しないとハンドラを呼び出さないようにしてしまってました orz
これは、これで修正するとして、
unbind()で削除するときの解釈をどうすべしか?
この場合、unbind()で指定した値(ORの場合もある)と、
完全に一致するものがあれば、削除する事にしておきます
さらについでに、パネルの背景のクリアと枠の表示も追加してみます
部品のグローバル座標を取得して、
そこから文字列を指定の装飾で表示する場面が増えてきます。
専用の関数 cui_draw_str() を用意してみます
$ make $ ./cui_test
+---------------------+ | | | Are you sure ? | | | | (OK) (Cancel) | | | +---------------------+
ほほう。いい感じです
ですが矢印キーで、フォーカスが移動しても、ボタンの表示はそのままです。
うん。フォーカス移動のときの表示の処理は、まだ何もしてませんでしたね
: obj = cui_focus_move_judge(top_obj, key, NULL, &val); if(obj) cui_focus = obj; :
cui_main() のここで、グローバル変数 cui_focus を上書きしてるだけでした。
処理を追加してみます
$ make $ ./cui_test
+---------------------+ | | | Are you sure ? | | | | (OK) (Cancel) | | | +---------------------+
OKです。
矢印キーでフォーカスがきて、移動もします。
'q'キーで終了します
さて次は、「ボタン押した」イベントの処理です
フォーカスきてるボタンがキーイベントでENTERを受け取ると、 表示をアクティブにして、ボタンイベントのハンドラを呼び出し、 戻ってくると、表示を元に戻せばよさそうです
ボタン押したイベントの値valは、何が妥当でしょうか?
ダブルタップとかで、回数とか?
とりあえず、今は押すだけなので、値valは0を指定しておきます
ボタン用のハンドラ cui_button_hdr() は、 とりあえず表示用の前提になってますが、 キーイベントもこの関数で共用させてみます
$ make $ ./cui_test
意図した通りに動作してるようですが...
ボタンのアクティブの表示が一瞬で終ってしまって、よく見えないです。
わざと少しウェイトしてから、ハンドラを呼び出すようにしてみます
0.2秒の待ちで、いい感じになりました
これでほぼ当初の目標の内容まできました。
最初に掲げてた上位な関数 cui_simple_dialog() を実装してみます
ここまで、newするだけで、解放処理をうっちゃってました。
cui_free(cui obj)で、全部解放するようにしておきます
動作も問題なさそうです
cui.h と cui.c が少々盛りだくさんになってきたので、 パネル, ラベル, ボタン, ハンドラ関連を別ファイルに分離しておきます
ここまでのソースファイル cui15.tgz
見直してみて、気付いた箇所をブラッシュアップしておきます
+----------+-----------------+--------------------------------------------------------------------+ | ファイル | 関数 | 変更内容 | +----------+-----------------+--------------------------------------------------------------------+ |cui.c |cui_simple_dialog|dialog.c dialog.h へと分離 | |cui.h | +--------------------------------------------------------------------+ | | |cui_simple_dialog_new()として、生成部分のみに変更 | | | +--------------------------------------------------------------------+ | | |他の部品と同様にcui_main()は、使用するユーザ側から呼び出す | | +-----------------+--------------------------------------------------------------------+ | |cui_fill_rect |新規追加 | | |cui_clear_rect | | | |cui_clear | | | +-----------------+--------------------------------------------------------------------+ | |cui_main |全部品を表示する前にtop_objの領域のクリアを追加 | +----------+-----------------+--------------------------------------------------------------------+ |esc.c |cui_esc_inter |動作の表示結果を残しておきたいので、画面の保存、復帰処理をやめる | | |cui_esc_exit | | +----------+-----------------+--------------------------------------------------------------------+ |hadler.c |cui_handler_call |登録順にハンドラ関数を呼びだいていたが、 | | | |最後に登録されたものから順に呼ぶように変更 | | | |(継承でいちいちcui_unbind()しなくても上書きしたように振舞えれるので)| +----------+-----------------+--------------------------------------------------------------------+ |button.c |cui_button_init |cui_unbind()の呼び出し削除 | +----------+-----------------+--------------------------------------------------------------------+ |panel.c |cui_panel_hdr |枠描画時のクリア処理を削除 (cui_main()に移動) | | | +--------------------------------------------------------------------+ | | |枠描画ではcui_fill_rect()を使うように変更 | +----------+-----------------+--------------------------------------------------------------------+ |cui_test.c|main |cui_simple_dialog()呼び出しを | | | |cui_simple_dialog_new(), cui_main(), cui_free()に変更 | +----------+-----------------+--------------------------------------------------------------------+
$ make gcc -Wall -c -o cui_test.o cui_test.c gcc -Wall -c -o cui.o cui.c gcc -Wall -c -o dialog.o dialog.c gcc -Wall -c -o button.o button.c gcc -Wall -c -o label.o label.c gcc -Wall -c -o panel.o panel.c gcc -Wall -c -o handler.o handler.c gcc -Wall -c -o esc.o esc.c gcc -Wall -c -o key.o key.c gcc -Wall -o cui_test cui_test.o cui.o dialog.o button.o label.o panel.o handler.o esc.o key.o $ ./cui_test
矢印キーとENTERキーで操作して終了
部品はパネル、ラベル、ボタンだけのままで、少し機能を追加してみます
まずは簡単に、途中でラベルやボタンの文字列を変更可能にしてみます
$ make $ ./cui_test +--------------------+ | | | Are you sure ? | | | | (OK) (Cancel) | | | +--------------------+ 下矢印キー ENTERキー +--------------------+ | | | Realy ? sure ? | | | | (Yes) (No)cel) | | | +--------------------+ とりあえずENTERキーで終了 result Yes $
ダメだこりゃ
前の文字列の表示が残ってしまってます
という事で、部品の表示/非表示の機能を追加してみます。
文字列を変更したいときは、一旦表示を消して、内部で文字列を変更してから、
表示しなおすようにしてみます
ここでcui.c cui_draw() を見直してて、ふと気付きました。
兄弟の部品へのポインタ next で単純に再帰してますが、これはまずいのでは..?
void cui_draw(cui obj) { int val; if(obj == NULL) return; val = (obj == cui_focus) ? CUI_DRAW_FOCUS : CUI_DRAW_NORMAL; cui_handler_call(obj, obj->handler_list, CUI_EVT_DRAW, val); cui_draw(obj->next); cui_draw(obj->children); }
対象の部品の子供は全部表示したいですが、兄弟は関係ないはずです。
こうなってしまってる理由は、子供全部の表示を一回の呼び出しで済ませようとして、
先頭の子供から、兄弟へと連携させたためです。
初回だけはnextの再帰は不要です
cui_draw_chain() を追加して、兄弟の方向にも対応するようにしてみます
パネルを非表示にしたときは、当然パネルに入ってるボタンも非表示にしたいはずです。
ですが、入れものの中の部品全部の状態を設定して回るのも、ちょっと...
なので、表示する処理側で対処してみます。
部品を表示するとき、親の入れもの方向に非表示の状態のものが無いか判定を入れます。
なければ描画するし、あれば描画しません。
こうしておけば、入れものの表示/非表示の状態を変更するだけで、部品全部の描画の有無に影響を与えて、
かつ、中の部品個別にも、表示/非表示の状態を設定できます
そうした上で、ラベルの文字列の変更では、一旦非表示にしてから、文字列を変更し、再度表示しなおすようにします。
ボタンの文字列の変更でも、ラベルの設定関数を呼ぶようにしてるので、変更が反映されます。
最後の文字列を表示する処理自体は、ハンドラに設定されてるボタン用のものが呼び出されるので、問題ないはずです
$ cd make $ ./cui_test +--------------------+ | | | Are you sure ? | | | | (OK) (Cancel) | | | +--------------------+ 下矢印キー ENTERキー +--------------------+ | | | Realy ? | | | | (Yes) (No) | | | +--------------------+ ENTERキー result Yes $
うまくいきました
パネルの表示/非表示も確かめてみます
パネルは枠表示のついた入れものですが、 全ての部品は cui_base から継承してるので、 どの部品も入れものになれます
ということで、ダイアログと、ボタン1つを並べた「入れもの」を cui_base を使って用意して、cui_main() で実行してみます
とおもったら、cui_baseだけの生成関数を作ってませんでした。
追加しておきます
cui_test.c では、適当な大きさのベースの入れものを用意して、
その中にボタンと、ダイアログを配置します。
ダイアログはフラグで非表示に設定しておいて、ボタンが押されたら、
表示/非表示をトグルするようにしてみます
$ cd make $ ./cui_test (Show Dialog) 下矢印キー 反応なし!? さらに下矢印キー (Show Dialog) 反応しました... ENTERキー (Hide Dialog) +--------------------+ | | | Are you sure ? | | | | (OK) (Cancel) | | | +--------------------+ ふむふむ ENTERキー (Show Dialog) この状態から下矢印キーを押すたびに、フォーカスの有無がトグルしてます... フォーカスきてる状態で ENTERキーで (Hide Dialog) +--------------------+ | | | Are you sure ? | | | | (OK) (Cancel) | | | +--------------------+ この動作は、よし ここで下矢印キー (Hide Dialog) +--------------------+ | | | Are you sure ? | | | | (OK) (Cancel) | | | +--------------------+ ふむふむ 右矢印キー (Hide Dialog) +--------------------+ | | | Are you sure ? | | | | (OK) (Cancel) | | | +--------------------+ ありゃ?! そんな... ここでさらに右矢印を繰り返し押しても、フォーカスは 'Hide Dialog'と、OKを往復するだけ... とりあえずOKフォーカスのときに、ENTER (Hide Dialog) +--------------------+ | | | Realy ? | | | | (Yes) (No) | | | +--------------------+ まぁ、ここは以前の通り ENTER result Yes $
うーむ。いくつか問題がありました
まず部品が1つだけ表示されてる時に、矢印キーを押すと、おそらく、非表示の部品にフォーカスが移動してるであろう動作
これは原因が容易に想像できます。
cui.c cui_focus_move_judge() ここで、部品の表示/非表示を気にしてないからでしょう。
cui_is_visible() を使って、判定を追加しておきます
次は、OKボタンにフォーカスしたあと、右矢印でCancelボタンにフォーカスが移る事を期待してるのに、 'Hide Dialog'ボタンにフォーカスが戻る動作について
部品にフォーカスが有り、右矢印のときの、判定は
+------+------------------------------------+ |右矢印|右にある一番近い部品にフォーカス移動| +------+------------------------------------+ +----------------------------+--------------------------------------------+ |「右にある一番近い部品」とは|グローバルなx座標+wの値が自分以上であり、 | | |部品の中心からの距離が最小で、自分以外の部品| +----------------------------+--------------------------------------------+
でした
「値が自分以上」というのが少しひっかかるものの、 'Hide dialog'ボタンから右矢印で、部品の右端が、左にあるOKボタンにフォーカスが移動するのは、あきらかに、おかしい...
: }else if(cui_focus){ switch(key){ case CUI_KEY_UP: jg = cui_gy(obj) <= cui_gy(cui_focus); break; case CUI_KEY_DOWN: jg = cui_gy(obj) >= cui_gy(cui_focus); break; case CUI_KEY_LEFT: jg = cui_gx(obj) <= cui_gx(cui_focus); break; case CUI_KEY_RIGHT: jg = cui_gx(obj) >= cui_gx(cui_focus); break; } jg = jg && v < *val; :
バグ発見。
右矢印、下矢印のとき、wとhが考慮されてなかったです
: case CUI_KEY_DOWN: jg = cui_gy(obj) + obj->h >= cui_gy(cui_focus) + cui_focus->h; break; : case CUI_KEY_RIGHT: jg = cui_gx(obj) >= cui_gx(cui_focus); break; :
とすべきでした。
これはこれですが、それでも変です。
修正前の状態で、右矢印でOKボタンから'Hide Dialog'ボタンにフォーカスが移動してました。
修正前なので、部品の左端の座標で比較してるはずなのに、座標の値がより小さい部品へと移動してます
: v = cui_focus_move_val(obj, key); if(!old){ jg = TRUE; }else if(cui_focus){ switch(key){ case CUI_KEY_UP: :
ここ!
old が NULL なら、無条件で候補にしてるのがダメでした。
フォーカスが無い状態なら、これでいいですが、
フォーカスが有る状態からは、old が NULL のときも、座標の判定が必要でした
: if(cui_focus){ switch(key){ case CUI_KEY_UP: jg = cui_gy(obj) <= cui_gy(cui_focus); break; case CUI_KEY_DOWN: jg = cui_gy(obj) + obj->h >= cui_gy(cui_focus) + cui_focus->h; break; case CUI_KEY_LEFT: jg = cui_gx(obj) <= cui_gx(cui_focus); break; case CUI_KEY_RIGHT: jg = cui_gx(obj) + obj->w >= cui_gx(cui_focus) + cui_focus->w; break; } if(old) jg = jg && v < *val; }else if(!old){ jg = TRUE; }else{ switch(key){ case CUI_KEY_UP: case CUI_KEY_LEFT: jg = v < *val; break; case CUI_KEY_DOWN: case CUI_KEY_RIGHT: jg = v > *val; break; } } :
これでとりあえず、仕様通りの実装になったはずです
$ cd make $ ./cui_test (Show Dialog) 下矢印キー (Show Dialog) うむうむ ENTERキー (Hide Dialog) +--------------------+ | | | Are you sure ? | | | | (OK) (Cancel) | | | +--------------------+ うむうむ 下矢印キー (Hide Dialog) +--------------------+ | | | Are you sure ? | | | | (OK) (Cancel) | | | +--------------------+ うむうむ 右矢印キー (Hide Dialog) +--------------------+ | | | Are you sure ? | | | | (OK) (Cancel) | | | +--------------------+ はう!? 右矢印キー (Hide Dialog) +--------------------+ | | | Are you sure ? | | | | (OK) (Cancel) | | | +--------------------+ うむうむ ENTERキー result Cancel $
なるほど、よくみれば...
OKボタンの右端と'Hide Dialog'ボタンの右端を比べると、'Hide Dialog'ボタンの方が右にありますね。
そしてその距離が、Cancelボタンの右端よりも近いと。
なるほど納得です
フォーカスの移動が自然じゃない気もしますが、 とりあえず、これでフォーカスこない問題は解消したので、一旦よしとしましょう
ここまでのソースファイル cui20.tgz
ラベルの文字列が変更可能になったので、 長いメッセージを設定してみると、問題が出ました
$ make $ ./cui_test (Show Dialog) 下矢印、ENTER、下矢印 (Hide Dialog) +--------------------+ | | | Are you sure ? | | | | (OK) (Cancel) | | | +--------------------+ ENTER (Hide Dialog) +--------------------+ | | | Realy ? Select Yes or No button | | | (Yes) (No) | | | +--------------------+ うー ENTER result Yes $
ラベルの表示が、入れものからはみ出してます
cui.c の描画でクリッピングする事を考えてみます
void cui_draw_str(cui obj, int x, int y, char *s, int attr) { cui_esc_loc(cui_gx(obj) + x, cui_gy(obj) + y); cui_esc_attr(attr); printf("%s", s); fflush(stdout); }
この関数で親の入れもののサイズを考慮して、 描画する範囲を制限してみます
歴代の祖先の入れもののサイズを調べて、
自分の座標系で、見えている範囲を取得出来るようにしてみます。
実装は再帰になるので、直接の親との関係だけに注目すれば簡単です
RECTを扱うので、構造体を追加しておきます
typedef struct cui_rect{ int x, y, w, h; } *cui_rect;
cui_base構造体のメンバ x, y, w ,h を置き換えたくなりますが... まだ時期早尚な気もするので、そのままにしておきます ;-p)
void cui_rect_init(cui_rect r, int x, int y, int w, int h) { r->x = x; r->y = y; r->w = w; r->h = h; } void cui_rect_wh_to_x2y2(cui_rect r) { r->w += r->x; r->h += r->y; } void cui_rect_x2y2_to_wh(cui_rect r) { r->w -= r->x; r->h -= r->y; } int cui_rect_and(cui_rect a, cui_rect b, cui_rect r) { struct cui_rect tmp; cui_rect t = &tmp; cui_rect_wh_to_x2y2(a); cui_rect_wh_to_x2y2(b); cui_rect_init(t, a->x > b->x ? a->x : b->x, a->y > b->y ? a->y : b->y, a->w < b->w ? a->w : b->w, a->h < b->h ? a->h : b->h); cui_rect_x2y2_to_wh(t); cui_rect_x2y2_to_wh(a); cui_rect_x2y2_to_wh(b); *r = *t; return !cui_rect_chk_empty(r); } int cui_rect_chk_empty(cui_rect r) { return r->w <= 0 || r->h <= 0; } int cui_visible_rect(cui obj, cui_rect r) { struct cui_rect tmp; cui_rect t = &tmp; cui_rect_init(r, 0, 0, obj->w, obj->h); if(obj->parent == NULL) return !cui_rect_chk_empty(r); /* empty --> FALSE */ if(!cui_visible_rect(obj->parent, t)) return FALSE; t->x -= obj->x; t->y -= obj->y; return cui_rect_and(r, t, r); /* r and t --> r */ }
この cui_visible_rect() を使って、cui_draw_str() を変更します
void cui_draw_str(cui obj, int x, int y, char *s, int attr) { struct cui_rect t1, t2; cui_rect r1 = &t1, r2 = &t2; int n = strlen(s); char *s2 = NULL; if(!cui_visible_rect(obj, r1)) return; cui_rect_init(r2, x, y, n, 1); if(!cui_rect_and(r1, r2, r2)) return; /* r1 and r2 --> r2 */ s += r2->x - x; if(r2->x + r2->w < x + n){ n = r2->w; if((s2 = malloc(n + 1)) == NULL) ERR("No Mem"); memcpy(s2, s, n); s2[n] = '\0'; s = s2; } x = r2->x; cui_esc_loc(cui_gx(obj) + x, cui_gy(obj) + y); cui_esc_attr(attr); printf("%s", s); fflush(stdout); if(s2) free(s2); }
そして、RECT関連は rect.h , rect.c に分離しておきます
$ make $ ./cui_test (Show Dialog) 下矢印、ENTER (Hide Dialog) +--------------------+ | | | Are you sure ? | | | | (OK) (Cancel) | | | +--------------------+ 下矢印、ENTER (Hide Dialog) +--------------------+ | | | Realy ? Selec | | | | (Yes (No) | | | +--------------------+ むー ENTER result Yes $
文字列を変更する前の部品のサイズで、クリッピングされてます。
これはこれで、クリッピングの動作としては正しいです。
文字列の長さが変わっても、部品の幅 w を追随させてないから、こうなってるはずです。
ということで処理を追加
$ make $ ./cui_test (Show Dialog) 下矢印、ENTER、下矢印、ENTER (Hide Dialog) +--------------------+ | | | Realy ? Select Yes | | | (Yes) (N | | | +--------------------+ ありゃ〜 上矢印、ENTER、ENTER、下矢印 (Hide Dialog) +--------------------+ | | | Realy ? Select Yes | | | (Yes) (No) | | | +--------------------+ なぜかNoボタン直ってる... ENTER result Yes $
どういうことか?
まずラベルの方は、期待通りの動作。
枠にかかってるのは、まぁしょうがなし
Yesボタンの方も期待通り
Noボタンがなぜこうなるか?
なるほど
label や button の str_set() にきたら、まずフラグをみて、表示/非表示の状態を確認して、
表示状態ならそのことを記録しておいて、cui_hide()を呼ぶ。
文字列変更した後は、記録をみて、必要ならばcui_show()を呼んで、元の表示状態にする
$ make $ ./cui_test (Show Dialog) 下矢印、ENTER、下矢印、ENTER、 (Hide Dialog) +--------------------+ | | | Realy ? Select Yes | | | (Yes) (No) | | | +--------------------+ よすよす ENTER resut Yes $
ボタンの表示でも、ラベルの使える処理は、出来るだけ呼び出して利用したいと考えます
ボタンのフォーカスとして下線をつけたくても、今のラベルでは下線がつきません。
まずはラベルの表示に、装飾が出来るように拡張してみます
ラベルはキーフォーカスを受けないので、あくまでオプションとして、装飾の機能を追加します。
とりあえず関数まで用意せずとも、構造体のメンバとして int attr を追加して、
初期値は CUI_ESC_NORMAL にしておきます。
変更したい人が、メンバ attr に値を設定して cui_draw() すれば、
ラベルのハンドラが、attr を参照して描画するようにしておきます
$ make $ ./cui_test (Show Dialog) 下矢印、ENTER (Hide Dialog) +--------------------+ | | | Are you sure ? | | | | (OK) (Cancel) | | | +--------------------+ 矢印、ENTER (Hide Dialog) +--------------------+ | | | Realy ? Select Yes | | | (Yes) (No) | | | +--------------------+ ENTER result Yes $
OK
では、ボタンの実装をできるだけラベルを使って書換えてみます
今のところパネルじゃなくても、全ての部品は入れものになれます。
ボタンは "("、"hoge"、")" の3つのラベルを配置してる「入れもの」と捕えたらどうでしょう。
これまでボタンはラベルから継承してましたが、ラベルを保持した入れものとして実装してみます
とりあえず、今まで通り動作してます
button.c で、ラベルに追加したメンバ attr への値の設定してますが、解りにくいです。
やはり、専用の関数を用意する事にします
あら? 終了したときに、ボタンの表示がアクティブな表示のまま?
これは、エスケープシーケンスの罠です。
esc.h CUI_ESC_REVERSE 指定の後、
CUI_ESC_ULINE を指定しても、反転の属性はそのままです。
反転の属性を解除するには、CUI_ESC_REVERSE_OFF を指定するか、
CUI_ESC_NORMAL を指定して、全部の属性を解除するしかありません
指定の値は、ビットのORになってる訳でもないのに、 OR指定してるかのような動作になってるという罠です
とりあえず cui.c cui_draw_str() の出口で、 CUI_ESC_NORMAL に設定して、属性の指定を解除するようにしておきます
これで元に戻りました
さらに気付いた個所を修正しておきます
cui.c cui_draw_str() の出口で必ず CUI_ESC_NORMAL に設定されるようにしたので、 入り口でも CUI_ESC_NORMAL 以外に設定するときだけ、属性の設定をするようにしておきます
x, y, w, h の変更するときの処理も、
専用の関数を追加しておきます。
表示状態なら、非表示にしてから変更し、
最後に表示状態に戻すようにします
ボタンの文字列の変更時も、保持してる')'ラベルの移動と、 自身の幅wを変更する処理で、専用関数を用いるようにします
今のところ全ての部品は、入れものとしても機能します
今度は逆に、ラベルの中に、ボタンを入れて試してみます
$ make $ ./cui_test (Show Dialog) 下矢印、ENTER、下矢印 (Hide Dialog) +--------------------+ | | | Are you sure ? | | | | (OK) (Cancel) | | | +--------------------+ そしてENTERで (Hide Dialog) +--------------------+ | | system panic! (reboot) (power off) | | | (Yes) (No) | | | +--------------------+ ENTER (Hide Dialog) +--------------------+ | | It's a joke (sorry) | | | (Yes) (No) | | | +--------------------+ ENTER (Hide Dialog) +--------------------+ | | | Realy ? | | | | (Yes) (No) | | | +--------------------+ めでたしめでたし 下矢印、ENTERで無事終了 result Yes $
などと遊んでみました。
ほぼ思惑通りでしたが、色々試すといくつか気付く点がありました
まず、フォーカス移動について。
(reboot) (power off)ボタンが表示された時点で、矢印操作をすると、他のボタンも選べてしまいます。
これでは、悪戯の肝の部分が損なわれます。
なんとしても、フォーカス移動を2つのボタンだけに制限出来るようにせねば!
要するに追加したボタン付きのラベルを、モーダル・ダイアログのように振舞わせたいわけです。
安直に考えると、引数指定して cui_main() を呼び直せばそれでいいのかも。
ボタンのハンドラの中からですが、cui_main() が受け取る引数 cui top_obj は、
グローバル変数に記録されないので、スタック上にあります。
モーダル・ダイアログとしての呼び出しの cui_main() か戻っても、
最初の cui_main() 呼び出しの方の top_obj は、スタック上に残ってて大丈夫そうです
逆にグローバル変数として状態を持っているのは、フォーカスしてる部品を記録してる cui cui_focus と、
メインループの処理の停止指示用に用意したint cui_running です。
これを、なんとかしなければなりません。
cui_focus の方は、同時に2つフォーカスする事はまず不要なので、 前のcui_main()のフォーカスを覚えておいて、 重ねたcui_main()の処理が終れば、前のフォーカスに戻せばなんとかなりそうです
cui_running をどうすべしか。
cui_main() の呼び出し自体が、ネストした包含関係があるので、
ここはスタックの構造で考えたらええでしょう。
cui_main() を呼び出すと、その回の呼び出しに関する状態の情報は、
スタックにプッシュします。
cui_quit() からは、スタックの先頭の情報をみて処理します。
cui_main() から抜ける直前に、その回の情報をスタックからポップして削除します。
さて、どう実装すべしか...
スタックの先頭の情報は、cui_main() の外部である、cui_quit() から参照可能にしないとダメですが、
スタック上のデータそのものは、cui_main() のローカル変数のスタックを使ってしまいたいところです。
スタックの先頭を指すポインタだけ、外部にグローバル変数として持たせて、
残りは、前のスタックへのチェーンも含めて、cui_main()のローカル変数として持ってれば、いけそうです。
この方向で実装してみます
どうせやるなら、フォーカスの記録 cui_focusの方も、スタックに積むようにしてみます
とりあえず以前と同じ動作か確認
$ make $ ./cui_test 下矢印、ENTER、下矢印、ENTER (Hide Dialog) +--------------------+ | | system panic! (reboot) (power off) | | | (Yes) (No) | | | +--------------------+ ENTER、ENTER (Hide Dialog) +--------------------+ | | | Realy ? | | | | (Yes) (No) | | | +--------------------+ 下矢印、ENTERで終了
問題なさそうです
これを元に、cui_main() を多重で呼び出しても大丈夫かどうか
はたしてどうか?
$ make $ ./cui_test 下矢印、ENTER、下矢印、ENTER (Hide Dialog) +--------------------+ | | system panic! (reboot) (power off) | | | (Yes) (No) | | | +--------------------+ うーむ。前のcui_main()のフォーカス表示が見えたまま... まぁそうなるか 左矢印 (Hide Dialog) +--------------------+ | | system panic! (reboot) (power off) | | | (Yes) (No) | | | +--------------------+ ダメー orz 右矢印、ENTER (Hide Dialog) +--------------------+ | | It's a joke (sorry) | | | (Yes) (No) | | | +--------------------+ 下矢印 (Hide Dialog) +--------------------+ | | It's a joke (sorry) | | | (Yes) (No) | | | +--------------------+ やっぱしダメー orz 上矢印、ENTER、ENTER result Yes $
ショック...。ダブルショック。
期待通りに動作しない上に、端末の設定が、元に戻ってないです (T_T)
cui.c から呼び出してる key.c cui_key_enter(), cui_key_exit() の処理
これが状態をもってました
static struct termios term_bak; void cui_key_enter(void) { struct termios raw; tcgetattr(KEY_FD, &term_bak); cfmakeraw(&raw); raw.c_lflag &= ~FLUSHO; tcsetattr(KEY_FD, TCSANOW, &raw); } void cui_key_exit(void) { tcsetattr(KEY_FD, TCSANOW, &term_bak); }
端末の設定を、グローバル変数 term_bak に保存してました orz
APIを変更して対処します。
cui_key_enter() で元の端末設定は malloc() した領域に保存し、返すようにします。
cui_key_exit() で保存した領域を受け取って、設定を戻した後は free() で領域を解放します
これで、とりあえず端末の状態は、元に戻るようになりました
そして、フォーカスの制限が出来てないのはなぜか?
前に cui_draw() で、next 側も表示してたのと、同じ誤りでした。
cui.c cui_focus_move_judge() に top_obj を渡しているので
if(obj == NULL) return old; old = cui_focus_move_judge(obj->next, key, old, val); old = cui_focus_move_judge(obj->children, key, old, val);
この next 方向の再帰で、関係のない next 以降もフォーカス判定の
範囲に入ってました。
cui_draw() での対処と同じく cui_focus_move_judge() に対して
cui_focus_move_judge_chain() を追加しておきます
$ make $ ./cui_test 下矢印、ENTER、下矢印、ENTER (Hide Dialog) +--------------------+ | | system panic! (reboot) (power off) | | | (Yes) (No) | | | +--------------------+ 左矢印 フォーカスそのままです 各矢印キーでも rebootボタンと'power off'ボタンにしか、 フォーカスは移りません よしよし ENTER、ENTER、ENTER で終了 result Yes $
デバッグしながらなので、完全にネタばれだ
ここで一旦ブラッシュアップしときます
+--------+--------------------+----------------------------------------------------+ |ファイル| 関数 | 内容 | +--------+--------------------+----------------------------------------------------+ |cui.c |cui_focus_move_judge|focus移動の関数を focus.c へ分離 | |cui.h +--------------------+----------------------------------------------------+ | |cui_free |前に cui_draw() でnext 側も表示してたのと同じ誤り | | | |cui_free_chain() 追加して対策 | | +--------------------+----------------------------------------------------+ | |cui_del |cui_xxx_new() で親の入れものに追加するが、 | | | |取り外し用の関数が無かったので追加 | | +--------------------+----------------------------------------------------+ | |cui_draw_str |装飾指定 attr がエスケープシーケンスの罠でややこしい| | |cui_fill_rect |CUI_ATTR_NORMAL | | | |CUI_ATTR_ULINE | | | |CUI_ATTR_REVERSE | | | |を新規追加して、論理和で指定できるように変更 | +--------+--------------------+----------------------------------------------------+ |esc.c |cui_esc_enter |カーソル位置を保存して状態を持っていた | | |cui_esc_exit |この場合は変数にではなく、 | | | |エスケープシーケンスの機能自体が状態を持っている | | | |ネストしてenter, exitの組で呼び出されるので、 | | | |カウンタを追加して初回だけ処理するように変更 | +--------+--------------------+----------------------------------------------------+ |key.c |cui_key_enter |mallocして状態を保存して対策したが、少々大げさだった| | |cui_key_exit |cui_esc_enter(), cui_esc_exit() と同様に、 | | | |カウンタの処理に変更しておく | +--------+--------------------+----------------------------------------------------+ |fill.c |cui_fill_new |パネルの枠の描画用に塗りつぶし用の単純な部品として、| |fill.h |cui_fill_init |ラベルから継承して追加 | | | |ラベルと同様に文字列と装飾の指定を保持するが | | | |w, h の範囲を文字列で埋めるように描画する | +--------+--------------------+----------------------------------------------------+ |cui.c |cui_fill_rect |cui_fillの追加に伴って、1文字の繰り返しから | |cui.h | |文字列の繰り返しに拡張して変更 | +--------+--------------------+----------------------------------------------------+ |panel.c |cui_panel_hdr |パネルのハンドラは廃止 | | | |cui_fillを並べただけの入れものとして実装 | +--------+--------------------+----------------------------------------------------+
ここまでのソースファイル cui35.tgz
以前 cui.c に cui_xywh_set() を追加しましたが、内部的に使ってるだけで、
ちゃんと動作確認してませんでした。
cui_test.c から簡単に試してみます
$ make $ ./cui_test +--------------------------------------+ | (Show Dialog) (move) | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | +--------------------------------------+ 右矢印 +--------------------------------------+ | (Show Dialog) (move) | | | : ENTER +--------------------------------------+ | (Show Dialog) 2 | | (move) | | | : ENTER、ENTER、ENTER +--------------------------------------+ | (Show Dialog) 5 | | | | | | | | (move) | | | : よしよし 上矢印 +--------------------------------------+ | (Show Dialog) 5 | | | | | | | | (move) | | | : ENTER +--------------------------------------+ | (Hide Dialog) 5 | | +--------------------+ | | | | | | | Are you sure ? | | | | (move)| | | | (OK) (Cancel) | | | | | | | +--------------------+ | | | : いつものやつの表示も出ます 右矢印、右矢印 +--------------------------------------+ | (Hide Dialog) 5 | | +--------------------+ | | | | | | | Are you sure ? | | | | (move)| | | | (OK) (Cancel) | | | | | | | +--------------------+ | | | : (move)にフォーカスして ENTER、ENTER +--------------------------------------+ | (Hide Dialog) 7 | | +--------------------+ | | | | | | | Are you sure ? | | | | | | | | (OK) (Can | | | | (move)| | | +--------------------+ | | | : まぁそうなるでしょうな... ENTER連打... +--------------------------------------+ | (Hide Dialog) 12 | | +--------------------+ | | | | | | | Are you sure ? | | | | | | | | (OK) (Can | | | | | | | +-------------- + | | | | | | | | (move) | | | | | | | | | | | | | +--------------------------------------+ (Cancel)さんに、ひどいことしてからに ENTER連打... +--------------------------------------+ | (Hide Dialog) 19 | | +--------------------+ | | | | | | | Are you sure ? | | | | | | | | (OK) (Can | | | | | | | +-------------- + | | | | | | | | | | | | | | | | | | | | | +----------------(move)----------------+ ENTER連打... +--------------------------------------+ | (Hide Dialog) 23 | | +--------------------+ | | | | | | | Are you sure ? | | | | | | | | (OK) (Can | | | | | | | +-------------- + | | | | | | | | | | | | | | | | | | | | | +---------------- ----------------+ 食べちらかして、どこかに行ってしまわれました 上矢印 +--------------------------------------+ | (Hide Dialog) 23 | | +--------------------+ | | | | | | | Are you sure ? | | | | | | | | (OK) (Cancel) | | | | | | | +-------------- + | | | : (Cancel)にフォーカスがくると、 下線つきで再描画するので、表示が戻りました ENTER で終了 relust Cancel $
いくつか問題がありました
まず、見えてない部品にフォーカスしてるのはマズイです
focus.c cui_focus_move_judge() の処理で
if(obj == focus) return old; if(!(obj->flags & CUI_FLG_CAN_FOCUS)) return old; if(!cui_is_visible(obj)) return old;
cui_is_visible() を使って非表示の人ははじいてましたが、
今回は、入れもののクリッピングで見えてない場合になります。
cui_visible_rect() を使って判定を追加してみます。
見えてる領域の有/無だけを判定したく、見えてるRECTの情報は不要なので、
cui_visible_rect() のラッパ関数をcui_is_visible_rect()として追加しておきます
そして、フォーカスを持った人が移動して見えなくなっても、
フォーカスを持ったままで、よろしくないでしょう。
これは、フォーカスを持った人が、非表示になって消えた場合でも、同様に思えます。
そういう状況になると、すみやかにフォーカスなし状態に設定したいのですが、
さてどこで判定すべきか?
次のフォーカス移動の要求を待っての処理にすると、判定箇所が絞りこめそうではありますが、
例えばフォーカスのボタンが消えてから、そのままENTERキーで、ボタンのハンドラが呼ばれてしまいます。
やはり消えるアクションがあった末尾で、フォーカス判定すべしでしょう
となると cui_xywh_set() と cui_hide()
ここでフォーカスなし状態か判定を入れてみます
ちょっと動かしてみて、即ダメと分かりました。
(Show Dialog) (Hide Dialog)ボタンを押すたびに、フォーカスなし状態になります
例えば label.c cui_label_str_set() では、
変更前に cui_hide() で非表示状態にして、
変更後に cui_show() で表示状態に戻してます。
ボタンは内部的にラベルで構成されてるから、
ここで、フォーカスもってたら、離してしまうと...
なのでここでは、cui_hide() の中でフォーカス判定はやめておきます
フォーカスの確認用の関数だけ用意しておいて、
cui_hide() を呼ぶ人が、必要な場面では、
呼び終ってから責任を持って、フォーカスの確認用の関数を呼び出す事にします。
今のところ、未だそのような場面はないので、呼び出しの追加はなしです ;-p)
$ make $ ./cui_test +--------------------------------------+ | (Show Dialog) (move) | | | : 左矢印、ENTER、右矢印、右矢印 +--------------------------------------+ | (Hide Dialog) (move) | | +--------------------+ | | | | | | | Are you sure ? | | | | | | | | (OK) (Cancel) | | | | | | | +--------------------+ | | | : ENTER x 18回 +--------------------------------------+ | (Hide Dialog) 19 | | +-------------- + | | | | | | | Are you sure | | | | | | | | (OK) (Can | | | | | | | +-------------- + | | | | | | | | | | | | | | | | | | | | | +----------------(move)----------------+ ENTER +--------------------------------------+ | (Hide Dialog) 20 | | +-------------- + | | | | | | | Are you sure | | | | | | | | (OK) (Can | | | | | | | +-------------- + | | | | | | | | | | | | | | | | | | | | | +---------------- ----------------+ さらに ENTERで、数字の表示に変化なし (^_^v OKです。ハンドラは呼ばれてません 画面から消えたら、フォーカスなしになってます 上矢印、左矢印、ENTER で終了 result Cancel $
次なる問題は、移動による食べちらかしです
cui.c cui_xywh_set() の処理は、cui_hide() で消して、 x, y, w, h を変更して、cui_show() で再表示
消すときの cui_hide() では、フラグのHIDEを設定して、
自分の領域をクリアだけ。
また cui_hide() が注目を集めてます...
本来ならここで、もし下に隠れてた部品が見えるようになったなら、
その領域の表示をアップデートで再表示させたいところです。
でも、部品の位置のオーバーラップがあるかとか、
不要な箇所まで再表示させてしまったら、重くなるとか...
ここもまた、呼び出し側の責任ということで、 cui_hide() 自体はいじらずにおいて、 必要と思った人が、cui_hide() を呼び出した後に、 cui_draw(obj->parent) を呼び出して、 オーバーラップしていた可能性のある、親の入れものに入っている 兄弟の部品を再描画させる事にしておきます
つまり結局、cui_xywh_set() から、cui_hide() を呼んだあとに、 親の入れものの再描画処理を追加してみます
$ make $ ./cui_test 左矢印、ENTER、右矢印、右矢印 +--------------------------------------+ | (Hide Dialog) (move) | | +--------------------+ | | | | | | | Are you sure ? | | | | | | | | (OK) (Cancel) | | | | | | | +--------------------+ | | | : ENTER連打 +--------------------------------------+ | (Hide Dialog) 10 | | +--------------------+ | | | | | | | Are you sure ? | | | | | | | | (OK) (Cancel) | | | | | | | +--------------------+ | | | | (move) | | | : よすよす 上矢印、ENTERで終了 result Cancel $
移動して見えなくなった部品があるなら、スクロールして見れるようにしてみます
既にクリッピング表示と移動は対応してるので、下地は十分出来てるように思えます。
例えば大きな部品を、小さな入れもののなかに配置します。
小さな入れもの領域でクリッピングされて表示されるので、大きな部品の一部しか表示されません。
大きな部品の配置してる位置を移動させれば、見えてる範囲が変わります
本当にこんなのでいけるのだろうか。
とりあえず実装してみましょう
$ make $ ./cui_test +--------------------------------------+ | (Show Dialog) (move) (U) | | (L)(D)(R) | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | +--------------------------------------+ 左矢印 +--------------------------------------+ | (Show Dialog) (move) (U) | | (L)(D)(R) | : ENTER +--------------------------------------+ | (Hide Dialog) (move) (U) | | (L)(D)(R) | | +----------------------------------+ | | |+ | | | | +--------------------++----------| | | | | || | | | | | Are you sure ? || Continue| | | | | || | | | | | (OK) (Cancel) || (Yes) | | | | | || | | | | +--------------------++----------| | | | +------------------+ +----------| | | | | | | | | | | | How are you ? | | (A1) | | | | | | | | | | | | (^_^) (T_T) | | (C3) | | | | | | | | | | +----------------------------------+ | +--------------------------------------+ でたでた なんか思惑にない '+' が1つ見えてまするが... 右矢印 +--------------------------------------+ | (Hide Dialog) (move) (U) | | (L)(D)(R) | | +----------------------------------+ | : 右矢印 +--------------------------------------+ | (Hide Dialog) (move) (U) | | (L)(D)(R) | | +----------------------------------+ | | |+ | | | | +--------------------++----------| | | | | || | | | | | Are you sure ? || Continue| | | | | || | | | | | (OK) (Cancel) || (Yes) | | | | | || | | : ぬぬ、なんともキーによるフォーカス移動が、はがゆい! さらに右矢印 +--------------------------------------+ | (Hide Dialog) (move) (U) | | (L)(D)(R) | | +----------------------------------+ | : って orz とりあえず、右矢印 (Cancel)フォーカスで ENTERで終了 result Cancel $
スクロール以前に、フォーカスの移動に難あり
まず、Y方向の距離は見た目と違って、数値的にはXの2倍ほど近い事が、かなりはがゆいです。
cui.c cui_glen() はフォーカス移動でしか使ってないので、
Y方向の距離はXの2倍として扱うように変更します
そして以前から心にひっかかっってる「自分以上」という記述
+----------------------------+--------------------------------------------+ |「右にある一番近い部品」とは|グローバルなx座標+wの値が自分以上であり、 | | |部品の中心からの距離が最小で、自分以外の部品| +----------------------------+--------------------------------------------+
「自分以上」じゃなくて「自分より大きな値」であって、 「自分以下」は「自分より小さな値(未満な値)」にすべきでしょう
focus.c cui_focus_move_judge() 変更します
$ make $ ./cui_test 左矢印、ENTER +--------------------------------------+ | (Hide Dialog) (move) (U) | | (L)(D)(R) | | +----------------------------------+ | | |+ | | | | +--------------------++----------| | : 矢印キーを色々と... よーし かなり自然な感じでフォーカスが移動するようになりました (R)にフォーカスして ENTER +--------------------------------------+ | (Hide Dialog) (move) (U) | | (L)(D)(R) | | +----------------------------------+ | | | | | | |+--------------------++-----------| | | || || | | | || Are you sure ? || Continue | | | || || | | | || (OK) (Cancel) || (Yes) | | | || || | | | |+--------------------++-----------| | | |+------------------+ +-----------| | | || | | | | | || How are you ? | | (A1) | | | || | | | | | || (^_^) (T_T) | | (C3) (| | | || | | | | | +----------------------------------+ | +--------------------------------------+ おおー ENTER連打ー +--------------------------------------+ | (Hide Dialog) (move) (U) | | (L)(D)(R) | | +----------------------------------+ | | | | | | |------------++-----------------+ | | | | || | | | | |u sure ? || Continue ? | | | | | || | | | | | (Cancel) || (Yes) (No) | | | | | || | | | | |------------++-----------------+ | | | |----------+ +-----------------+ | | | | | | | | | | |e you ? | | (A1) (B2) | | | | | | | | | | | | (T_T) | | (C3) (D4) | | | | | | | | | | | +----------------------------------+ | +--------------------------------------+ 続いて (D)フォーカスにして ENTER連打ー +--------------------------------------+ | (Hide Dialog) (move) (U) | | (L)(D)(R) | | +----------------------------------+ | | | || | | | | | (Cancel) || (Yes) (No) | | | | | || | | | | |------------++-----------------+ | | | |----------+ +-----------------+ | | | | | | | | | | |e you ? | | (A1) (B2) | | | | | | | | | | | | (T_T) | | (C3) (D4) | | | | | | | | | | | | | | | | | | | | | | | | | | | +----------------------------------+ | +--------------------------------------+ スクロール動作自体は大丈夫そうですが、 中身のbigな入れものが、ぎりぎり足りてないのかな? 続いて (L)フォーカスにして ENTER連打 +--------------------------------------+ | (Hide Dialog) (move) (U) | | (L)(D)(R) | | +----------------------------------+ | | | | || | | | | | (OK) (Cancel) || (Ye| | | | | || | | | | +--------------------++------| | | | +------------------+ +------| | | | | | | | | | | | How are you ? | | (A1)| | | | | | | | | | | | (^_^) (T_T) | | (C3| | | | | | | | | | | | | | | | | | | | | | | | | | +----------------------------------+ | +--------------------------------------+ 続いて (U)フォーカスにして ENTER連打 +--------------------------------------+ | (Hide Dialog) (move) (U) | | (L)(D)(R) | | +----------------------------------+ | | | | | | | | | | | + | | | | +--------------------++------| | | | | || | | | | | Are you sure ? || Cont| | | | | || | | | | | (OK) (Cancel) || (Ye| | | | | || | | | | +--------------------++------| | | | +------------------+ +------| | | | | | | | | | | | How are you ? | | (A1)| | | | | | | | | | +----------------------------------+ | +--------------------------------------+ やっぱり '+' がありますね。? とりあえず (Cancel)フォーカス ENTER で終了 result Cancel $
cui_test.c みなおしてみると、big_new()の中で最初のdialogの位置指定を、間違えてました。
修正しておきます
jokeラベルをのっける入れものは、smallの中の入れものにしてると、 見えないかもしれないので、外の入れものに載せるように変えておきます
ここにきて button.c のハンドラで 200 msec のウェイトが、重く感じます。
50 msec くらいにしてみます
$ make $ ./cui_test 左、ENTER +--------------------------------------+ | (Hide Dialog) (move) (U) | | (L)(D)(R) | | +----------------------------------+ | | |+--------------------++-----------| | | || || | | | || Are you sure ? || Continue | | | || || | | | || (OK) (Cancel) || (Yes) | | | || || | | | |+--------------------++-----------| | | |+------------------+ +-----------| | | || | | | | | || How are you ? | | (A1) | | | || | | | | | || (^_^) (T_T) | | (C3) (| | | || | | | | | |+------------------+ +-----------| | | +----------------------------------+ | +--------------------------------------+ キー操作いろいろ... +--------------------------------------+ | (Hide Dialog) 15 (U) | | (L)(D)(R) | | +----------------------------------+ | | | || | | | | |) (No) || (Yes) (No) | | | | | || | | | | |-------------++-----------------+ | | | |-----------+ +-----------------+ | | | | | | | | | | |re you ? | | (A1) (B2) | | | | | | | | | | | |) (T_T) | | (C3) (D4) | | | | | | | | | | | |-----------+ +-----------------+ | | | | (move) | | | | | | | | | | | +----------------------------------+ | +--------------------------------------+ (No)フォーカスでENTERで終了 result No $
なかなかいい感じです。
多少画面がちらついては、います
ここまでのソースファイル cui42.tgz
ラベル変化させていた(Show Dialog) (Hide dialog)ボタンは、 ON/OFFの状態をもっていて、いわゆるチェックボックスのように機能させてました
ボタンでやせ我慢せずに、ちゃんとチェックボックスを用意してみます
イベントはボタンと同じくCUI_EVT_BUTTONを使って、
値valにTRUE, FALSEの状態を渡すようにします。
ボタンのときは値valは常に0にしてましたが、こうして使うべきものだったんですね
$ make $ ./cui_test +--------------------------------------+ | [ ] Show (move) (U) | | (L)(D)(R) | | | : 左矢印 +--------------------------------------+ | [ ] Show (move) (U) | | (L)(D)(R) | | | : ENTER +--------------------------------------+ | [X] Show (move) (U) | | (L)(D)(R) | | +----------------------------------+ | | |+--------------------++-----------| | | || || | | | || Are you sure ? || Continue | | | || || | | | || (OK) (Cancel) || (Yes) | | | || || | | | |+--------------------++-----------| | | |+------------------+ +-----------| | | || | | | | | || How are you ? | | (A1) | | | || | | | | | || (^_^) (T_T) | | (C3) (| | | || | | | | | |+------------------+ +-----------| | | +----------------------------------+ | +--------------------------------------+ OK 確かにENTERでトグルします (Cancel)フォーカス ENTER で終了します result Cancel $
ここで一息入れて、ささいなブラッシュアップをば
ここまでの試した部品の配置の場面では、 端っこに配置された部品の右下の座標を算出して、 入れ物のサイズを決める場面が多く出てきました
少しでも判り易くするために、右下の座標を返す専用の関数を追加して、 書き換えておきます
チェックボックスに続いてラジオボタンを追加してみます
ON/OFFの状態をもったボタンである事は、チェックボックスとよく似てるので、 チェックボックスから継承して作ってみます
ラジオボタンの難しいところは、単体ではあまり意味がなくて、
複数のボタンがグループとして機能している事でしょう。
ラジオボタン専用のグループの型などがあったりして、どうも複雑になりがちです。
そんな事も、近頃ラジオボタンをあまり見かけない原因の一つかもしれません
グループと言えば、入れものに入ってる部品もグループです。
どの部品も入れものになれるので、ここはひとつ次のような縛りをかせてみます
ラジオボタンの直接の親の入れものは、ラジオボタンしか入れてはいけない事にします。
こうしておけば、ラジオボタンは、自分を載っけてる親の入れものから、
children, next, next, ... とグループのボタン達をたぐって、
どのボタンがONになってるか判定できます
ラジオボタンなので、cui_test.c にラジオ的なパネルを追加して、試してみます
コーディングしてみると、サンプル部品の配置がどうにもややこしくなってきたので、 いくつか便利機能を追加しておきます
$ make $ ./cui_test 左矢印、ENTER +--------------------------------------+ | [X] Show (move) (U) | | (L)(D)(R) | | +----------------------------------+ | | |+--------------------+ +----------| | | || | |Radio | | | || Are you sure ? | | ( ) ON (| | | || | | | | | || (OK) (Cancel) | | | | | || | | | | | |+--------------------+ | | | | |+------------------+ +----------| | | : 右矢印 x 4 でフォーカスを(R)に +--------------------------------------+ | [X] Show (move) (U) | | (L)(D)(R) | : ENTER x 10 +--------------------------------------+ | [X] Show (move) (U) | | (L)(D)(R) | | +----------------------------------+ | | |-----------+ +-------------------+| | | | | |Radio || | | | sure ? | | ( ) ON (O) OFF || | | | | | || | | | (Cancel) | | || | | | | | || | | |-----------+ | || | | |---------+ +-------------------+| | | | | | | : やっと全貌が明かになりました 下、左でフォーカスを ( ) ON に移して ENTER : | |-----------+ +-------------------+| | | | | |Radio || | | | sure ? | | (O) ON ( ) OFF || | | | | | ( ) ABC kHz|| | | | (Cancel) | | ( ) KBS || | | | | | ( ) MBS || | | |-----------+ | ( ) OBC || | | |---------+ +-------------------+| | : 下矢印で局を選び ENTER : | |-----------+ +-------------------+| | | | | |Radio || | | | sure ? | | (O) ON ( ) OFF || | | | | | ( ) ABC 1179 kHz|| | | | (Cancel) | | ( ) KBS || | | | | | (O) MBS || | | |-----------+ | ( ) OBC || | | |---------+ +-------------------+| | : 局を選ぶと周波数の表示が切り替わり、 ON/OFFで下の表示/非表示 OKです (Cancel)フォーカス ENTERで終了 result Cancel $
プルダウンメニューの実装を考えてみます
選択したアイテムの状態を保持している、ポップアップメニューを考える前に、 まず、いわゆるメニューバーから吊り下がっているような、 プルダウンメニューで考えてみます
動作と機能を考えてみると、とりあえず手持ちの部品のパネルとボタンでも、
似たようなあものを作って試せそうです。
例えば、ボタンを押すと、メニューアイテムに相当するボタンを縦に配置したパネルを表示して、
モーダルダイアログとしてメイン関数を呼び出すようにします。
これで、フォーカスはメニューアイテムのボタンの間だけで移動するモードになれます。
各ボタンにハンドラを登録しておけば、アイテムを選択したときに、
ボタンのイベントでハンドラが呼び出され、処理後に、パネルを消しておけばよさそうです
$ make $ ./cui_test +--------------------------------------+ |(File) [ ] Show (move) (U) | | (L)(D)(R) | | | : とりあえずFileメニューのボタンを左端に追加しておいて 左、右、ENTERで、いつものダイアログを表示 +--------------------------------------+ |(File) [X] Show (move) (U) | | (L)(D)(R) | | +----------------------------------+ | | |+--------------------+ +----------| | | || | |Radio | | | || Are you sure ? | | ( ) ON (| | | || | | | | | || (OK) (Cancel) | | | | | || | | | | | |+--------------------+ | | | | |+------------------+ +----------| | : 左で(File)フォーカスで、ENTER +--------------------------------------+ |(File) [X] Show (move) (U) | |+-------+ (L)(D)(R) | ||(Open) |---------------------------+ | ||(Save) |--------------+ +----------| | ||(Close)| | |Radio | | ||(Quit) |you sure ? | | ( ) ON (| | |+-------+ | | | | | || (OK) (Cancel) | | | | | || | | | | | |+--------------------+ | | | | |+------------------+ +----------| | : よしよし (Open)フォーカスでENTER +--------------------------------------+ |(File) [X] Show (move) Open (U) | | (L)(D)(R) | | +----------------------------------+ | | |+--------------------+ +----------| | | || | |Radio | | | || Are you sure ? | | ( ) ON (| | | || | | | | | || (OK) (Cancel) | | | | | || | | | | | |+--------------------+ | | | | |+------------------+ +----------| | : パネルが消えて、オーバーラップしてた下の表示が戻り、 拝借した(move)右横のラベルに、メニューとして選択した アイテムの文字列が表示されます ENERでメニューのパネルを開いて、 他のアイテムを選択して動作を確認 Quitを選択すると、プログラム終了です +--------------------------------------+ |(File) [X] Show (move) Save (U) | |+-------+ (L)(D)(R) | ||(Open) |---------------------------+ | ||(Save) |--------------+ +----------| | ||(Close)| | |Radio | | ||(Quit) |you sure ? | | ( ) ON (| | |+-------+ | | | | | || (OK) (Cancel) | | | | | || | | | | | |+--------------------+ | | | | |+------------------+ +----------| | : result (null) $
最後のresult表示は、
想定してる cui_simple_dialog のボタンで
終了しなかったので不定でした。
初期値を設定して修正しておきます
とりあえずそれらしい振る舞いで、それなりに機能しそうですが、
表示がボタンそのものなので、まぎらわしいです。
やはり、専用の表示を持たせたいところ。
メニューボタン用の部品とメニューアイテム用の部品を用意してみます
$ make $ ./cui_test +--------------------------------------+ |File| [ ] Show (move) (U) | | (L)(D)(R) | | | : Fileメニューボタンにフォーカスがきてるわけではありません こういうデザインです (^^;) 左矢印でフォーカスがきます フォーカスの装飾は反転にしてみました +--------------------------------------+ |File| [ ] Show (move) (U) | | (L)(D)(R) | | | : ENTER +--------------------------------------+ |File| [ ] Show (move) (U) | |Open | (L)(D)(R) | |Save | | |Close| | |Quit | | | | : 操作を確認して QuitフォーカスでENTERで終了 result not yet $
さて、表示はこれでいいとして...
一旦メニューを開いたら、取り消しが出来ないので、必ずいづれかのアイテムのハンドラが呼び出される事になってます。
取り消しもできるようにしてみます
ここは、ESCキーで取り消し動作にしてみます
ESCキーを入力したときのコードを確認しておきます
$ make key_test gcc -Wall -c -o key_test.o key_test.c gcc -Wall -c -o key.o key.c gcc -Wall -o key_test key_test.o key.o $ ./key_test ESCキーを3回押してみて、qキーで終了 p ............1b ......1b .......1b .... $
ESCキーはそのまま 0x1b ですね。
key.h のマクロ CUI_KEY_ESC がそのまま使えそうです
メニューアイテムはイベント描画のハンドラだけ設定して、
キーイベントは継承元の祖先のボタンでの処理に頼ってました。
キーイベントの独自ハンドラを登録して、ESCキーに対応してみます。
ENTERキーに加えてESCキーでもボタンイベントのハンドラを呼ぶようにします。
ただしその時のイベントの値valは、ENTERなら1、ESCなら0を渡すようにします。
あとはアイテムのボタンイベントのハンドラの処理に委ねられるのですが...
お試しの cui_test.c のアイテムのハンドラは、menu.c に取り込んでしまって、そこで処理を追加してみます。
あと、アイテムを並べた入れものの生成処理も、menu.c 側に cui_menu_new() として追加しておきます。
そしてユーザは、この入れものである menu に、ボタンハンドラを登録しておいて、
アイテムがENTERで選択されたときは、入れものボタンハンドラが値valに、
選択されたアイテムのインデックスを渡して呼び出すようにします。
ESCで取り消されたときは、値valに、-1を渡して呼び出すようにします
$ make $ ./cui_test 左矢印、ENTER、ENTER +--------------------------------------+ |File| [ ] Show (move) Open (U) | | (L)(D)(R) | | | : 一旦ラベルにOpenの記録をつけて ENTER、下矢印 +--------------------------------------+ |File| [ ] Show (move) Open (U) | |Open | (L)(D)(R) | |Save | | |Close| | |Quit | | | | : ESCキー +--------------------------------------+ |File| [ ] Show (move) Open (U) | | (L)(D)(R) | | | OKです ラベルはOpenのままで、メニューの処理は取り消されてます ENTERでメニューを開いてQuitフォーカス ENTERで終了 result not yet $
これで無事ESCキーで取り消しできるようになりました
容易に予想されますが長いメニューを作ると、問題が出ます
画面に収まりきらないときに、下の方にあるアイテムが表示されず、 選択できません
$ make $ ./cui_test 左、右、ENTER +--------------------------------------+ |File| [X] Show (move) (U) | | (L)(D)(R) | | +----------------------------------+ | | |+--------------------+ +----------| | | || | |Radio | | | || Are you sure ? | | ( ) ON (| | | || | | | | | || (OK) (Cancel) | | | | | || | | | | | |+--------------------+ | | | | |+------------------+ +----------| | | || | | | | || How are you ? | | | | || | LONG| | | | || (^_^) (T_T) | | | | || | | | | |+------------------+ | | | +----------------------------------+ | +--------------------------------------+ LONGメニューボタンが右下に追加されてます 下、下、右、右 でフォーカスを合わせて ENTER 下、下、下... +--------------------------------------+ |File| [X] Show (move) (U) | | (L)(D)(R) | | +----------------------------------+ | | |+--------------------+ +----------| | | || | |Radio | | | || Are you sure ? | | ( ) ON (| | | || | | | | | || (OK) (Cancel) | | | | | || | | | | | |+--------------------+ | | | | |+------------------+ +----------| | | || | | | | || How are you ? | | | | || | LONG| | | | || (^_^) (T_T) | cmd 0 | | | | || | cmd 1 | | | | |+------------------+ cmd 2 | | | | +----------------------------------+ | +--------------------------------------+ 下にメニューアイテムが続いてるはずですが、 予想通り選択できません ESC、上で(Cancel)フォーカス ENTERで終了 result Cancel $
アイテムのハンドラでENERキーとキャンセルキーの他に、 上下キーも判定して、次にフォーカスが移るであろうアイテムが存在して、 見えてなければ、自身の位置をスライドさせるようにしてみます
$ make $ ./cui_test 左、右、ENTER 下、下、右、右、ENTER 下、下、下... +--------------------------------------+ |File| [X] Show (move) (U) | | (L)(D)(R) | | +----------------------------------+ | | |+--------------------+ cmd 6 |----| | | || | cmd 7 | | | | || Are you sure ? | cmd 8 |ON (| | | || | cmd 9 | | | | || (OK) (Cancel) | cmd 10| | | | || | cmd 11| | | | |+--------------------+ cmd 12| | | | |+------------------+ cmd 13|----| | | || | cmd 14| | | | || How are you ? | cmd 15| | | | || | cmd 16| | | | || (^_^) (T_T) | cmd 17| | | | || | cmd 18| | | | |+------------------+ cmd 19| | | | +----------------------------------+ | +--------------------------------------+ モリモリとせり上がってきました これで末尾の'cmd 19'も選択できます 上、上、上... +--------------------------------------+ |File| [X] Show (move) (U) | | (L)(D)(R) | | +----------------------------------+ | | |+--------------------+ cmd 0 |----| | | || | cmd 1 | | | | || Are you sure ? | cmd 2 |ON (| | | || | cmd 3 | | | | || (OK) (Cancel) | cmd 4 | | | | || | cmd 5 | | | | |+--------------------+ cmd 6 | | | | |+------------------+ cmd 7 |----| | | || | cmd 8 | | | | || How are you ? | cmd 9 | | | | || | cmd 10| | | | || (^_^) (T_T) | cmd 11| | | | || | cmd 12| | | | |+------------------+ cmd 13| | | | +----------------------------------+ | +--------------------------------------+ ちゃんと先頭のアイテムまで戻れました ENTER、上、ENTERで終了 result Cancel $
続いて、選択したアイテムを覚えているポップアップメニューを考えてみます
メニューを開く前のボタンは、メニューボタンや、メニューアイテムと同じ見た目でよいですが、
メニューアイテムを選択すると、ラベルを選択したアイテムの文字列に更新します。
そのたびにサイズの幅wが変わるのもなんなので、初期値で指定した幅で固定する仕様にします。
メニューが開くときは、開く前のボタン位置に、丁度選択アイテムがくるよう開きます。
メニューはどうせモーダルで開くので、出来るだけ全部表示できた方がいいはずです。
なのでメニューは、一番根元の祖先の入れものに配置するようにしてみます
$ make $ ./cui_test 左、右、ENTER 追加部分が見えにくいので 右 x 3 で (R)にフォーカス ENTER x 8 +--------------------------------------+ |File| [X] Show (move) (U) | | (L)(D)(R) | | +----------------------------------+ | | |-------------+ +------------------| | | | | |Radio | | | |ou sure ? | | ( ) ON (O) OFF | | | | | | | | | | (Cancel) | | | | | | | | | | | |-------------+ | | | | |-----------+ +------------------| | | | | | | | |re you ? | | | | | | LONG| | | | |) (T_T) | | | | | | January| last 1/31| | | |-----------+ | | | +----------------------------------+ | +--------------------------------------+ 下 x 3 で追加したポップアップメニューにフォーカス : | |re you ? | | | | | | LONG| | | | |) (T_T) | | | | | | January| last 1/31| | | |-----------+ | | | +----------------------------------+ | +--------------------------------------+ ENTER : | |re you ? | | | | | | LONG| | | | |) (T_T) | | | | | | January |last 1/31| | | |-----------+ February | | | | +---------------March |---------+ | +-----------------April |-----------+ よーし 祖先のルートの入れものいっぱいまで、表示でてます 下矢印連打でスライドの動作も確かめて... : | | | | | | | | (Cancel) | January | | | | | | February | | | | |-------------+ March | | | | |-----------+ April |---------| | | | | May | | | | |re you ? | June | | | | | | July | | | | |) (T_T) | August | | | | | | September|last 1/31| | | |-----------+ October | | | | +---------------November |---------+ | +-----------------December |-----------+ ENTER : | |re you ? | | | | | | LONG| | | | |) (T_T) | | | | | | Decembe|| last 12/3| | | |-----------+ | | | +----------------------------------+ | +--------------------------------------+ おっと、表示が乱れてしまってます 上矢印 x 3、ENTER、ENTER で表示しなおしてみると : | |re you ? | | | | | | LONG| | | | |) (T_T) | | | | | | Decembe| last 12/3| | | |-----------+ | | | +----------------------------------+ | +--------------------------------------+ 追加したポップアップメニューのサイズは8固定なので、 これが正しい表示です 下、ENTER でCancel終了 result Cancel $
どうも、メニューを開いたときの表示が残ってる様子です。
メニューの表示はモーダルとしてcui_main()を呼び出してますが、
終了して領域をクリアしたあと、ポップアップメニューを
描画しなおすところでしくじってるようです。
cui_menu_item_str_set() の中の処理で、一時的にサイズwが拡大されて、元に戻すときに、
このwが広がった一瞬の隙をついて、ラベルが描画してしまって、後にwが戻ってました。
修正しておきます
これでアイテムを選択した後の表示も、意図した通りになりました。
メニューはプルダウンのときと同じ部品を使ってるので、
ESCキーによる取り消しもそのままちゃんと機能してます
ここまでのソースファイル cui52.tgz
1行のテキストを編集できる部品を作ってみます
難易度が高そうです。
次のような外観と機能をめざしてみます
[foo ] 幅wは初期化で指定して固定 [foo ] フォーカスくると下線 [foo ] ENTERキーで編集モードに入ってカーソル表示 [foo ] 左右の矢印キーでカーソル移動 [f123oo ] 矢印やENTER以外のキー入力(例えば'1', '2', '3')で、テキスト挿入 [f12oo ] BSキーでカーソル手前の文字を削除 [f1234oo] 編集モードではテキストの長さとカーソル位置を考慮してスクロール [f12345o] 編集モードではテキストの長さとカーソル位置を考慮してスクロール [123456o] 編集モードではテキストの長さとカーソル位置を考慮してスクロール [f123456] ENTERキーで更新して編集モードを終る(見えない部分はあきらめ) [foo ] 編集モードでESCキーを入力したら編集前の状態に戻して編集モードを終る
$ make $ ./cui_test +--------------------------------------+ |File| [ ] Show (move) (U) | |[foo ] (L)(D)(R) | | | : 下矢印 +--------------------------------------+ |File| [ ] Show (move) (U) | |[foo ] (L)(D)(R) | | | : フォーカスの表示だけで、まだ何もできません 上、ENTER、下、下、下 Quitフォーカス ENTERで終了 resut not yet $
それではENTERキーで編集モードに入るようにしてみます
構造体に編集モードのフラグ変数を追加しておきます
cui_main()を呼び出しての、モーダル動作にしてでもいいですが、 編集モード中は、ハンドラでのキーイベントの処理で、 TRUEを返すようにするだけでも、フォーカスの移動を阻止できます
$ make $ ./cui_test 下、ENTER で編集モードに入って +--------------------------------------+ |File| [ ] Show (move) (U) | |[foo ] (L)(D)(R) | | | : 右、0123456 +--------------------------------------+ |File| [ ] Show (move) (U) | |[23456o] (L)(D)(R) | | | : ENTER で編集モードを抜けると +--------------------------------------+ |File| [ ] Show (move) 'f0123456oo' | |[f01234] (L)(D)(R) | | | : 上、ENTER、下、下、下 Quitフォーカス ENTER で終了 resutl not yet $
なんとか動作するものの、かなり「えいやぁ」感がある実装になってしまった。
そのうちブラッシュアップしよう...
ということで、ここでまた一旦、全体をブラッシュアップします
まず、気になってたのが、cui_wh_fit()
$ grep cui_wh_fit *.c cui.c:cui_wh_fit(cui obj, cui child) cui.c: cui_wh_fit(obj, child->next); cui_test.c: cui_wh_fit(grp, grp->children); cui_test.c: cui_wh_fit(grp2, grp2->children); cui_test.c: cui_wh_fit(base, base->children); cui_test.c: cui_wh_fit(panel, panel->children); cui_test.c: cui_wh_fit(base, base->children); etext.c: cui_wh_fit(obj, obj->children); etext.c: cui_wh_fit(p->inner, p->inner->children); etext.c: cui_wh_fit(p->inner, p->inner->children); etext.c: cui_wh_fit(p->inner, p->inner->children); etext.c: cui_wh_fit(p->inner, p->inner->children); menu.c: cui_wh_fit(obj, obj->children); menu.c: cui_wh_fit(obj, obj->children); $
結構使ってます。
2つめの引数で毎回 obj->children を指定してますが、
これは内部の実装を再帰にしてる都合です。
内部用の chi_wh_fit_chain() に分離して、
cui_wh_fit() 自身は引数1つに修正します
同様に cui_handler_call()
$ grep cui_handler_call *.c button.c: cui_handler_call(obj, obj->handler_list, CUI_EVT_DRAW, CUI_DRAW_ACTIVE); button.c: cui_handler_call(obj, obj->handler_list, CUI_EVT_BUTTON, 0); ckbox.c: cui_handler_call(obj, obj->handler_list, CUI_EVT_BUTTON, p->v); cui.c: cui_handler_call(obj, obj->handler_list, CUI_EVT_DRAW, val); cui.c: if(cui_handler_call(focus, focus->handler_list, CUI_EVT_KEY, key)) continue; etext.c: cui_handler_call(obj, obj->handler_list, CUI_EVT_BUTTON, 0); handler.c:cui_handler_call(cui obj, cui_handler_list list, int evt, int val) handler.c: cui_handler_call(obj, list->next, evt, val); menu.c: cui_handler_call(obj->parent, obj->parent->handler_list, CUI_EVT_BUTTON, val); menu.c: cui_handler_call(obj, obj->handler_list, CUI_EVT_BUTTON, val == CUI_KEY_ENTER); menu.c: cui_handler_call(obj, obj->handler_list, CUI_EVT_BUTTON, p->val); radio.c: cui_handler_call(obj, obj->handler_list, CUI_EVT_BUTTON, ck->v); $
これも2つめの引数で obj->handler_list の指定ばかり。
同様に '_chain' つきの関数に分離しておきます
cui_y_set(), cui_h_set() も追加して
$ grep -e 'cui_xy.*set' -e 'cui_wh.*set' *.c cui.c:cui_xywh_set(cui obj, int x, int y, int w, int h) cui.c: cui_xywh_set(obj, x, obj->y, obj->w, obj->h); cui.c: cui_xywh_set(obj, obj->x, y, obj->w, obj->h); cui.c:cui_xy_set(cui obj, int x, int y) cui.c: cui_xywh_set(obj, x, y, obj->w, obj->h); cui.c: cui_xywh_set(obj, obj->x, obj->y, w, obj->h); cui.c: cui_xywh_set(obj, obj->x, obj->y, obj->w, h); cui.c:cui_wh_set(cui obj, int w, int h) cui.c: cui_xywh_set(obj, obj->x, obj->y, w, h); cui_test.c: cui_xywh_set(obj, obj->x, obj->y + 1, obj->w, obj->h); cui_test.c: cui_xywh_set(big, x, y, big->w, big->h); menu.c: cui_xy_set(obj->parent, obj->parent->x, obj->parent->y - dir); panel.c: cui_xywh_set(p->fill[0], 1, 0, obj->w-2, 1); panel.c: cui_xywh_set(p->fill[1], 1, obj->h-1, obj->w-2, 1); panel.c: cui_xywh_set(p->fill[2], 0, 1, 1, obj->h-2); panel.c: cui_xywh_set(p->fill[3], obj->w-1, 1, 1, obj->h-2); panel.c: cui_xywh_set(p->fill[4], 0, 0, 1, 1); panel.c: cui_xywh_set(p->fill[5], obj->w-1, 0, 1, 1); panel.c: cui_xywh_set(p->fill[6], obj->w-1, obj->h-1, 1, 1); panel.c: cui_xywh_set(p->fill[7], 0, obj->h-1, 1, 1); $
せっかく追加したので、できるだけ専用の関数に書換えておきます
あと、よくあるケースとして、部品が見えてるかどうか判定して、
見えてたら cui_hide() で消しておいて、何か処理。
終ったら、元の状態にcui_show()で戻すパターンもちょいちょいあります
int visi = cui_is_visible(obj); if(visi) cui_hide(obj); : <何か処理して> : if(visi) cui_show(obj); のパターン
$ grep 'visi =' *.c cui.c: int visi = cui_is_visible(obj); label.c: int visi = cui_is_visible(obj); panel.c: int visi = cui_is_visible(obj); $
返って解りにくくならない事を願いつつ
int bak = cui_hide_enter(obj); : cui_hide_exit(obj, bak);
などと用意して、置き換えてみます
さらに..
cui_main() で稼働する前の段階では、
cui_hide(), cui_show() すると、クリアと描画が入るので、
なるべく flags の CUI_FLG_HIDE ビットで操作してました。
cui_show(), cui_hide() だけに統一したいところです。
cui_show(), cui_hide() の中で、稼働中かどうか判定できればよさそうですが、
さてどうすべきか?
出力側の問題なので、esc.c に設けてる cui_esc_cnt のカウンタ。
これはネストして cui_esc_enter() を呼び出した回数のはずなので、
この値を参照して 0 ならば、フラグ操作だけにしてみます
$ grep FLG_HIDE *.c cui.c: obj->flags |= CUI_FLG_HIDE; cui.c: obj->flags &= ~CUI_FLG_HIDE; cui.c: return !(obj->flags & CUI_FLG_HIDE) && cui_is_visible(obj->parent); cui_test.c: base->flags |= CUI_FLG_HIDE; cui_test.c: menu->flags |= CUI_FLG_HIDE; cui_test.c: small->flags |= CUI_FLG_HIDE; etext.c: p->lb_cursor->flags |= CUI_FLG_HIDE; menu.c: btn->lb1->flags |= CUI_FLG_HIDE; menu.c: obj->flags |= CUI_FLG_HIDE; menu.c: obj->flags &= ~CUI_FLG_HIDE; menu.c: obj->flags |= CUI_FLG_HIDE; menu.c: obj->flags &= ~CUI_FLG_HIDE; menu.c: obj->flags |= CUI_FLG_HIDE; menu.c: obj->flags &= ~CUI_FLG_HIDE; menu.c: p->menu->flags |= CUI_FLG_HIDE; radio.c: obj->flags |= CUI_FLG_HIDE; radio.c: obj->flags &= ~CUI_FLG_HIDE; $
けっこうあるな...
メニューやエディットテキストを作ってみて気付いた事として、
ラベルに続けてfillをつなげておいて、fillの空白部分も同じ装飾の設定で使うケースがよくありました。
固定長のラベルで、末尾はfillの空白で埋めてくれる専用の部品があれば、便利かもしれません。
既存のラベルとfillを入れた入れものを lblfix として追加してみます
エディットテキストのカーソル周辺の実装が、とりあえず動く程度で、
あまりにも解りにくいです。
少し考えて、整えてみます
今の構造はこんな感じ
lb_cursor - lbf ------------------ inner -------------------- view ----------- base ------------------------
base の入れものに '[', 'view という入れもの', ']' が載っかって、
固定幅の view の中に、inner という大きな入れものが入ってます。
inner の位置をずらして、スクロールさせてます。
inner という大きな入れものには、文字を表示するための固定幅のラベル lbf が入ってて、
その lbf の上に、カーソル用のラベルが載っかってます
まず lbf はラベルと違って縮まないので、innerの代わりにそのままスクロール用に使えます。
逆にinnerは用済みです
lb_cursor に独自の2バイトのバッファをもたせていて、 カーソル移動のたびに、カレントの位置の文字をいちいちコピーしてきて更新してます。 ここが、なんともドロドロしてるので、なんとかしてみましょう
lbf_shadow ------------------ (lbfと同じバッファを共用) view_cursor - lbf ------------------ view ----------- base ------------------------
こんなアイデアでどうでしょう
view までは同じで、inner廃止ですぐlbfの文字列がきます。
その上にカーソル用の1文字の大きさの入れものを載せます。
その小さな入れものの中に、lbf と同じ文字列の固定幅ラベルを、
入れてしまい、描画の装飾の指定は反転表示にしておきます
lbfとview_cursorは、カーソル位置分だけずらして配置し、
view_cursorとlbf_shadow は、逆方向にに同じ分だけずらします。
結果、lbfとlbf_shadowは、見た目同じ位置で、重なるようにします。
2つの固定幅ラベルの間を、1文字の大きさの入れものがカーソルとして移動して、
上にのってる反転のラベルは、クリッピングされて、1文字だけが見えてる状態になります。
viewで見えてる他の範囲は、lbfの通常の文字列の描画が見えてます
これまた、かえって複雑に思われない事をいのりつつ、この案で実装してみます
$ make $ ./cui_test +--------------------------------------+ |File| [ ] Show (move) (U) | |[fo123o] (L)(D)(R) | | | :
動作は特に問題なさそうです
エディットテキストの機能を改善してみます
操作してみると、ENTERで編集モードに入った後、少々使い勝手が悪い気がします。
例えば既に文字列がある状態で、全部書換えたいケースでは、末尾にカーソル移動して、BSキーで全部削除せねばなりません。
末尾に少し追加したい場合も、まず末尾までカーソル移動になります
ENTERキーで編集モードに入る前に、もし文字列があった場合は、1クッション入れて、全選択の状態のモードを追加してみます。
選択モードからENTERキーで従来のように編集モードへ移ります。
選択モードからBSキーを押すと、文字列をクリアしてから編集モードへ移るようにします。
選択モードから右矢印キーを押すと、編集モードへ移りますが、カーソル位置を文字列の末尾に移動した状態にします。
左矢印キーはENTERキーと同じ扱いにして、従来通りカーソル位置は先頭でいいでしょう。
ESCキーで通常のモードに戻るようにします
edit_mode というフラグ変数のメンバで状態を持ってましたが、 名前は mode にして、値 のマクロを追加しておきます
エディットテキストの状態遷移 右、左矢印 BS ENTER ENTER 通常モード -------> 全選択モード --------> 編集モード <------- | ^ ESC | | | +------------------------------------------+ ENTER、ESC
$ make $ ./cui_test 下、ENTER で一旦全選択モードに入って +--------------------------------------+ |File| [ ] Show (move) (U) | |[foo ] (L)(D)(R) | | | : ESCキーで通常モードへ +--------------------------------------+ |File| [ ] Show (move) (U) | |[foo ] (L)(D)(R) | | | : ENTER、ENTERで編集モードへ +--------------------------------------+ |File| [ ] Show (move) (U) | |[foo ] (L)(D)(R) | | | : ENTER、ENTERで全選択に戻して 右矢印 +--------------------------------------+ |File| [ ] Show (move) (U) | |[foo ] (L)(D)(R) | | | : OK 'b' 'a' 'r' ENTER で更新 +--------------------------------------+ |File| [ ] Show (move) 'foobar' | |[foobar] (L)(D)(R) | | | : ENTERで全選択して BSキー +--------------------------------------+ |File| [ ] Show (move) 'foobar' | |[ ] (L)(D)(R) | | | : OK ENTERで空に設定しておいて +--------------------------------------+ |File| [ ] Show (move) '' | |[ ] (L)(D)(R) | | | : ENTERで、空のときは全選択には入らずに、編集モードへ +--------------------------------------+ |File| [ ] Show (move) 'foobar' | |[ ] (L)(D)(R) | | | : OK ENTER、上、ENTER 下 x 3、ENTER で終了 result not yet $
OKです
エディットテキストにさらなる機能追加をしてみます
過去に入力した文字列をヒストリバッファに記録しておいて、 プルダウンメニューで選択出来るようにしてみます
全選択モードか編集モードで、下矢印キーを押すと動作するようにします。
ヒストリバッファは20個くらいにして、古いものは消えていく事にしておきます
ポップアップメニューのメニュー部分の動作だけ使ってみようとしましたが、
ESCキー終了で取り消したときの判定が、少々まずい事になってました。
合わせて修正してみます
下、ENTER、下 font +--------------------------------------+ |File| [ ] Show (move) (U) | |[foo ] (L)(D)(R) | | foo| | | | : とりあえず ESCキーで閉じて 全選択から BSキー、'b' 'a' 'r'、ENTER さらにENTER、下 +--------------------------------------+ |File| [ ] Show (move) 'bar'(U) | |[bar ] (L)(D)(R) | | bar| | | foo| | | | : 下、ENTER で fooを選択 +--------------------------------------+ |File| [ ] Show (move) 'bar'(U) | |[foo ] (L)(D)(R) | | foo| | | | : OK 右、'-' 'h' 'o' 'g' 'e'、ENTER さらにENTER、下 +--------------------------------------+ |File| [ ] Show (move) 'foo-(U)e' | |[foo-ho] (L)(D)(R) | | foo-hoge| | | foo | | | bar | | | | : OK ESC、ESC 上、ENTER 下 x 3、ENTER で終了 result not yet $
ついでにもういっちょう
編集モードで、カーソル位置の文字が '0'〜'9' のどれかならば、 上矢印で、'0'〜'9' のポップアップメニューを出して、 選択して変更出来るようにしておきます
$ make $ ./cui_test 下、ENTER、右、'1' '2' '3'、左、左 +--------------------------------------+ |File| [ ] Show (move) (U) | |[oo123 ] (L)(D)(R) | | | : そして 上 +----0|--------------------------------+ |File1| [ ] Show (move) (U) | |[oo12| ] (L)(D)(R) | | 3| | | 4| | | 5| | | 6| | | 7| | | 8| | | 9| | | | : 下 x 3 で '5' を選んで、ENTER +--------------------------------------+ |File| [ ] Show (move) (U) | |[oo153 ] (L)(D)(R) | | | : OK ENTERで確定すると +--------------------------------------+ |File| [ ] Show (move) 'foo153' | |[foo153] (L)(D)(R) | | | : よしよし 上、ENTER、下 x 3、ENTER で終了
画面のシートを切替えるタブボタンを作ってみます
名称はタブメニュー? タブコントロール?
画面の上の方にあって、クリックすると表示してるシートを切替える部品です
------- ------- -------- / foo |/ bar |/ hoge | -------------------------- (foo のシートが表示されていて) : barのタブをクリックすると ------- ------- -------- / foo |/ bar |/ hoge | -------- ------------------ (bar のシートに切り替わる) : こんなやつです
タブボタンそのものは項目が全部見えていて、 1つだけ選択された状態になっているので、 ラジオボタンの振舞いに似てます
シートの部分は、同じ親の入れものに、 複数の入れものを重ねて配置しておいて、 選択された1つだけを表示すればよさそうです
お試しとして cui_test.c で、ラジオボタンで組み込んでみます
$ make $ ./cui_test +----------------------------------------+ |(O) foo ( ) bar ( ) hoge | |+--------------------------------------+| ||File| [ ] Show (move) (U) || ||[foo ] (L)(D)(R) || || || : 左、右、右、上 (むー...) barフォーカスで ENTER +----------------------------------------+ |( ) foo (O) bar ( ) hoge | | | | | | bar sheet | | | : 右、ENTER +----------------------------------------+ |( ) foo ( ) bar (O) hoge | | | | | | hoge sheet | | | : 左、左、ENTER +----------------------------------------+ |(O) foo ( ) bar ( ) hoge | |+--------------------------------------+| ||File| [ ] Show (move) (U) || ||[foo ] (L)(D)(R) || || || : OK 下、ENTER、下 x 3、ENTER で終了 result not yet $
うまくいきました
ではラジオボタンから継承して作ることにして、表示をそれらしくしてみます
コーディングしてみると、継承元のラジオボタンのハンドラの処理が、とても悩ましい状態でした。
radio.c cui_radio_hdr() から cui_radio_toggle() を呼び出してるのですが、
この中で外観の変更のため、ラベルの文字列をその場で変更してます。
完全にラジオボタン専用になってるので、この関数自体を継承して書き換えたいのですが、
C++でもJavaでもないので、仕組みがありません。
かといって、トグル動作用のイベントを追加するのも違う気がします。
むむむと悩んで、考えが至りました。
外観の表示を変えるなら既存の描画イベントを使ってハンドラで処理させるべきであって、
その描画のハンドラについて、タブ用のものを追加登録して継承すればいいのでは!?
普通のボタンの描画のハンドラのときは、valで通常、フォーカス、アクティブの3種類を考えてました。
チェックボックスでは、通常とフォーカスだけ処理してて、アクティブな状態への外観の更新は、
キーイベントのハンドラで処理してました。
ここからして本来は、キーイベントのハンドラから、描画イベントのハンドラを呼び出して、処理するべきでした。
ラベルとボタンでは、アクティブな状態の描画が意味がなかったですが、
チェックボックスやラジオボタンでは、アクティブ指定の意味があります
そして、フォーカスとアクティブの指定は独立です。
cui.h では
#define CUI_DRAW_NORMAL 0 #define CUI_DRAW_FOCUS 1 #define CUI_DRAW_ACTIVE 2
たまたまビット指定可能な値になってますが、独立で指定するとして、...
ここまで考えて、チェックボックスのソースを見て、暗雲がたちこめました。
描画イベントのフォーカスの指定は、システム側のメインループからの要求で、
そのときに、アクティブかどうかは、ベースの型からだけでは判定できません。
また、システムから指定すべきものでは無い気がします。
結局アクティブの指定は使わずに、描画ハンドラの中で、個別にアクティブかどうかは判定すべきなのか...
うーむ。とりあえずその方法で試してみましょう
という事で、チェックボックスとラジオボタンのの実装から修正を入れていって、 タブボタンのコーディングをすすめてみます
途中えらくはまってしまいましたが、なんとかなりました
タブボタンの初期化途中での cui_x_set() から cui_hide() cui_show() での cui_draw()
ここからの描画ハンドラ呼び出しは、ラジオボタンのハンドラまでしか登録されてなくて、
まだタブボタンのハンドラ登録が済んでない段階です。
ここで、ラジオボタンの描画ハンドラから、ラベルが付け変わってました
あと cui_wh_fit() が拡大方向にしか動作しない事をすっかり忘れてて、 タブボタンの配置が広がってしまう現象になやまされてました
+----------------------------------------+ |/foo|/bar|/hoge| | |+--------------------------------------+| ||File| [ ] Show (move) (U) || ||[foo ]heet (L)(D)(R) || || || : 左、右 x 3 で barフォーカス ENTER +----------------------------------------+ |/foo|/bar|/hoge| | | | | | | bar sheet | | | : 左、ENTER +----------------------------------------+ |/foo|/bar|/hoge| | |+--------------------------------------+| ||File| [ ] Show (move) (U) || ||[foo ] (L)(D)(R) || || || OK 下、ENTER、下 x 3、ENTER で終了 result not yet $
以前にスクロールの機能をためして、上下左右ボタンを配置したままになっていました。
ちゃんと専用のスクロールバーを用意してみます
- | | | = = = | | - |-----======--------|
こんな外観にするとして、
縦長の場合、上下のボタンが'-'の部分と、
上下のページボタンが'|'の部分。
真ん中の'='の部分がthumb部とします
フォーカスがくると基本的に下線の表示にして、
thumbはENTERでモーダル動作に入り、反転表示にします。
矢印キーでthumbを移動させ、ENTERで決定、ESCで取り消しにして、
モーダル動作から抜けるようにします
値は0から指定した上限の値までの整数で変化させる事にします
値の変化と共に、ページボタン部とthumb部の大きさと位置を変化させねばなりません。
ページボタン部は、fillの表示ですが、フォーカス可能で、キーイベントを受ける必要があります。
なのでまず、fillの機能とボタンの機能を合わせもつ部品として、fillbtnを作っておきます
$ make $ ./cui_test 左、右 x 3 で barフォーカス ENTER +----------------------------------------+ |/foo|/bar|/hoge| | | | | bar | | ++++ | | ++++ | | ++++ | | ++++ | | | : 下 +----------------------------------------+ |/foo|/bar|/hoge| | | | | bar | | ++++ | | ++++ | | ++++ | | ++++ | | | : ENTER x 3 +----------------------------------------+ |/foo|/bar|/hoge| | | | | 2 | | ++++ | | ++++ | | ++++ | | ++++ | | | : OK 上 で foo フォーカス ENTER、下、ENTER、下 x 3、ENTER で終了 result not yet $
ではこの fillbtn を使ってスクロールバーを作ってみます
試してみて、はまりました。
fillbtn のサイズを変更しても、なぜか表示に反映されない現象に。
それもそのはず、fillbtn は入れものとして作っていて、中に fill を載せてるだけの部品でした。
サイズを変更したら、中の fill もそのサイズに追随させる必要がありました
力尽きたので、とりえあず thumb部分 の実装はあとまわし。
横長の場合だけ試して、縦長の場合もあとまわし
$ make $ ./cui_test 左、右 x 3、ENTER +----------------------------------------+ |/foo|/bar|/hoge| | | | | bar | | |--------=---------| | | |========----------| | | | : 下、左、ENTER +----------------------------------------+ |/foo|/bar|/hoge| | | | | 14 | | |-------=----------| | | |========----------| | | | : ENTER x 14 +----------------------------------------+ |/foo|/bar|/hoge| | | | | 0 | | |=-----------------| | | |========----------| | | | : 右 x 3、ENTER +----------------------------------------+ |/foo|/bar|/hoge| | | | | 20 | | |-----------=------| | | |========----------| | | | : 下、右、ENTER x 10 +----------------------------------------+ |/foo|/bar|/hoge| | | | | 10 | | |-----------=------| | | |----------========| | | | : OK 上、上、左、左、fooフォーカスでENTER 下、ENTER、下 x 3、ENTER で終了 result not yet $
fillbtn のサイズ変更処理ではまりましたが、
よくよく考えると、スクロールバーも入れもの、fillbtnも入れもの。
なのでスクロールバーも同じような事をしてるのに、
fillbtn だけサイズ追随用の処理をするのは変でした。
スクロールバーと同様、fillbtn も描画ハンドラの中で、
サイズに追随させればいいはずなので、修正しておきます
その点で気付くのは、パネルの枠のサイズ、位置の追随の箇所です。
$ cat panel.c : void cui_panel_adj(cui obj) { cui_panel p = (cui_panel)obj; int bak = cui_hide_enter(obj); cui_xywh_set(p->fill[0], 1, 0, obj->w-2, 1); cui_xywh_set(p->fill[1], 1, obj->h-1, obj->w-2, 1); cui_xywh_set(p->fill[2], 0, 1, 1, obj->h-2); cui_xywh_set(p->fill[3], obj->w-1, 1, 1, obj->h-2); cui_xywh_set(p->fill[4], 0, 0, 1, 1); cui_xywh_set(p->fill[5], obj->w-1, 0, 1, 1); cui_xywh_set(p->fill[6], obj->w-1, obj->h-1, 1, 1); cui_xywh_set(p->fill[7], 0, obj->h-1, 1, 1); cui_hide_exit(obj, bak); } $ grep cui_panel_adj *.c cui_test.c: cui_panel_adj(panel); panel.c: cui_panel_adj(obj); panel.c:cui_panel_adj(cui obj) $
ここも描画ハンドラで処理させるように修正してみます
そして、縦長の場合を追加します
つづいてスクロールバーのthumb部を作ってみます
thumbがモーダルに入って処理中であるフラグ変数と、ESCキーでの取り消しに備えて、値のバックアップを追加します。
キーイベントハンドラを追加で登録し、fillbtnのハンドラを上書きして処理を組み込んでみます
とりあえず整数だけで、なんとか位置決めをしてみましたが、小数を使った方がよかったかもしれません
$ make $ ./cui_test 左、右 x 3、ENTER 下、右、ENTER +----------------------------------------+ |/foo|/bar|/hoge| | | | | bar | | |--------=---------| - | | |========----------| | | | | | : 右 x 5 +----------------------------------------+ |/foo|/bar|/hoge| | | | | 23 | | |-------------=----| - | | |========----------| | | | | | : 左 x 10 +----------------------------------------+ |/foo|/bar|/hoge| | | | | 2 | | |-=----------------| - | | |========----------| | | | | | : 終了は ENTER、上、ENTER、下、ENTER、下 x 3、ENTER result not yet $
では、スクロールバーを使って、スクロールバーつきのパネルを用意してみます
パネルの枠の領域にスクロールバーを配置し、非表示にして隠しておきます。
パネルの枠の内側に、ビューエリアとして入れものを載せておきます。
さらにビューエリアの上に表示したいシートを載せます。
シートのサイズがビューエリア内に収まるなら、スクロールバーは隠したままにして、
収まらなければ、表示するようにします
スクロールバーの値や最大値の設定/参照する専用の関数を追加しておきます
cui_test.c では、矢印ボタンを廃止して、 パネルをスクロールパネルに置き換えてみます
$ make $ ./cui_test 右、左、下 (かなりコツを必要としますな...) Showフォーカスで ENTER +----------------------------------------+ |/foo|/bar|/hoge| | |+--------------------------------------+| ||File| [X] Show (move) || ||[foo ] || || +----------------------------------+ || || |+--------------------+ +----------- || || || | |Radio = || || || Are you sure ? | | ( ) ON (= || || || | | = || || || (OK) (Cancel) | | = || || || | | = || || |+--------------------+ | = || || |+------------------+ +----------= || || || | | || || || How are you ? | | || || || | LONG| | || || || (^_^) (T_T) | | || || || | January| l| || || |+------------------+ - || || +|======================----------|+ || |+--------------------------------------+| +----------------------------------------+ よしよし 右 x 3、下 x 2、thumb フォーカスで ENTER +----------------------------------------+ |/foo|/bar|/hoge| | |+--------------------------------------+| ||File| [X] Show (move) || ||[foo ] || || +----------------------------------+ || || |+--------------------+ +----------- || || || | |Radio = || || || Are you sure ? | | ( ) ON (= || || || | | = || || || (OK) (Cancel) | | = || || || | | = || || |+--------------------+ | = || || |+------------------+ +----------= || || || | | || || || How are you ? | | || || || | LONG| | || || || (^_^) (T_T) | | || || || | January| l| || || |+------------------+ - || || +|======================----------|+ || |+--------------------------------------+| +----------------------------------------+ 下 x 5、ENTER +----------------------------------------+ |/foo|/bar|/hoge| | |+--------------------------------------+| ||File| [X] Show (move) || ||[foo ] || || +----------------------------------+ || || || | | - || || |+--------------------+ | | || || |+------------------+ +----------| || || || | | || || || How are you ? | | || || || | LONG| | || || || (^_^) (T_T) | = || || || | January| l= || || |+------------------+ = || || | = || || | = || || | = || || | = || || |bottom ... bottom ... bottom ... b- || || +|======================----------|+ || |+--------------------------------------+| +----------------------------------------+ 下、左 x 2、ENTER +----------------------------------------+ |/foo|/bar|/hoge| | |+--------------------------------------+| ||File| [X] Show (move) || ||[foo ] || || +----------------------------------+ || || || | | - || || |+--------------------+ | | || || |+------------------+ +----------| || || || | | || || || How are you ? | | || || || | LONG| | || || || (^_^) (T_T) | = || || || | January| l= || || |+------------------+ = || || | = || || | = || || | = || || | = || || |bottom ... bottom ... bottom ... b- || || +|======================----------|+ || |+--------------------------------------+| +----------------------------------------+ 右 x 10、ENTER +----------------------------------------+ |/foo|/bar|/hoge| | |+--------------------------------------+| ||File| [X] Show (move) || ||[foo ] || || +----------------------------------+ || || | | | |- || || |-----------+ | || || || |---------+ +-------------------+| || || | | | || || | you ? | | || || | | LONG| | || || | (T_T) | = || || | | January| last 1/31 = || || |---------+ = || || | = || || | = || || | = || || | = || || | bottom ... bottom ... bottom ... - || || +|----------======================|+ || |+--------------------------------------+| +----------------------------------------+ OK 上 x 5、左 x 4、下 で Fileフォーカス ENTER、下 x 3、ENTER で終了 result not yet $
こうなってくると、もう少しフォーカスを自然に移動させたいです
現状の仕様では
フォーカスがあれば
+------+------------------------------------+ |上矢印|上にある一番近い部品にフォーカス移動| +------+------------------------------------+ |下矢印|下にある一番近い部品にフォーカス移動| +------+------------------------------------+ |左矢印|左にある一番近い部品にフォーカス移動| +------+------------------------------------+ |右矢印|右にある一番近い部品にフォーカス移動| +------+------------------------------------+ +----------------------------+-------------------------------------------------+ |「上にある一番近い部品」とは|グローバルなy座標の値が自分より小さな値であり、 | | |部品の中心からの距離が最小で、自分以外の部品 | +----------------------------+-------------------------------------------------+ |「下にある一番近い部品」とは|グローバルなy座標+hの値が自分より大きな値であり、| | |部品の中心からの距離が最小で、自分以外の部品 | +----------------------------+-------------------------------------------------+ |「左にある一番近い部品」とは|グローバルなx座標の値が自分より小さな値であり、 | | |部品の中心からの距離が最小で、自分以外の部品 | +----------------------------+-------------------------------------------------+ |「右にある一番近い部品」とは|グローバルなx座標+wの値が自分より大きな値であり、| | |部品の中心からの距離が最小で、自分以外の部品 | +----------------------------+-------------------------------------------------+
そして距離はy方向はx方向に比べて、2倍長くなるように取り扱ってます
一旦ややこしいのをやめて、部品の中心の座標で考えてみます。
そして、左右の矢印キーなら、距離を出すとき、Y方向の成分は従来の4倍離れているように、
逆に上下キーなら、距離のX方向の成分は従来の4倍に。
これで少しは寄り道しないように改善できないでしょうか
お、なかなかいい感じ
画面の描画を更新するときに、ちらつきが気になります
特にスクロールバー関連。
なんとかしたいものです...
cui.c の cui_draw() にコメントとして入れてる usleep()の呼び出しを有効にして、 わざとゆっくり描画させて、どのくらいちらついてるか確かめてみます
void cui_draw(cui obj) { int val; if(obj == NULL) return; if(!cui_is_visible(obj)) return; val = (obj == cui_focus_get()) ? CUI_DRAW_FOCUS : CUI_DRAW_NORMAL; cui_handler_call(obj, CUI_EVT_DRAW, val); usleep(20*1000); // for debug cui_draw_chain(obj->children); }
スクロールバーのボタンを押すたびに、2回クリアと描画が入ってるようです。
月の表示のポップアップメニューを開いたときも、2回描画されてます。
ポップアップメニューを閉じたときは、rootから再描画が入るので、ちらつきがよくわかります。
スクロールパネルを一度描画したあと、パネルの枠の描画で上書きされて、再度、スクロールバーを上書きするのが見えてます
これは、なんとかせねばなりません
一度ポップアップメニューを開いてから閉じるまでの、描画の処理を追いかけてみます
cui_menu_popup_hdr() で ENTERキーの処理 cui_show(menu) cui_esc_cnt > 0 で cui_draw(obj) cui_handler_call(obj, CUI_EVT_DRAW, val) menu自体はbaseの入れものだけで描画ハンドラなし cui_draw_chain(obj->children) cui_draw(obj) cui_handler_call(obj, CUI_EVT_DRAW, val) メニューアイテムのハンドラ呼び出し cui_menu_item_hdr() cui_lblfix_attr_set(p->lbf, attr) cui_label_attr_set(p->lb, attr) cui_draw(obj) cui_handler_call(obj, CUI_EVT_DRAW, val) ラベルのハンドラ呼び出し cui_label_hdr() cui_draw_str() # ここで先頭のメニューアイテムの文字列部分の1回目の描画 cui_fill_attr_set(p->fill, attr) cui_label_attr_set() cui_draw(obj) cui_handler_call(obj, CUI_EVT_DRAW, val) fillのハンドラ呼び出し cui_fill_hdr() cui_fill_rect() cui_draw_str() # ここで先頭のメニューアイテムの後続の空白部分の1回目の描画 cui_label_attr_set(p->lb_end, attr); cui_draw(obj) cui_handler_call(obj, CUI_EVT_DRAW, val) ラベルのハンドラ呼び出し cui_label_hdr() cui_draw_str() # ここで先頭のメニューアイテムの末尾'|'部分の1回目の描画 cui_draw_chain(obj->next); 2つめ以降のメニューアイテムも同様の処理 : # メニューが表示された状態から cui_main(menu, cui_index_to_child(menu, p->val)) cui_clear(top_obj) cui_clear_rect() cui_fill_rect() cui_draw_str() # メニュー消去 cui_draw(top_obj); : # 同じ処理でメニュー表示2回目(!) : ENTERキー入力 cui_handler_call(focus, CUI_EVT_KEY, key) メニューアイテムのハンドラ呼び出し cui_menu_item_hdr() CUI_EVT_KEYの処理 cui_handler_call(obj, CUI_EVT_BUTTON, val == CUI_KEY_ENTER) メニューアイテムのハンドラ呼び出し cui_menu_item_hdr() CUI_EVT_BUTTONの処理 cui_handler_call(obj->parent, CUI_EVT_BUTTON, val) メニューのハンドラ呼び出し cui_menu_popup_hdr() CUI_EVT_BUTTONの処理 cui_quit() # cui_main()を抜けてcui_menu_popup_hdr()に戻る cui_hide(menu) cui_clear(obj) cui_clear_rect() cui_fill_rect() cui_draw_str() # メニュー消去 cui_handler_call(obj, CUI_EVT_BUTTON, p->val) cui_test.c popup_hdr() # ラベルの描画処理 : cui_draw(menu->parent) # ここ (!) ポップアップメニューで隠れてた領域を再描画するため # cui_test.c の root のパネル # bs = cui_panel_new(NULL, 0, 0, 42, 23) が指定される cui_handler_call(obj, CUI_EVT_DRAW, val) まずパネルのハンドラ cui_panel_hdr() bak = cui_hide_enter(p->fill[0]); cui_hide(obj) cui_clear() cui_clear_rect() cui_fill_rect() cui_draw_str() # 枠の一辺消去 cui_xywh_set(p->fill[0], 1, 0, obj->w-2, 1); cui_hide_enter() # すでに非表示なので何もしない cui_hide_exit() # すでに非表示なので何もしない cui_hide_exit(p->fill[0], bak); cui_show(obj) cui_draw(obj) cui_handler_call(obj, CUI_EVT_DRAW, val) # fillのハンドラ呼び出し cui_fill_rect() cui_draw_str() # 枠の一辺描画 : # 同様に他の辺と頂点を消去、描画 : cui_draw_chain(obj->children) # パネルに載ってる子孫全部を表示していく # その中の1つのスクロールパネルの描画の場合 cui_draw(obj) cui_handler_call(obj, CUI_EVT_DRAW, val) # スクロールパネルのハンドラ呼び出し cui_scpanel_hdr() cui_scbar_max_set(p->hbar, p->sheet->w - p->view->w) cui_hide_enter() cui_hide() cui_clear() cui_clear_rect() cui_fill_rect() cui_draw_str() # スクロールバーの領域消去 (max変更と思ってる) cui_hide_exit() cui_show() cui_draw() cui_handler_call(obj, CUI_EVT_DRAW, val) # スクロールバーのハンドラ呼び出し cui_scbar_hdr() cui_hide_enter(p->dec_pg) cui_hide() cui_clear() cui_clear_rect() cui_fill_rect() cui_draw_str() # スクロールバーのdecボタン消去 cui_w_set(p->dec_pg, pos) # 非表示なので w の更新のみ cui_hide_exit(p->dec_pg, bak) cui_show() cui_handler_call(obj, CUI_EVT_DRAW, val) # スクロールバーのdecボタンのfillbtnのハンドラ呼び出し cui_fillbtn_hdr() cui_fill_attr_set(p->fill, attr) cui_label_attr_set() cui_draw() cui_handler_call(obj, CUI_EVT_DRAW, val) # fillのハンドラ呼び出し cui_fill_hdr() cui_fill_rect() cui_draw_str() # スクロールバーのdecボタン描画 cui_wh_set(p->fill, obj->w, obj->h); cui_xywh_set() cui_hide_enter() cui_hide() cui_clear() cui_clear_rect() cui_fill_rect() cui_draw_str() # スクロールバーのdecボタン消去(2回目) # x,y,w,h変更して cui_hide_exit() cui_show() cui_handler_call(obj, CUI_EVT_DRAW, val) # スクロールバーのdecボタンのfillbtnのハンドラ呼び出し cui_fillbtn_hdr() cui_fill_hdr() cui_fill_rect() cui_draw_str() # スクロールバーのdecボタン描画(2回目) # cui_scbar_hdr()に戻って # スクロールバーの他のボタンも同様の処理 : # cui_scpanel_hdr() に戻って cui_show(p->hbar) cui_draw() cui_handler_call(obj, CUI_EVT_DRAW, val) # スクロールバーのハンドラ呼び出し cui_scbar_hdr() : # 同じ処理が繰り返され # スクロールバーの decボタンから順に # 2回づつ消去と表示されていく : # vbar についても同様の処理がなされた # return FALSE (!) # cui_scpanel_hdr() が FALSE を返してたので # 続いてパネルのハンドラ呼び出し cui_panel_hdr() # 先述の通り、枠の一辺から順に消去と描画 # これで、スクロールバーの描画は、枠の描画で上書きされてしまう : # そして scpanel に載ってる children の cui_draw_chain() がはじまり cui_draw_chain(obj->children) # scpanelに載ってる子孫全部を表示していく # その中の1つのスクロールバーの描画 cui_draw() cui_handler_call(obj, CUI_EVT_DRAW, val) # スクロールバーのハンドラ呼び出し cui_scbar_hdr() : # 同じ処理が繰り返され # スクロールバーの decボタンから順に # 2回づつ消去と表示されていく : # これでパネルの枠のを消してスクロールバーで上書き :
うひょー
何回スクロールバーを描画し直しているんだ〜
ポップアップメニューを開いたときの2回描画は、 menu.c cui_menu_popup_hdr() から cui_main() 呼ぶ直前の cui_show() による描画と、cui_main()に入ってからの cui_clear()とdraw() による描画が原因でした
flags の CUI_FLG_HIDE を落したいけど、どうせcui_main()で描画するから、
描画はしたくないと...
ここは、flags の設定だけにしておきます
void cui_xywh_set(cui obj, int x, int y, int w, int h) { int bak = cui_hide_enter(obj); if(bak) cui_draw(obj->parent); obj->x = x; obj->y = y; obj->w = w; obj->h = h; cui_hide_exit(obj, bak); if(obj == cui_focus_get() && !cui_is_visible_rect(obj)) cui_focus_set(NULL); }
cui.c cui_xywh_set() からの cui_draw() では、変更によって露出する背景の描画をしてました。
変更が不要な場合も多くあるので、一旦削除して保留にしておきます。
ポップアップメニューでは、cui_hide() を呼び出した人が、
責任もって背景の更新を cui_draw(obj->parent) でするようにします
それよりも、大きな問題としては...
scpanel.c の描画ハンドラは、パネルのサイズを、載せてるスクロールバーに対して
通知するために存在してて、呼び出すとスクロールバーを再描画してます。
ところがスクロールバーの描画自体は、パネルに載ってる部品として、
再帰的に描画されるので、重複してしまってます
解決の一つの方向としては、スクロールバーのサイズ合わせと、 viewのサイズ判定による非表示の処理さえなんとかなってれば、 scpanelの描画ハンドラは不要になるはずです
もう一つの方向としては、判定はひきつづき描画ハンドラでするけども、 設定が必要なときだけ処理するようにして、 中に入れてるスクロールバーの描画はしないようにして、 再帰でスクロールバー自身の回の描画にまかせるようにします
スクロールバーのサイズ合わせや、表示/非表示の切替えが必要になるのは、
入れもののパネルのリサイズ時と、パネルの中のsheetのリサイズ時の、
どちらかのはずです。
今のところリサイズは cui_xywh_set() を経由するので、
その中からの cui_draw() で、その部品固有のハンドラで、
サイズ合わせの処理をしています。
さて、どうしたものか...
例えば見えてる入れものをリサイズしたくて cui_xywh_set() を呼ぶと、
hideして w, h をリサイズしてshowします。
showからdrawで、入れものの描画ハンドラ、中にはいってる部品の描画ハンドラの順に呼び出されます。
入れものの描画ハンドラから、中の部品について cui_xywh_set() を呼び出してフィットさせると、
フィットさせるための cui_xywh_set() で、部品の hide, w, h リサイズ, show で描画され、
さらに、自身の描画の回で再度、描画ハンドラが呼び出されます
むむむ。
ここは前者の方向で考えて、リサイズはリサイズでちゃんとする。
リサイズ用のイベントを追加すべきでしょうか。
描画は描画のハンドラ、リサイズはリサイズのハンドラ。
cui_xywh_set() でw, h変更したら、その部品のリサイズのハンドラを呼び出します。
ハンドラの中で、必要なら中の部品に対して cui_xywh_set() 呼び出すようにします。
これでどうでしょう
x, y の位置変更用も用意すべきでしょうか?
位置の方は今のところ使う場面を想像できてないので、やめておきます
リサイズイベントとしては、w, h を変更した後、変更が入ったよ通知としての、
ハンドラ呼び出しになるので、イベントの値valは不要です。
とりあえず TRUE でも渡しておきます
コーディングして試してみます。
さて、うまくいくだろうか
とりあえず panel, scpanel, scbar, fillbtn, lblfix でリサイズイベント対応に
$ grep cui_lblfix_w_set *.c lblfix.c:cui_lblfix_w_set(cui obj, int w) menu.c: cui_lblfix_w_set(p->lbf, obj->w - 1); $
lblfix をリサイズイベント対応にして、_w_set() は廃止。
なので、いもづる式に menu の cui_menu_item_w_set() も廃止して、
ハンドラで対応に
cui_show(), cui_hide() も必要なときだけ cui_draw(), cui_clear()するように修正します
スロー再生の様子をみてると、
cui_test.c 起動時に cui_main() を呼び出すまでに、散々描画されてます。
そして、cui_main() に入り、その中で結局クリアして、描画しなおす動作になってます。
cui_show() では、cui_main() 実行前は、cui_draw() しないようにしてるはずなので変です。
追っかけてみると、cui_popup_menu_init() が原因でした。
cui_menu_new() で予めポップアップすべきメニューを作っておいて、cui_hide() で隠してます。
そのとき、クリアされた領域を再描画すべく cui_draw(p->menu->parent) してました
メニューは生成と同時に表示したい事はまずないので、非表示の状態で生成して、
使う人がcui_show()する方が、より自然と言えましょうや。
そのように修正しておきます。
これでかなり描画のちらつきは軽減されました (^_^v
リサイズ処理が整ったので、スクロールバーに続いて、 リサイズボックスを追加してみます
fillbtnから継承し、スクロールバーのthumb部と同様に、
ENTERでモーダルに入ってドラッグ処理します。
矢印キーで移動して、ENTERキーで決定、ESCキーで取り消します。
配置は親の入れものの右下隅に固定。
どの入れものでも動作可能に作っておきますが、
とりあえずスクロールパネルには、標準で追加しておきます
モーダルに入ってるかどうかを、fillbtnの保持してる値vを使いたいので、 fillbtnにvの設定/参照用の関数を追加しておきます
直接関係ないですが、ckbox.c の値設定用の関数で、 無駄な処理が残ってたので、修正しておきます
$ make $ ./cui_test 右、左、ENTER +----------------------------------------+ |/foo|/bar|/hoge| | |+--------------------------------------+| ||File| [X] Show (move) || ||[foo ] || || +----------------------------------+ || || |+--------------------+ +----------- || || || | |Radio = || || || Are you sure ? | | ( ) ON (= || || || | | = || || || (OK) (Cancel) | | = || || || | | = || || |+--------------------+ | = || || |+------------------+ +----------= || || || | | || || || How are you ? | | || || || | LONG| | || || || (^_^) (T_T) | | || || || | January| l| || || |+------------------+ - || || +|======================----------|+ || |+--------------------------------------+| +----------------------------------------+ 右、右、下 x 4 +----------------------------------------+ |/foo|/bar|/hoge| | |+--------------------------------------+| ||File| [X] Show (move) || ||[foo ] || || +----------------------------------+ || || |+--------------------+ +----------- || || || | |Radio = || || || Are you sure ? | | ( ) ON (= || || || | | = || || || (OK) (Cancel) | | = || || || | | = || || |+--------------------+ | = || || |+------------------+ +----------= || || || | | || || || How are you ? | | || || || | LONG| | || || || (^_^) (T_T) | | || || || | January| l| || || |+------------------+ - || || +|======================----------|+ || |+--------------------------------------+| +----------------------------------------+ 無事フォーカスがきました ENTER、上、上、左、左 +----------------------------------------+ ||File| [X] Show (move) || |+--------------------------------------+| ||File| [X] Show (move) || ||[foo ] || || +--------------------------------- || || |+--------------------+ +--------- || || || | |Radio = || || || Are you sure ? | | ( ) ON= || || || | | = || || || (OK) (Cancel) | | | || || || | | | || || |+--------------------+ | | || || |+------------------+ +--------| || || || | | || || || How are you ? | | || || || | LONG| | || || || (^_^) (T_T) | - || || ||==================------------|+ || || || || || |+--------------------------------------+| +----------------------------------------+ よーす 一旦 ENTER で決定して ENTER、上 x 4、左 x 4、ESC +----------------------------------------+ ||File| [X] Show (move) || |+--------------------------------------+| ||File| [X] Show (move) || ||[foo ] || || +--------------------------------- || || |+--------------------+ +--------- || || || | |Radio = || || || Are you sure ? | | ( ) ON= || || || | | = || || || (OK) (Cancel) | | | || || || | | | || || |+--------------------+ | | || || |+------------------+ +--------| || || || | | || || || How are you ? | | || || || | LONG| | || || || (^_^) (T_T) | - || || ||==================------------|+ || || || || || |+--------------------------------------+| +----------------------------------------+ よーす 取り消しOK ENTER、上、上、上 ..... +----------------------------------------+ |/foo|/bar|/hoge| | |+--------------------------------------+| ||File| [X] Show (move) || ||[foo ] || || || || || || || || || || || || Floating point exceptionon $ || || || || || || || || || || || || || || || || || || || |+--------------------------------------+| +----------------------------------------+ がーん (T_T)
そう、わかってます。
たぶんスクロールバーの位置計算がまずいです...
スクロールバーの位置計算を見直します
現状の scbar.c の処理をみてみます。
まず、縦長/横長を共通で処理できるように、
スクロールバーの長い方向のサイズを返す関数
int cui_scbar_pg_len(cui obj) { cui_scbar p = (cui_scbar)obj; return p->hv == CUI_SCBAR_H ? obj->w : obj->h; }
これは問題ないです
次に、thumbの長さの計算
int cui_scbar_thumb_len(cui obj) { cui_scbar p = (cui_scbar)obj; int len = cui_scbar_pg_len(obj) - (2 + p->max_v); return len < 1 ? 1 : len; }
スクロールバーの長さから、先頭と末尾のincとdecのボタンの幅2を引いて、
thumb部が描画される領域の長さになります。
「thumb部が描画される領域の長さ」をLとします。
今仮に、長さLの領域に、サイズ1のthumbとすると、
thumb部の位置は、0 から L-1 まで移動できます。
スクロールバーの値は 0 から max_v まで変化します。
L-1 > max_v ならば、thumbの位置は、0 から途中の max_v まで移動できれば十分です。
なので、その場合は、thumbのサイズを大きくして、余った部分を埋めることにしてます。
これが、そもそも、thumbのサイズを変化させる事の理由です。
そこで一旦 L-1 > max_v の場合について、余った部分から thumb のサイズを算出してみます。
L-1 == max_v つまり L-1 - max_v == 0 つまり L - max_v == 1 ならば thumb部のサイズは1
L - max_v == 2 ならばthumb部のサイズは2
なので、この場合は L - max_v が thumb部のサイズになります
逆に、L - max_v の値が 1 未満になる場合は、
thumb部の長さは最小の1に固定します。
thumb部の位置をひとつ動かすと、スクロールバーの持つ値は、1以上変化させる事になります。
L は 「thumb部が描画される領域の長さ」 で 「スクロールバーの長い方向のサイズ」- 2
そこからさらにmax_vを引くとthumb部のサイズで、1より小さければ1を返してます。
この関数もOKです
次は thumb がとる最大の位置
int cui_scbar_thumb_pos_max(cui obj) { return cui_scbar_pg_len(obj) - (2 + cui_scbar_thumb_len(obj)); }
「スクロールバーの長い方向のサイズ」 - (2 + thumb部のサイズ)
thumb部の位置は 0 から、この pos_max まで移動可能です
問題ないです
スクロールバーの持つ値 v に対して、対応するthumb部の位置を返す関数です
int cui_scbar_thumb_pos(cui obj) { cui_scbar p = (cui_scbar)obj; int pos_max = cui_scbar_thumb_pos_max(obj); int add100 = 100 * p->max_v * (pos_max + 1) / pos_max - 100 * p->max_v; return p->v * (pos_max +1) * 100 / (p->max_v*100 + add100); }
これは問題ありそうです。
なんか、とりあえず、ややこしいです。
thumb部の位置は 0 から pos_max まで変化して、値は 0 から max_v まで変化するはず。
単純に考えると 「thumb部位置 pos」= v * pos_max / max_v なのですが...
pos_max < max_v で、thumb部を一つ動かすと、値が1以上変化する場合を考えてみます。
仮に pos_max = 2, max_v = 10 とすると
+---+-+-+-+-+-+-+-+-+-+-+--+ |v |0|1|2|3|4|5|6|7|8|9|10| +---+-+-+-+-+-+-+-+-+-+-+--+ |pos|0|0|0|0|0|1|1|1|1|1|2 | +---+-+-+-+-+-+-+-+-+-+-+--+
このような対応関係になります。
ここをなんとか
+---+-+-+-+-+-+-+-+-+-+-+--+ |v |0|1|2|3|4|5|6|7|8|9|10| +---+-+-+-+-+-+-+-+-+-+-+--+ |pos|0|0|0|0|1|1|1|1|2|2|2 | +---+-+-+-+-+-+-+-+-+-+-+--+
的にしたいわけです。
このため、ごちゃごちゃと計算してました
ですが、今思うともっと単純にできます。
v が max_v + 1 のとき pos が pos_max + 1 になればいいのでは?
つまり pos = v * (pos_max + 1) / (max_v + 1) です。
これなら
+---+-+-+-+-+-+-+-+-+-+-+--+--+ |v |0|1|2|3|4|5|6|7|8|9|10|11| +---+-+-+-+-+-+-+-+-+-+-+--+--+ |pos|0|0|0|0|1|1|1|1|2|2|2 |3 | +---+-+-+-+-+-+-+-+-+-+-+--+--+
の対応関係になります。
そして使うのは v が 0 から 10 の範囲だけです
次に逆の対応です。
thumb部を移動させた時、その位置に対応するスクロールバーの値 v を
算出する関数です
int cui_scbar_thumb_pos_to_val(cui obj, int pos) { cui_scbar p = (cui_scbar)obj; int pos_max = cui_scbar_thumb_pos_max(obj); int add100 = 100 * p->max_v * (pos_max + 1) / pos_max - 100 * p->max_v; return (pos * (p->max_v*100 + add100) * 2 + (p->max_v*100 + add100)) / ((pos_max + 1) * 100 * 2); }
これまた、先のややこしいやつの逆向きの動作をさせようとして、かなり難解です。
単純に修正した方の、逆向きならどうでしょうか?
v = pos * (max_v + 1) / (pos_max + 1)
+---+-+-+-+--+ |pos|0|1|2|3 | +---+-+-+-+--+ |v |0|4|8|11| +---+-+-+-+--+
使うのは pos = 0, 1, 2 だけですが、
割算で整数に切捨てるので、pos に対応する v の範囲のうち、
一番小さい側の値になってます。
これは、まずいです。
+---+-+-+--+ |pos|0|1|2 | +---+-+-+--+ |v |0|5|10| +---+-+-+--+
こんな感じになってほしいところです。
よくみると、最初のまずかった対応
+---+-+-+-+-+-+-+-+-+-+-+--+ |v |0|1|2|3|4|5|6|7|8|9|10| +---+-+-+-+-+-+-+-+-+-+-+--+ |pos|0|0|0|0|0|1|1|1|1|1|2 | +---+-+-+-+-+-+-+-+-+-+-+--+
これの逆パターンになってます。
という事は v = pos * max_v / pos_max でよさそうです
さて、この修正でどうでしょうか
$ make $ ./cui_test 左、右、barフォーカスで ENTER +----------------------------------------+ |/foo|/bar|/hoge| | | | | bar | | |--------=---------| - | | |========----------| | | | | | : 下、右、thumb部フォーカスで ENTER +----------------------------------------+ |/foo|/bar|/hoge| | | | | bar | | |--------=---------| - | | |========----------| | | | | | : 左 x 4 +----------------------------------------+ |/foo|/bar|/hoge| | | | | 1 | | |=-----------------| - | ありゃ 0 にならんです 右、右、って移動しない... とりあえず撤収 ESC、上、左、barフォーカスで ENTER 下、ENTER、下 x 3、ENTER result not yet $
何が起こっているのでしょうか?
cui_test.c cui sc = cui_scbar_new(bs_bar, 1, 2, 20, 1, 30, 15);
の設定なので、サイズ 20 最大値 30
thumb部のサイズは 1 になるはずで
「thumb部が描画される領域の長さ」L は 20-2 で 18
pos_max は L - 「thumb部のサイズ」で 18-1 で 17
max_v は 30
スクロールバー scbar.c でthumb部を操作するときの処理は
case CUI_KEY_LEFT: if(p->hv == CUI_SCBAR_H){ pos = fb->x - 1; if(pos > 0) cui_handler_call(fb, CUI_EVT_BUTTON, cui_scbar_thumb_pos_to_val(obj, pos-1)); } break; case CUI_KEY_RIGHT: if(p->hv == CUI_SCBAR_H){ pos = fb->x - 1; if(pos < pos_max) cui_handler_call(fb, CUI_EVT_BUTTON, cui_scbar_thumb_pos_to_val(obj, pos+1)); } break;
pos は thumb部のfillbtnの x 座標から、前にある decボタンの幅1を引くのは、これでよしとして...
初期値 v = 15 なので 最初の pos 位置は 15 * (17 + 1) / (30 + 1) = 8
| bar | | |--------=---------| - |
この表示であってます
ここから thumb部フォーカスでENTERのあと、
左 x 4 で thumb部が左端に辿りつくのは変です。
1回の左キーで、2こま移動してます。
初期状態で pos = 8
左キー入力で cui_scbar_thumb_pos_to_val(obj, pos-1) の実行
pos_max は 17, max_v は 30 なので実行結果の返り値は
(8-1) * 30 / 17 = 12
この値 12 を v に設定しなおしにいくので、ハンドラ cui_scbar_btn_hdr が val = 12 で呼ばれて
}else{ /* fb == p->thumb */ if(0 <= val && val <= p->max_v){ p->v = val; cui_scbar_update(obj); cui_handler_call(obj, CUI_EVT_BUTTON, p->v); }
確かに v が 12 に設定されて、cui_scbar_update() で表示を更新しにかかります
void cui_scbar_update(cui obj) { cui_scbar p = (cui_scbar)obj; int pos = cui_scbar_thumb_pos(obj); int len = cui_scbar_thumb_len(obj); /* evt == CUI_EVT_RESIZE */ int bak = cui_hide_enter(obj); if(p->hv == CUI_SCBAR_H){ cui_w_set(p->dec_pg, pos); cui_x_set(p->thumb, cui_x2(p->dec_pg)); cui_w_set(p->thumb, len); cui_x_set(p->inc_pg, cui_x2(p->thumb)); cui_w_set(p->inc_pg, obj->w-1 - cui_x2(p->thumb)); cui_x_set(p->inc, obj->w-1); }else{
冒頭で pos を算出して
12 * (17 + 1) / (30 + 1) = 6
なんと!?
うーむ。なるほど。計算してみると
pos から v +---+-+-+-+-+-+-+--+--+--+ |pos|0|1|2|3|4|5|6 |7 |8 |... +---+-+-+-+-+-+-+--+--+--+ |v |0|1|3|5|7|8|10|12|14|... +---+-+-+-+-+-+-+--+--+--+
v から pos +---+-+-+-+-+-+-+-+-+-+-+--+--+--+--+--+ |v |0|1|2|3|4|5|6|7|8|9|10|11|12|13|14|... +---+-+-+-+-+-+-+-+-+-+-+--+--+--+--+--+ |pos|0|0|1|1|2|2|3|4|4|5|5 |6 |6 |7 |8 |... +---+-+-+-+-+-+-+-+-+-+-+--+--+--+--+--+
pos=8 で v=15 の初期値から
pos=7 にしようとして v を算出すると v = 12
v=12 から pos を算出すると pos=6
よって、pos=8 から 6 へ2つ移動すると...
これは pos から v の算出方法を簡単に考え過ぎてました。
v から pos の表
+---+-+-+-+-+-+-+-+-+-+-+--+--+ |v |0|1|2|3|4|5|6|7|8|9|10|11| +---+-+-+-+-+-+-+-+-+-+-+--+--+ |pos|0|0|0|0|1|1|1|1|2|2|2 |3 | +---+-+-+-+-+-+-+-+-+-+-+--+--+
これは確定でよいとして
この表をひっくりかえして
+---+-+-+-+-+-+-+-+-+-+-+--+--+ |pos|0|0|0|0|1|1|1|1|2|2|2 |3 | +---+-+-+-+-+-+-+-+-+-+-+--+--+ |v |0|1|2|3|4|5|6|7|8|9|10|11| +---+-+-+-+-+-+-+-+-+-+-+--+--+
pos に対する v の値は範囲内にしないとダメでした。
例えば pos=1 に対しては、
v=4〜7 の範囲から選ばないと、対応の関係がくずれます
vの値の範囲内の最小と最大は
最小 = pos * (max_v + 1) / (pos_max + 1)
で、単なる逆の計算で切捨て
細大 = (pos + 1) * (max_v + 1) / (pos_max + 1) - 1
で、となりの最小をもとめて、1引けばいいでしょう
この範囲からどう選ぶか?
0とのきは最小の0
pos_maxのときは最大のmax_v
間をどうするか?
一律で、(最小 + 最大) / 2 で真中に固定するか?
入力のposの値は 0 から pos_max まで変化するので、重みをつけて
最小 + (最大 - 最小) * pos / pos_max
これなら範囲内の条件を満たしつつ、均等に変化しそうです
計算してみると
$ ( for v in $(seq 0 31) ; do echo $v ; done | paste -s -d ' ' ; \ for v in $(seq 0 31) ; do echo "$v*17/31" | bc ; done | paste -s -d ' ' ) | \ sed -e 's/ /\\| /g' -e 's/$/ \\-/' | txtbl +-+-+-+-+-+-+-+-+-+-+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ |0|1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31| +-+-+-+-+-+-+-+-+-+-+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ |0|0|1|1|2|2|3|3|4|4|5 |6 |6 |7 |7 |8 |8 |9 |9 |10|10|11|12|12|13|13|14|14|15|15|16|17| +-+-+-+-+-+-+-+-+-+-+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 逆方向は $ ( for pos in $(seq 0 17) ; do echo $pos ; done | paste -s -d ' ' ; \ for pos in $(seq 0 17) ; do echo "($pos * 31 / 17 )" | bc ; done | paste -s -d ' ' ; \ for pos in $(seq 0 17) ; do a="($pos * 31 / 17 )" ; b="(($pos + 1) * 31 / 17 - 1)" ; \ echo "$a + ($b - $a) * $pos / 16" | bc ; done | paste -s -d ' ' ) | \ sed -e 's/ /\\| /g' -e 's/$/ \\-/' | txtbl +-+-+-+-+-+-+--+--+--+--+--+--+--+--+--+--+--+--+ |0|1|2|3|4|5|6 |7 |8 |9 |10|11|12|13|14|15|16|17| +-+-+-+-+-+-+--+--+--+--+--+--+--+--+--+--+--+--+ |0|1|3|5|7|9|10|12|14|16|18|20|21|23|25|27|29|31| +-+-+-+-+-+-+--+--+--+--+--+--+--+--+--+--+--+--+ |0|1|3|5|7|9|10|12|14|16|18|20|21|23|25|27|30|31| +-+-+-+-+-+-+--+--+--+--+--+--+--+--+--+--+--+--+ $ txtblコマンドはこちら ;-p) なので +---+-+-+-+-+-+-+-+-+-+-+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ |v |0|1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31| +---+-+-+-+-+-+-+-+-+-+-+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ |pos|0|0|1|1|2|2|3|3|4|4|5 |6 |6 |7 |7 |8 |8 |9 |9 |10|10|11|12|12|13|13|14|14|15|15|16|17| +---+-+-+-+-+-+-+-+-+-+-+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ +---+-+-+-+-+-+-+--+--+--+--+--+--+--+--+--+--+--+--+ |pos|0|1|2|3|4|5|6 |7 |8 |9 |10|11|12|13|14|15|16|17| +---+-+-+-+-+-+-+--+--+--+--+--+--+--+--+--+--+--+--+ |v |0|1|3|5|7|9|10|12|14|16|18|20|21|23|25|27|30|31| +---+-+-+-+-+-+-+--+--+--+--+--+--+--+--+--+--+--+--+
だめだ。整数割算の切捨てでズレた
逆方向は割算で切り上げしなければなりません
v = pos * (max_v + 1) / (pos_max + 1) の箇所が
v = (pos * (max_v + 1) + pos_max) / (pos_max + 1) です
やりなおしてみると
$ ( for pos in $(seq 0 17) ; do echo $pos ; done | paste -s -d ' ' ; \ for pos in $(seq 0 17) ; do echo "(($pos * 31 + 16) / 17 )" | bc ; done | paste -s -d ' ' ; \ for pos in $(seq 0 17) ; do a="(($pos * 31 + 16) / 17 )" ; b="((($pos + 1) * 31 + 16) / 17 - 1)" ; \ echo "$a + ($b - $a) * $pos / 16" | bc ; done | paste -s -d ' ' ) | \ sed -e 's/ /\\| /g' -e 's/$/ \\-/' | txtbl +-+-+-+-+-+--+--+--+--+--+--+--+--+--+--+--+--+--+ |0|1|2|3|4|5 |6 |7 |8 |9 |10|11|12|13|14|15|16|17| +-+-+-+-+-+--+--+--+--+--+--+--+--+--+--+--+--+--+ |0|2|4|6|8|10|11|13|15|17|19|21|22|24|26|28|30|31| +-+-+-+-+-+--+--+--+--+--+--+--+--+--+--+--+--+--+ |0|2|4|6|8|10|11|13|15|17|19|21|22|24|26|28|30|32| +-+-+-+-+-+--+--+--+--+--+--+--+--+--+--+--+--+--+ なので +---+-+-+-+-+-+-+-+-+-+-+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ |v |0|1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31| +---+-+-+-+-+-+-+-+-+-+-+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ |pos|0|0|1|1|2|2|3|3|4|4|5 |6 |6 |7 |7 |8 |8 |9 |9 |10|10|11|12|12|13|13|14|14|15|15|16|17| +---+-+-+-+-+-+-+-+-+-+-+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ に対して +---+-+-+-+-+-+--+--+--+--+--+--+--+--+--+--+--+--+--+ |pos|0|1|2|3|4|5 |6 |7 |8 |9 |10|11|12|13|14|15|16|17| +---+-+-+-+-+-+--+--+--+--+--+--+--+--+--+--+--+--+--+ |v |0|2|4|6|8|10|11|13|15|17|19|21|22|24|26|28|30|32| +---+-+-+-+-+-+--+--+--+--+--+--+--+--+--+--+--+--+--+
int cui_scbar_thumb_pos_to_val(cui obj, int pos) { cui_scbar p = (cui_scbar)obj; int pos_max = cui_scbar_thumb_pos_max(obj); int vs = (pos * (p->max_v + 1) + pos_max) / (pos_max + 1); int ve = ((pos + 1) * (p->max_v + 1) + pos_max) / (pos_max + 1) - 1; return vs + (ve - vs) * pos / pos_max; }
$ make $ .cui_test 左、右、ENTER、下、右、ENTER : | bar | | |--------=---------| - | : 左 x 8 OK 右 x 17 OK ENTER、上、左、左、barフォーカス ENTER 下、右、右、Showフォーカス ENTER 右、右、下 x 4 リサイズボックスにフォーカス ENTER +----------------------------------------+ |/foo|/bar|/hoge| | |+--------------------------------------+| ||File| [X] Show (move) || ||[foo ] || || +----------------------------------+ || || |+--------------------+ +----------- || || || | |Radio = || || || Are you sure ? | | ( ) ON (= || || || | | = || || || (OK) (Cancel) | | = || || || | | = || || |+--------------------+ | = || || |+------------------+ +----------= || || || | | || || || How are you ? | | || || || | LONG| | || || || (^_^) (T_T) | | || || || | January| l| || || |+------------------+ - || || +|======================----------|+ || |+--------------------------------------+| +----------------------------------------+ 上 x 14 +----------------------------------------+ |/foo|/bar|/hoge| | |+--------------------------------------+| ||File| [X] Show (move) || ||[foo ] || || +----------------------------------+ || || ||======================----------|+ || || || || || || || || || || || || || || || || || || || || || || || || || || || || || |+--------------------------------------+| +----------------------------------------+ 見事に縮みました 左 x 40 +----------------------------------------+ |/foo|/bar|/hoge| | |+--------------------------------------+| ||File| [X] Show (move) || ||[foo ] || || +----- || || ||=-|+ || || || || || || || || || || || || || || || || || || || || || || || || || || || || || |+--------------------------------------+| +----------------------------------------+ 左 x 5、上 +----------------------------------------+ |/foo|/bar|/hoge| | |+--------------------------------------+| ||File| [X] Show (move) || ||[foo ] || || + || || || || || || || || || || || || || || || || || || || || || || || || || || || || || || || |+--------------------------------------+| +----------------------------------------+ リサイズボックスだけにまで、縮みました さすがにこれ以上は無理ですね 下と右を適当に連打して ENTER +----------------------------------------+ |/foo|/bar|/hoge| | |+--------------------------------------+| ||File| [X] Show (move) || ||[foo ] || || +------------------------ || || |+--------------------+ - || || || | = || || || Are you sure ? | | || || || | | || || || (OK) (Cancel) | | || || || | | || || |+--------------------+ - || || ||=--------------------|+ || || || || || || || || || || || || || || || |+--------------------------------------+| +----------------------------------------+ 左 x 3、thumb部フォーカス ENTER 右と左を適当に連打して動作を確認し、ESC 右、右、上 x 3、thumb部フォーカス ENTER 上と下を適当に連打して動作を確認し、ESC 左、Cancelフォーカス ENTERで終了 result Cancel $
なんとかなりました
タイトルバーを作ってみます
リサイズボックスと同様に、とりあえずスクロールパネルに追加できるようにしてみます。
スクロールパネル生成時に、文字列指定がNULL以外なら、その文字列のタイトルバーを追加するようにします
cui_test.c の fooのタブにしてるパネルの枠の表示邪魔なので、枠なしのcui_baseに変更しておきます
$ make $ ./cui_test +----------------------------------------+ |/foo|/bar|/hoge| | | | | File| [ ] Show (move) | | [foo ] | | | : 少しすっきりしました 左下右右 Showフォーカス ENTER +----------------------------------------+ |/foo|/bar|/hoge| | | | | File| [X] Show (move) | | [foo ] | | ============== title =============== | | |+--------------------+ +----------- | | || | |Radio = | | || Are you sure ? | | ( ) ON (= | | || | | = | | || (OK) (Cancel) | | = | | || | | = | | |+--------------------+ | = | | |+------------------+ +----------= | | || | | | | || How are you ? | | | | || | LONG| | | | || (^_^) (T_T) | | | | || | January| l| | | |+------------------+ - | | +|======================----------|+ | | | +----------------------------------------+ ついてる 下右上下 (むつかしい...) title にフォーカス ENTER +----------------------------------------+ |/foo|/bar|/hoge| | | | | File| [X] Show (move) | | [foo ] | | ============== title =============== | | |+--------------------+ +----------- | | || | |Radio = | | || Are you sure ? | | ( ) ON (= | | || | | = | | || (OK) (Cancel) | | = | | || | | = | | |+--------------------+ | = | | |+------------------+ +----------= | | || | | | | || How are you ? | | | | || | LONG| | | | || (^_^) (T_T) | | | | || | January| l| | | |+------------------+ - | | +|======================----------|+ | | | +----------------------------------------+ 上上左左 +----------------------------------------+ |/foo|/bar|/hoge| | | | |============== title =============== | ||+--------------------+ +----------- | ||| | |Radio = | ||| Are you sure ? | | ( ) ON (= | ||| | | = | ||| (OK) (Cancel) | | = | ||| | | = | ||+--------------------+ | = | ||+------------------+ +----------= | ||| | | | ||| How are you ? | | | ||| | LONG| | | ||| (^_^) (T_T) | | | ||| | January| l| | ||+------------------+ - | |+|======================----------|+ | | | | | | | +----------------------------------------+ 下下 ENTER +----------------------------------------+ |/foo|/bar|/hoge| | | | | | | | |============== title =============== | ||+--------------------+ +----------- | ||| | |Radio = | ||| Are you sure ? | | ( ) ON (= | ||| | | = | ||| (OK) (Cancel) | | = | ||| | | = | ||+--------------------+ | = | ||+------------------+ +----------= | ||| | | | ||| How are you ? | | | ||| | LONG| | | ||| (^_^) (T_T) | | | ||| | January| l| | ||+------------------+ - | |+|======================----------|+ | | | +----------------------------------------+ あー 背景の再描画... 忘れてた ということはリサイズもしかり 上上左 barフォーカス ENTER 左 fooフォーカス ENTER で再描画させておいて 下 x 4、右 x 3 でリサイズボックスにフォーカスして ENTER 上 x 12、左 x27 で縮小しておいて ENTER +----------------------------------------+ |/foo|/bar|/hoge| | | | | File| [X] Show (move) | | [foo ] | |= title = | ||-------- | || - | |||=----|+ | | | : 左上 tilteフォーカス ENTER 上上上で移動してENTER +----------------------------------------+ |/foo|/bar|/hoge| | |= title = | ||--------X] Show (move) | || - | |||=----|+ | | | : これでFileメニューとエディットテキストを隠しておいて 下下右右... ううぅ リサイズボックスにフォーカスしようとして、 矢印キーをさまよってると +----------------------------------------+ |/foo|/bar|/hoge| | |= title = | ||File|---X] Show (move) | ||[foo ] | |||=----|+ | | | : Fileメニューやエディットテキストにフォーカスしてしまい、 重なって表示が出てしましました そしてリサイズボックスにフォーカスしてENTER 右 x 4 +----------------------------------------+ |/foo|/bar|/hoge| | |=== title === | ||------------how (move) | || - | |||=--------|+ | : 広げると消えてしまって... 左 x 8 ENTER +----------------------------------------+ |/foo|/bar|/hoge| | |= t = | ||---- how (move) | || - | |||=|+ | | | 縮めてみても、再描画はされないと 上 Fileメニューにフォーカス ENTER 下 x 3 ENTER で終了 result not yet $
まだまだ問題ありますね
ここまでのソースファイル cui72.tgz
背景の再描画を何とかしてみます
ここまで、兄弟の部品のオーバーラップは無視というか、
重ならないように並べる暗黙の前提できました。
ただし、タブボタンで切替える入れものについては、
重なっていても、一つだけが表示されるという前提なので、問題ありませんでした。
このオーバーラップの問題もあるのですが、とりあえず一旦先送りして、
重ならないように配置するという前提のままで進めます
そしてまず、背景の再描画からとりかかります
キーポイントはやはり cui_xywh_set() です
void cui_xywh_set(cui obj, int x, int y, int w, int h) { int bak = cui_hide_enter(obj); int rsz = (obj->w != w || obj->h != h); obj->x = x; obj->y = y; obj->w = w; obj->h = h; if(rsz) cui_handler_call(obj, CUI_EVT_RESIZE, TRUE); cui_hide_exit(obj, bak); if(obj == cui_focus_get() && !cui_is_visible_rect(obj)) cui_focus_set(NULL); }
部品のリサイズと移動は、ここを経由します。
見えてないときは x, y, w, h を変更するだけ。
見えてたら cui_hide() してクリアしてから、x, y, w, h 変更して、
変更後に、cui_show() で描画しなおしてます
ちらつき対策のときに、ここに入れてた背景の再描画の処理は、
一旦保留として削除してました。
そして、再描画が必要な場面では、とりあえずcui_xywh_set() 呼び出し側で、
予め cui_hide(obj)、cui_draw(obj->parent)、cui_xywh_set()、cui_show(obj)
などとしてました。
ですが、やはりこの cui_xywh_set() の中でやっつけてしまうのが、本当でしょう。
部品が見えてる場合では、cui_hide()、cui_show() で表示/非表示せずとも、
変更前と変更後の領域をクリアして、x, y, w, h 変更してから、
親の入れものから cui_draw() で描画してもらえば、それでよさげです。
ただし単純にそうしてしまうと、不要な領域も描画しなおすので、ちらついたり、時間がかかったりします
結局 cui_xywh_set() で描画を更新したい領域は、
変更前と変更後のORだけです。
ここはひとつクリッピング領域を設けて制限してみます。
クリッピング処理は既に描画の処理に入れてました。
全ての描画の基底の処理 cui_draw_str() で、
表示したい文字列の領域に対して、
その部品の見えてる領域をcui_visible_rect() で求めて、
ANDで残った部分だけ文字列を描画してます。
これは結局、親の入れものの方向での、見えてる領域のクリッピングで、
これは、これで必須です
上記のクリッピングに加えて、さらに cui_xywh_set() での、 「変更前と変更後をORした領域」とANDをとって、 さらなる絞り込みをしたい訳です
ここで脱線して、2つのRECTのORについて考えてみます
包含関係にあれば、結果は大きい方のRECTになります。
重複領域が無ければ、結果は元の2つのRECTのままです
さて、包含関係ではなくて、重複があった場合について。
単純にOR結果をイメージしてみると、
どちらかの大きい方のRECTはそのままにしておいて、
小さい方のRECTを分割して並べてるような印象です
どちらをそのままにすべきかは、さて置き、小さい方の分割を考えると、
元のRECTから、AND領域を取り去る処理になります。
包含関係なしで、重複ありで、RECTからRECTを取り去るとなると、
次の4パターンに分類できます
(1) o-----------o | and | *-----------* | | | | | | +-----------+ (2) +-----------+ | | *-----------* | and | *-----------* | | +-----------+ (3) o-----*-----o | and | | *-----+ | | | | | | | +-----------+ (4) +-----------+ | | *------+ | | and | | *------+ | | | +-----------+
and が取り去るべきANDの領域で、'o'は頂点が重なってる箇所、 '*'は頂点が辺上にある箇所です
一致する頂点を調べれば、(1)と(3)とそれ以外に判定できます。
それ以外の(2)と(4)の判定はどうすべきか?
o.x, o.y, o.w, o.h i.x, i.y, i.w, i.h として x + w --> x2 y + y --> y2 o.x, o.y, o.x2, o.y2 i.x, i.y, i.x2, i.y2 cnt = (o.x == i.x) + (o.x2 == i.x2) + (o.y == i.y) + (o.y2 == i.y2) この cnt が 2なら(4)、4なら(2) 頂点一致の判定も、垂直線と水平線でcntを分ければ この辺の重複判定で、一緒にできそうです cnt_x = (o.x == i.x) + (o.x2 == i.x2) cnt_y = (o.y == i.y) + (o.y2 == i.y2) cnt = cnt_x + cnt_y cnt == 3 なら (1) cnt == 1 なら (4) cnt_x == cnt_y なら (3) その他 (2) これだけなら x2, y2 も w, h に戻して cnt_x = (o.x == i.x) + (o.x + o.w == i.x + i.w) cnt_y = (o.y == i.y) + (o.y + o.h == i.y + i.h) cnt = cnt_x + cnt_yさて、and をとりさって分割するとどうなるか?
(1)が一番楽そうです。
and領域を取り去ったあともRECTの数は1のままです。
RECTについて、重複してた3つの辺と、重複してなかった1つの辺ととらえると、
重複してなかった辺の向かい側の辺を、and領域で重複してなかった辺へとズラします。
if(o.x != i.x) o.x2 = i.x; else if(o.x2 != i.x2) o.x = i.x2; else if(o.y != i.y) o.y2 = i.y; else o.y = i.y2; /* o.y2 != i.y2 */ x2, y2, を w, h で考えると x2へvを設定するのは、w=v-xを設定します y2へvを設定するのは、h=v-yを設定します xへvを設定するのは、w=w+x-v, x=v の順に設定します yへvを設定するのは、h=h+y-v, y=v の順に設定します if(o.x != i.x) o.w = i.x - o.x; else if(o.x + o.w != i.x2){ o.w += o.x - (i.x + i.w;); o.x = i.x + i.w; }else if(o.y != i.y) o.h = i.y - o.y; else{ /* o.y + o.h != i.y2 */ o.h += o.y - (i.y + i.h); o.y = i.y + i.h; }
続いて(2)
これはRECTが2つになります。
とりあえず o の複製をとって o2 とします。
重複してなかった辺2つのうち、o では大きい側の辺を、
o2 では小さい側の辺を移動することにします
o2 = o; if(cnt_x){ o.y2 = i.y; o2.y = i.y2; }else{ o.x2 = i.x; o2.x = i.x2 } w, h にすると if(cnt_x){ o.h = i.y - o.y; o2.h += o2.y - (i.y + i.h); o2.y = i.y + i.h; }else{ o.w = i.x - o.x; o2.w += o2.x - (i.x + i.w); o2.x = i.x + i.w; }
続いて(3)
これもRECTは2つになりまが、2通りの分割が考えられます。
とりあえず、水平線で分割する事にしてみます。
重複しなかった辺は水平方向で1つ、垂直方向で1つ。
その水平方向の辺が分割のときの水平線になります。
まず水平線で2つに分割して、and領域を含む側について、
(1)のパターンで変形させればよさそうです
o2 = o; div_y = (o.y != i.y) ? i.y : i.y2; o.y2 = div_y; o2.y = div_y; if(o.y == i.y){ o と i で (1)の処理 }else{ o2 と i で (1)の処理 } w, h にすると o2 = 0; div_y = (o.y != i.y) ? i.y : i.y + i.h; o.h = div_y - o.y; o2.h += o2.y - div_y; o2.y = div_y; if(o.y == i.y){ o と i で (1)の処理 }else{ o2 と i で (1)の処理 }
最後の(4)
3つのRECTになります。
and領域で重複辺の向かいの辺。これでまず分割してみます。
分割後and領域を含む方について(2)のパターンで分割します
そう考えると、(3)を含めて共通の処理として、 水平か垂直で2分割する処理があるとよさそうです
処理(div) まず複製して o2 = o; 水平辺に重複があれば、その向かいの水平線で分割 なければ、垂直辺に重複があれば、その向かいの垂直線で分割 if(o.y == i.y || o.y2 == i.y2){ div = o.y != i.y ? i.y : i.y2; o.y2 = div; o2.y = div; }else{ /* o.x == i.x || o.x2 == i.x2 */ div = o.x != i.x ? i.x : i.x2; o.x2 = div; o2.x = div; } w, h になおすと if(o.y == i.y || o.y + o.h == i.y + i.h){ div == o.y != i.y ? i.y : i.y + i.h; o.y = div - o.y; o2.h += o.y - div; o2.y = div; }else{ /* o.x == i.x || o.x + o.w == i.x + i.w */ div == o.x != i.x ? i.x : i.x + i.w; o.w = div - o.x; o2.w += o.x - div; o2.x = div; } (3)は(div)を使って (div)の処理 if(o.y == i.y){ o と i で (1)の処理 }else{ o2 と i で (1)の処理 } そして(4)では (div)の処理 if(o.y == i.y || o.x == i.x){ o3 = o2; }else{ o3 = o; o = o2; } としておいて o と i で (2)の処理 結果は o, o2, o3
さて、そもそも2つのRECTのORでは、どちらをそのままにして、どちらを分割すべきでしょうか
先の4つのパターンを考えてみると、自ずと相手側の状況も決まるようです
(1) o-----------o | and | *-----------* | | | | | | +-----------+ 相手のRECTは、ほとんど(4)、まれに(3)、ごくまれに(1) (2) +-----------+ | | *-----------* | and | *-----------* | | +-----------+ 相手のRECTは、ほとんど(2)、まれに(1) (3) o-----*-----o | and | | *-----+ | | | | | | | +-----------+ 相手のRECTは、ほとんど(3)、まれに(1) (4) +-----------+ | | *------+ | | and | | *------+ | | | +-----------+ 相手のRECTは、かならず(1)
OR処理の総和として、なるべく分割しない方が好ましいでしょう。
なので(1)から(4)のパターンでは、より(1)側の分割無しを目指します。
2つのRECTに対して、and領域との関係を(1)から(4)に分類して、
分割が多い方を、分割せずそのままにして、
分割が少ない方を、分割するようにします
この方法で実現すれば、2つのRECTのOR領域を、重複しない複数のRECTで表現できて、 個別にクリップRECTとして設定して描画を繰り返したときに、 同じ領域を重複して描画する事はないので、ちらつきが軽減します
少々脱線してました
という事でまず、描画処理に一時的なクリップRECTを追加しておきます
グローバル変数でポインタを追加してNULLでなければ、
従来の親方向のクリップRECTに対し、さらにANDをとるようにします。
使った後は、設定した人が忘れずにNULLに戻しておくようにします
他、いつくか修正です。
cui.c cui_ly() に重大なtypoがありました。修正!
scpanel.c リサイズイベントで、
継承元のpanelも呼ばれるようにFALSEリターンに修正。
タイトルバーは判りやすいよう全体が反応するように修正
2つのRECTのOR関連の処理を入れて試してみます
cui_test.c で hogeタブのシートで、適当なRECTを2つ用意して、 ORをとってみて、分割具合を表示してみます
$ make $ ./cui_test ================ cui_test ================ |/foo|/bar|/hoge| | | | | File| [ ] Show (move) | | [foo ] | | | : 上、左、ENTER ================ cui_test ================ |/foo|/bar|/hoge| | | | | | | 1111111111 | | 1111111111 | | 1111111111 | | 0002222222222 | | 0002222222222 | | 2222222222 | | 2222222222 | | 2222222222 | | | : 左、左、fooタブフォーカス ENTER 下、Fileメニューフォーカス ENTER 下、下、下、ENTER result not yet $
それらしい感じで表示が出てます
お仕事が忙しすぎて、一ヶ月ほど間があいてしまいました。
自分で作ったプログラムを、思い出せるか心配しつつ、ひき続きチャレンジ
おそるおそる cui_xywh_set() の処理に、rectのORを入れてみます
変更前と変更後のRECTのORの領域を、クリッピングして、 親の入れものを描画したかったはずですね
親がNULLのrootの場合は、どうしましょう?
あきらめて、全体をクリアして、素直にcui_draw()する事にします
$ make $ ./cui_test ================ cui_test ================ |/foo|/bar|/hoge| | | | | File| [ ] Show (move) | | [foo ] | | | : 上、下、左、Showフォーカス ENTER ================ cui_test ================ |/foo|/bar|/hoge| | | | | File| [X] Show (move) | | [foo ] | | ============== title =============== | | |+--------------------+ +----------- | | || | |Radio = | | || Are you sure ? | | ( ) ON (= | | || | | = | | || (OK) (Cancel) | | = | | || | | = | | |+--------------------+ | = | | |+------------------+ +----------= | | || | | | | || How are you ? | | | | || | LONG| | | | || (^_^) (T_T) | | | | || | January| l| | | |+------------------+ - | | +|======================----------|+ | | | +----------------------------------------+ 下、右、上 で tilteフォーカス ENTER 上、下 で移動してみて ENTER 画面右下の内側にある方のリサイズボックスにフォーカスして ENTER 上下左右に動かしてみて ENTER うーむ。どんなもんなんだろうか... FileメニューからQuitで終了 result not yet $
ちらつきが減ってるような、はたまた、そうでもないような...
例のusleepで、スローモーションにして確かめてみます
void cui_draw(cui obj) { int val; if(obj == NULL) return; if(!cui_is_visible(obj)) return; val = (obj == cui_focus_get()) ? CUI_DRAW_FOCUS : CUI_DRAW_NORMAL; cui_handler_call(obj, CUI_EVT_DRAW, val); //usleep(20*1000); // for debug cui_draw_chain(obj->children); }
usleep有効にして、cui73 と cui75 で動作の違いを観察してみます
title をドラッグして移動してみると、確かに描画が改善されてます v(^_^)
久しぶりなので、カンをとりもどすために、ちょっと練習
簡単なテキストファイルのビューワを作ってみます。
と言っても、cui_test.c をちょっといじってみるだけです。
表示するファイルも cui_test.c に固定で試してみます
$ make $ ./cui_test ================ cui_test ================ |/foo|/bar|/hoge| | | | | File| [ ] Show (move) | | [foo ] | | | : 上、左、hogeフォーカス ENTER ================ cui_test ================ |/foo|/bar|/hoge| | |+----------------------------+ | ||#include <stdio.h> - | ||#include <string.h> = | ||#include <unistd.h> | | ||#include "cui.h" | | ||#include "esc.h" | | ||#include "handler.h" | | ||#include "label.h" | | ||#include "button.h" | | ||#include "ckbox.h" | | ||#include "radio.h" | | ||#include "dialog.h" | | ||#include "menu.h" | | ||#include "etext.h" - | |+|=-------------------------|+ | | | | | | | | | | | +----------------------------------------+ 下、右、上、vbarのdownボタンフォーカスで ENTER 連打 ================ cui_test ================ |/foo|/bar|/hoge| | |+----------------------------+ | ||cui - | ||joke_new(cui parent, int x, | | ||{ = | || cui lb = cui_label_n| | || cui b1 = cui_button_| | || cui b2 = cui_button_| | || cui_w_set(lb, cui_x2| | || cui_bind(b1, CUI_EVT| | || cui_bind(b2, CUI_EVT| | || return lb; | | ||} | | || | | ||int - | |+|=-------------------------|+ | | | | | | | | | | | +----------------------------------------+ 下、でリサイズボックスフォーカスで ENTER 右 x 4 下 x 4 でサイズを広げて ENTER ================ cui_test ================ |/foo|/bar|/hoge| | |+--------------------------------+ | ||cui - | ||joke_new(cui parent, int x, int | | ||{ = | || cui lb = cui_label_new(p| | || cui b1 = cui_button_new(| | || cui b2 = cui_button_new(| | || cui_w_set(lb, cui_x2(b2)| | || cui_bind(b1, CUI_EVT_BUT| | || cui_bind(b2, CUI_EVT_BUT| | || return lb; | | ||} | | || | | ||int | | ||my_hdr(cui obj, int evt, int val| | ||{ | | || cui joke; | | || cui_simple_dialog p = (c- | |+|=-----------------------------|+ | | | +----------------------------------------+ 左、でhbarのrightボタンフォーカスで ENTER連打 ================ cui_test ================ |/foo|/bar|/hoge| | |+--------------------------------+ | || - | || | | || = | ||ent, x, y, "system panic!"); | | ||, lb->w + 1, 0, "reboot"); | | ||, cui_x2(b1) + 1, 0, "power off"| | || | | ||N, joke_hdr, lb); | | ||N, joke_hdr, lb); | | || | | || | | || | | || | | ||void *prm) | | || | | || | | ||_simple_dialog)prm; - | |+|---------------=--------------|+ | | | +----------------------------------------+ fooにフォーカス ENTER Fileにフォーカス ENTER Quit で終了 result not yet $
動作は特に問題なさそうです
描画ハンドラの中で毎回 fopen, fclose してる適当なプログラムですが、 まともに動いてるようです
だいぶ忘れてるので、もう少しリハビリが必要かも
ここらで、CUIの管理下の中でターミナル(端末)を作って試してみます
どこかのキャラクタ端末の上で動いてるプログラムなのに、
その中に端末を作って、はたして意味があるのか?
という疑問はさておき...
まずとっかかりは、表示側から
ベースの入れものcui_baseを継承して、 表示サイズ分のリングバッファと、ラインバッファ、 カーソルのON/OFF状態と、カーソル位置、 リングバッファのスクロール位置、装飾の状態を追加して、 cui_termとします
cui_term_new(), cui_term_init(), cui_term_hdr() はいつもの、生成、初期化、ハンドラ処理
cui_term_buf_alloc(), cui_term_buf_free() でリングバッファ、ラインバッファの割り付け
cui_term_putc() で 1 文字出力
cui_term_puts() で 文字列出力
バッファの割り付けとして、生成時、初期化の中で、cui_term_buf_alloc() を内部的に呼び出してます。
破棄時は終了処理のAPIは何も用意してないので、必要があれば cui_term_buf_free() を呼ぶようにします
とりあえず、一発目のお試しなので、カーソル表示は未対応、装飾も未対応。
cui_xywh_set() でリサイズしたときの処理も未対応。
改行 '\n' と '\t' で 8の倍数カラムへの移動だけ入れてみます
cui_test.c に termタブを追加して、シートにテスト用のボタンと、 適当なサイズの cui_term を配置します
テストボタンのクリックで、カウンタの数値を20個づつ表示してみます
$ make $ ./cui_test ================ cui_test ================ |/foo|/bar|/hoge|/term| | | | | File| [ ] Show (move) | | [foo ] | | | : 上、左、でtermフォーカス ENTER ================ cui_test ================ |/foo|/bar|/hoge|/term| | | | |(test) | | | : 下、でtestフォーカス ENTER ================ cui_test ================ |/foo|/bar|/hoge|/term| | | | |(test) | |5 | |6 | |7 | |8 | |9 | |10 | |11 | |12 | |13 | |14 | |15 | |16 | |17 | |18 | |19 | | | | | | | +----------------------------------------+ v^_^) ENTER連打 ================ cui_test ================ |/foo|/bar|/hoge|/term| | | | |(test) | |165 | |166 | |167 | |168 | |169 | |170 | |171 | |172 | |173 | |174 | |175 | |176 | |177 | |178 | |179 | | | | | | | +----------------------------------------+ スクロールできてます (これはスクロールバーとは無関係) 上、fooフォーカス ENTER 下、Fileフォーカス ENTER Quitフォーカス ENTER で終了 result not yet $
続いてカーソルの表示/非表示などに対応してみます
カーソルとして反転の描画自体は、 term.c cui_term_hdr() に処理を追加でいいとして...
問題は、エスケープシーケンスでON/OFFを切替える対応です。
cui_term_putc() の中で ESCのコードを見張って、処理するしかなさそうです
esc.c でサポートしてるエスケープシーケンスのうち、 全部は無理なので、めぼしいものだけ対応することにします
+---+-+--------+--+-+--------------+ |ESC|[|? |25|h|カーソル表示 | | | | | +-+--------------+ | | | | |l|カーソル非表示| | | +--------+--+-+--------------+ | | |(y);(x)H|カーソル移動 | | | +--------+-------------------+ | | |0m |装飾全解除 | | | +--------+-------------------+ | | |4m |下線 | | | +--------+-------------------+ | | |7m |反転 | | | +--------+-------------------+ | | |24m |下線解除 | | | +--------+-------------------+ | | |27m |反転解除 | +---+-+--------+-------------------+
装飾対応として、アトリビュート用の領域をバッファを追加しておきます
これで何となく、いけそうかなと思えましたが、実装を考えてみると、色々とややこしさがありました
cui_term の画面は、hide/show したときなど、全画面を再描画します。
よって、出力した1画面分の装飾情報は、全部覚えておかねばなりません。
最下行で改行が入りスクロールするときも、全画面更新になります。
バッファへの出力と、バッファから画面への描画を、ちゃんと整理して考えねばなりません
ここまでの cui_term_putc(), cui_term_puts() の構成は単純で
+---------------+----------------------------------+ |cui_term_putc()|改行対応 | | 1文字出力 +----------------------------------+ | |タブ対応 | | +----------------------------------+ | |1文字をバッファへ書き込み | | +----------------------------------+ | |DRAWイベントで全画面更新 | +---------------+----------------------------------+ |cui_term_puts()|1文字づつ cui_term_putc() 呼び出し| | 文字列出力 | | +---------------+----------------------------------+
なだけになってます。
これを次のように処理を分けてみます
+-----------------------------+-------------------------------------------+ |cui_term_putc() |cui_term_putc_buf() 呼び出し | | 1文字出力 +-------------------------------------------+ | |cui_term_update() 呼び出し | +-----------------------------+-------------------------------------------+ |cui_term_putc_buf() |エスケープシーケンス処理 | | バッファへの1文字出力 +-------------------------------------------+ | |cui_term_putc_buf_inner() 呼び出し | +-----------------------------+-------------------------------------------+ |cui_term_putc_buf_inner() |改行対応 | | バッファへの1文字出力(内部) +-------------------------------------------+ | |タブ対応 | | +-------------------------------------------+ | |1文字をバッファへ書き込み | +-----------------------------+-------------------------------------------+ |cui_term_puts() |cui_term_puts_buf() 呼び出し | | 文字列出力 +-------------------------------------------+ | |cui_term_update() 呼び出し | +-----------------------------+-------------------------------------------+ |cui_term_puts_buf() |1文字づつ cui_term_putc_buf() 呼び出し | | バッファへの文字列出力 | | +-----------------------------+-------------------------------------------+ |cui_term_puts_buf_inner() |1文字づつ cui_term_putc_buf_inner()呼び出し| | バッファへの文字列出力(内部)|呼び出し | +-----------------------------+-------------------------------------------+ |cui_term_upate() |DRAWイベントで全画面更新 | | バッファの内容を描画 | | +-----------------------------+-------------------------------------------+
cui_term_update() のバッファの描画では、 バッファを更新した位置のアトリビュート用の領域に、 目印のフラグを立てておいて、 必要な領域だけ描画しなおすようにします
また、別途設けた全画面更新フラグを見て、 全画面更新のときは、アトリビュートのフラグは無視して、 有無を言わせず、全画面更新するようにします
遅いマシンで試してみると、スクロールのときに、画面のクリアがあまりにも遅かったです。
cui.c cui_fill_rect() を少し高速化しておきます
$ make $ ./cui_test ================ cui_test ================ |/foo|/bar|/hoge|/term| | | | | File| [ ] Show (move) | | [foo ] | | | : 上、下、termフォーカス ENTER ================ cui_test ================ |/foo|/bar|/hoge|/term| | | | |(test) | | | : 下、testフォーカス ENTER ================ cui_test ================ |/foo|/bar|/hoge|/term| | | | |(test) | |6 | |7 | |8 | |9 | |10 | |11 | |12 | |13 | |14 | |15 | |16 | |17 | |18 | |19 | | | | | | | | | +----------------------------------------+ ENTER ================ cui_test ================ |/foo|/bar|/hoge|/term| | | | |(test) | |26 | |27 | |28 | |29 | |30 | |31 | |32 | |33 | |34 | |35 | |36 | |37 | |38 | |39 | | | | | | | | | +----------------------------------------+ ENTER ================ cui_test ================ |/foo|/bar|/hoge|/term| | | | |(test) | |46 | |47 | |48 | |49 | |50 | |51 | |52 | |53 | |54 | |55 | |56 | |57 | |58 | |59 | | | | | | | | | +----------------------------------------+ ENTER ================ cui_test ================ |/foo|/bar|/hoge|/term| | | | |(test) | |66 | |67 | |68 | |69 | |70 | |71 | |72 | |73 | |74 | |75 | |76 | |77 | |78 | |79 | | | | | | | | | +----------------------------------------+ ENTER ================ cui_test ================ |/foo|/bar|/hoge|/term| | | | |(test) | |86 | |87 | |88 | |89 | |90 | |91 | |92 | |93 | |94 | |95 | |96 | |97 | |98 | |99 | | | | | | | | | +----------------------------------------+ ENTER ================ cui_test ================ |/foo|/bar|/hoge|/term| | | | |(test) | |87 | |88 | |89 | |90 | |91 | |92 | |93 | |94 | |95 | |96 | |97 | |98 | |99 | |Hello World | | foo bar | | | | | | | +----------------------------------------+ ENTER ================ cui_test ================ |/foo|/bar|/hoge|/term| | | | |(test) | |89 | |90 | |91 | |92 | |93 | |94 | |95 | |96 | |97 | |98 | |99 | |Hello World | | foo bar | |ULINE hoge REVERSE | | | | | | | | | +----------------------------------------+ ENTER ================ cui_test ================ |/foo|/bar|/hoge|/term| | | | |(test) | |cui cui_focus_get(void); | |void cui_focus_set(cui obj); | |void cui_focus_chk(void); | |int cui_running_get(void); | |void cui_running_set(int v); | | | |void cui_quit(void); | |void cui_main(cui top_obj, cui | | init_focus); | |void cui_del(cui obj); | |void cui_free_chain(cui obj); | |void cui_free(cui obj); | | | |#endif | | | | | | | | | +----------------------------------------+ ENTER (カーソルが消えて、デモ的な動作) : OKでごんす 上、fooフォーカス ENTER 下、Fileフォーカス ENTER Quitフォーカス ENTERで終了 result not yet $
デバッグの末、ようやく思惑どおりの動作でごんす
cui_term に対して、途中で cui_xywh_set() でリサイズしたときの対応をしてみます
term.c int cui_term_hdr(cui obj, int evt, int val, void *prm) { switch(evt){ case CUI_EVT_DRAW: hdr_update(obj); return TRUE; case CUI_EVT_RESIZE: // ... break; } return FALSE; }
ちゃんと場所を予約してましたね
基本的にバッファを内容をできるだけ保存しつつ、バッファのサイズを変更したいわけです
変更前のバッファから位置を指定して値を取り出すには、変更前のサイズ w, h が必要です。
が、リサイズイベントでハンドラが呼ばれたときには、w, h は更新後の値になってるので、時既に遅し。
変更前の値も欲しいので、バックアップ用のメンバを構造体に追加しておきます。
初期化のとき、リサイズの時に、w, h のバックアップをとるようにします
ここで問題。
アップデート時に更新のあった領域だけ描画してますが、
hide/show などで描画ハンドラが呼ばれたときは、当然全書換え。
ところが、バッファの更新時は、部分書換えをデフォルトとして、
スクロールによる全書換えは、特別な状態としてます。
不意にくる hide/show の時は、
ほとんどが、デフォルトの部分書換えの状態のはずです
うまい具合に、バッファ更新時の時は、cui_term_update() 経由で、
描画ハンドラを呼び出してました。
ここで不意にくる描画要求と、バッファ更新時の描画要求を、区別できます。
cui_term_update() からは、描画ルーチン hdr_update() を
直接呼び出すようにして、描画ハンドラ cui_term_hdr() では、
必ず全書換えに設定してから、描画ルーチンを呼び出すようにしておきます
cui_test.c で termタブのシート bs_term にリサイズボックスを追加して試してみます。
bs_term にリサイズハンドラ bs_term_hdr() を設定して、
その中で cui_term のリサイズをするようにしてみます
$ make $ ./cui_test ================ cui_test ================ |/foo|/bar|/hoge|/term| | | | | File| [ ] Show (move) | | [foo ] | | | : 上、下、termフォーカス ENTER ================ cui_test ================ |/foo|/bar|/hoge|/term| | | | |(test) | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | + | | | | | | | +----------------------------------------+ リサイズボックス '+' が見えてます 単独で付けてると、変な感じ... 右、右、右、下、testフォーカス ENTER リサイズボックスが消えましたが、 気にしないで ENTER連打 ================ cui_test ================ |/foo|/bar|/hoge|/term| | | | |(test) | |89 | |90 | |91 | |92 | |93 | |94 | |95 | |96 | |97 | |98 | |99 | |Hello World | | foo bar | |ULINE hoge REVERSE | | | | | | | | | +----------------------------------------+ この状態まできて 下、 ================ cui_test ================ |/foo|/bar|/hoge|/term| | | | |(test) | |89 | |90 | |91 | |92 | |93 | |94 | |95 | |96 | |97 | |98 | |99 | |Hello World | | foo bar | |ULINE hoge REVERSE | | + | | | | | | | +----------------------------------------+ リサイズボックスにフォーカスがきて、表示が戻りました ENTER、矢印キーで適当にドラッグして... ENTER 縮める方向に動かすと、一旦バッファは縮小されるので、 拡大しても、表示は無くなってます ================ cui_test ================ |/foo|/bar|/hoge|/term| | | | |(test) | |89 | |90 | |91 | |92 | |93 | |94 | |95 | |96 + | | | | | | | | | | | | | | | | | | | | | +----------------------------------------+ testフォーカス ENTERで続きを実行 ================ cui_test ================ |/foo|/bar|/hoge|/term| | | | |(test) | |); | |void cui_free_chain( | |cui obj); | |void cui_free(cui ob | |j); | | | |#endif | | | | | | | | | | | | | | | | | | | | | | | +----------------------------------------+ 狭い範囲でスクロールして表示が流れていきました ENTER 狭い範囲で、デモ的な描画 下、でリサイズボックスにフォーカスでENTER 右と下連打でサイズを拡大してENTER testフォーカス ENTER 広い範囲で、デモ的な描画 OKでごんす fooフォーカス ENTER Fileフォーカス ENTER Quitフォーカス ENTER で終了 result not yet $
ターミナルの中でシェルを起動できるか試してみます
となるとキー入力に対応せねばなりません
cui_term は表示出力用として固定し、手を加えず、 新たに cui_terminal を設けて、その中で出力用に cui_term を使う事にします
cui_terminal から pipe, fork, exec で /bin/sh を起動して、 pipeからのシェルの出力を表示用の cui_term に投げます
cui_terminal はキーフォーカスを受けるようにして、 キーイベントがくると、シェルへのpipeに渡すようにします
ん〜
まてよ、キーフォーカスの移動は矢印キーのイベント...
キー入力全部シェルに渡してたら、他の部品にフォーカス移動できないぞよ
「特別なキーコンビネーションで抜ける」とかもありですが、 ここは、「タイムアウト制」で試してみます
cui_terminal にキーフォーカスがきてる状態で、ENTERで、モーダルな状態に入れます。
この後のキー入力は、全てシェルに渡すようにします。
10秒ほどキー入力が無い状態が続けば、モーダルな状態を抜けて、
cui_terminal にキーフォーカスがきてる状態に戻します。
一旦、この案で試してみます
そして、出力側の接続も、今のままでは問題があります。
誰がシェルの出力をpipeから読み出す面倒をみるのか?
専用にプロセスやスレッドをあげるべしか?
それでもいいですが、その場合は排他処理をちゃんとしないとダメです
例えば、別の場所のボタンの描画の真最中に、 シェルからの出力を、別の実行単位から cui_term_puts() を呼び出したりすると、 cui.c cui_draw_str() が状態を持ってて、リエントラントになってないので、 表示がめちゃめちゃになる気がします...
安易な解決として、メインループから、
タイマーイベントとして、ユーザの登録ハンドラを呼び出すようにしてみます。
これなら、メインループの実行単位からの呼び出しなので、排他処理も不要です。
となると CUI_EVT_TIMER イベントを追加して、タイマー処理を実装せねば。
キーイベント以外で、初めて起点のトリガとなるイベントです。
果たしてうまくいくでしょうか? ドキドキ...
次のような仕様のタイマー処理を追加してみます
まず、タイマー処理の実装から
安易に cui.c にグローバル変数でリストを追加しようとして、
思い出しました。
メインループを多重に呼べるように、状態はスタックに持たせてたはず!
cui.c 冒頭の struct cui_main_stack に、
タイマー用のリストのメンバ追加するようにします
あー
でも、メイン関数実行した後しか、タイマーの設定できないのは、
いかがなものか...
ちょっと試すにも、メイン関数からのハンドラの中からしか、
タイマーが設定できないのは、あまりにも不便では...
苦肉の策として、タイマー設定APIに、リストのポインタを渡して、
その変数に一旦登録するようにします。
設定API側では、リストのポインタが指定されたら、そのリストに登録、
リストのポインタNULLが指定されてたら、メイン関数のスタック上のリストに登録します。
メイン関数を呼び出すときに、タイマー用のリストのポインタを渡して、
スタックのリストに設定します。
美しくないですが、これでいってみましょう
$ make $ ./cui_test 上、下、右、timerフォーカス ENTER ================ cui_test ================ |/foo|/bar|/hoge|/term|/timer| | | | |5 foo | | not yet | | | : 更新を確認しつつ 10秒経過で ================ cui_test ================ |/foo|/bar|/hoge|/term|/timer| | | | |10 foo | | foo | | | : 下の方のは、一度だけ foo に更新されて、以降変化なし OKでごんす fooフォーカス ENTER Fileフォーカス ENTER Quitフォーカス ENTER 終了 result not yet $
デバッグで格闘の末、タイマーがなんとかなりました
さて続いて、cui_terminal としての実装へ
道のりは長い...
ここで、デグレ発見!
エディットテキストのヒストリ・メニューを出そうとすると、
落ちるようになってました
================ cui_test ================ |/foo|/bar|/hoge|/term|/timer| | | | | File| [ ] Show (move) | | [foo ] | | foo|ERR cui_fill_rect() cui.c L148 : No Mem $
これは、ターミナルの装飾対応で入れた「cui.c cui_fill_rect() を少し高速化しておきます」の影響です。
cui77_78.patch の
diff -urN cui77/cui.c cui78/cui.c --- cui77/cui.c Sun Mar 23 22:00:00 2014 +++ cui78/cui.c Wed Mar 26 22:00:00 2014 @@ -135,12 +135,13 @@ { int ix, iy; int n = strlen(s); + char *buf; - for(iy=0; iy<h; iy++){ - for(ix=0; ix<w; ix+=n){ - cui_draw_str(obj, x+ix, y+iy, s, attr); - } - } + if((buf = malloc(w + 1)) == NULL) ERR("No Mem"); + for(ix=0; ix<w; ix+=n) buf[ix] = s[ix%n]; + buf[w] = '\0'; + for(iy=0; iy<h; iy++) cui_draw_str(obj, x, y+iy, buf, attr); + free(buf); }
cui_fill_rect() の引数 w の値が負 (-2) で来たときに、現象が出てました。
malloc() に負の値を指定して NULL が返ってた事が原因です
変更前のコードでは、w が0や負のときは、内側のループは1回も回らないので、
結果的に何もしていませんでした。
h が0や負のときも、同様に何もしてません。
なので、変更後も w または h が0以下なら、何もせずに返るように修正しておきます
さて、cui_terminal の実装。
前回がんばってタイマーを実装しました。
これは、2つの目的があって、1つはシェルからの出力をpipeでリードする処理を、
排他制御なしにしたいので、メインループの実行単位を使って、処理させたいから。
2つめは、シェルへのキー入力を「タイムアウト制」にして切替えたいからでした
よく考えてみたら、前者は、もっといい方法があります。
メインループからのキー入力は、selectシステムコールを使って待ってます
key.c int cui_key_get(void) { fd_set rfs; struct timeval tm; int ret; unsigned char uc; FD_ZERO(&rfs); FD_SET(KEY_FD, &rfs); tm.tv_sec = 0; tm.tv_usec = 100*1000; ret = select(KEY_FD+1, &rfs, NULL, NULL, &tm); if(ret <= 0) return 0; if(read(KEY_FD, &uc, 1) != 1) return 0; return uc; }
せっかくselect()使ってるのに、キー入力だけを見張らせるのはもったいないです。
ここで、pipeからリード可能になったら、リード可能になったよイベントで、
知らせてくれるようにすれば、タイムラグもなくすっきりしそうです
せっかくがんばってタイマーを実装したのに残念。
でもまだ後者の目的が残っているのでよしとします ;-p)
という事で、新たなイベント CUI_EVT_READABLE を追加してみます
現在の構成は、cui.c メインループ cui_main()から、 key.c cui_key_get2() --> cui_key_get() を呼び出し、 その中で select() してます
select() の呼び出しは key.c cui_key_get() から
cui.c cui_main() に移動して、メインループで直接 select() するようにします。
メインループで select() を抜けて、
キー入力のファイルディスクリプタがリード可能ならば、
cui_key_get2() を呼び出してキーの入力処理。
キーイベントの処理が終って、他の入力可能なディスクリプタについては、
CUI_EVT_READBLE のハンドラを呼び出す。
このように構成を変えてみます
となると、CUI_EVT_READBLE を受けたいオブジェクトの登録で、
また、タイマーのようにリストで保持...
となると、cui_main() の入れ子の呼び出し対応のため、
メインのスタックへの追加...
ここで、もう一度よく考えてみたら...!
タイマーのハンドラ呼び出しは、メインループがネストしてようが、
呼び出されて欲しいはずです。
なんでメイン関数ごとのスタックに載せてるのか?
そもそもメイン関数に載せてる情報とは?
cui.c struct cui_main_stack{ cui focus; int running; void *timer_list; cui_main_stack prev; };
focus はそのメイン関数がフォーカスしてる部品
これは、ネストしてるメイン関数から抜けてきたら、
以前にフォーカスしてる状態をキープしておきたいので必須
running はハンドラルーチンから、メイン関数を終了させるために使ってます。
このメンバ変数に値FALSEを設定すれば、メイン関数のループを抜けます。
これは、現在実行してるトップのメイン関数だけ抜けたいので必須
timer_list ...!
これはメイン関数ごとに必須とは言えません。
むしろ世界に一つの方がよさげです
なので、timer_list はグローバル変数で保持してれば十分でした
select で見張るファイルディスクリプタのリストについても、
メイン関数ごとに個別に持つ必要なさそうです。
とりあえず、別プロセスで実行してるシェルからの出力を、リードして処理したいわけなので、
何かモーダルダイアログでも出してる時でも、
シェルからの出力があれば、後ろでハンドラを呼び出して処理した方が、ありがたいはずです
ではまずタイマーのリストの整理から
そしてselect()の呼び出し箇所の移動
そして CUI_EVT_READBLE の追加。
cui.c に処理を追加します
タイマーイベントとリード可能イベントでは、
ハンドラの登録と、イベントの登録が分かれているので面倒です。
一度に設定できる、シュガーシンタックス(糖衣構文)を追加しておきます
そしてようやく cui_terminal の実装へ
cui_terminal の初期化時に、pipe, fork, execl でシェルを起動します
cui_terminal のイベントハンドラは、 表示イベント、キーイベント、リサイズイベント、タイマーイベント、 リード可能イベントと、イベントタイプの揃い踏みです
ここで select() ではまりました (>_<)
cui.c cui_main() のメインループ手間で、cui_select_init() してましたが、
これが、うっかりミスでした。
cui_main() を呼び出す前に、初期化段階で登録した、
シェルのパイプの登録がクリアされてしまい、
いつまで待っても、シェルからの出力がリード可能になりませんでした。
とほほ...
まだまだ、デバッグ中です
$ make $ ./cui_test [1]+ Stopped ./cui_test | Suspend するので fg ENTER ================ cui_test ================ |/foo|/bar|/hoge|/term|/timer| | | | | File| [ ] Show (move) | | $ o ] | | | : termのシートはhideのはずなのに、なんか見えてます... 上、下、termフォーカス ENTER ================ cui_test ================ |/foo|/bar|/hoge|/term|/timer| | | | |(test) | ||$ | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || + | | | | | | | +----------------------------------------+ 下 ================ cui_test ================ |/foo|/bar|/hoge|/term|/timer| | | | |(test) | |/$ / | |/ / | |/ / | |/ / | |/ / | |/ / | |/ / | |/ / | |/ / | |/ / | |/ / | |/ / | |/ / | |/ / | |/ + | | | | | | | +----------------------------------------+ これがキーフォーカスしてる表示 そして ENTER ================ cui_test ================ |/foo|/bar|/hoge|/term|/timer| | | | |(test) | |/$ / | |/ / | |/ / | |/ / | |/ / | |/ / | |/ / | |/ / | |/ / | |/ / | |/ / | |/ / | |/ / | |/ / | |/ + | | | | | | | +----------------------------------------+ これがモーダルな状態に入ったシェルにキー入力可能な状態 3秒で元に戻ります 元に戻ったら、再びENTER そして素早く ls ENTER ================ cui_test ================ |/foo|/bar|/hoge|/term|/timer| | | | |(test) | |/scpanel.h / | |/scpanel.o / | |/tab.c / | |/tab.h / | |/tab.o / | |/tbar.c / | |/tbar.h / | |/tbar.o / | |/term.c / | |/term.h / | |/term.o / | |/timer.c / | |/timer.h / | |/timer.o / | |/$ + | | | | | | | +----------------------------------------+
なんとか表示でてますが、不安定です...
ここらで気分を変えて一息いれます
画面でカーソル移動するプログラムなので、デバッグ時にデバッグプリントを入れると、 画面が乱れて訳がわからなくなります
今さらですが、デバッグ出力を他の画面に出すようにして、 少しでも容易にデバッグを出来るように改善してみます
netcat を使ってみます。
ネットワーク上の他のマシンにデバッグ出力を投げて表示するだけです
cui_test の 起動オプションに -dbg '<ホスト名> <ポート番号>' を指定しておけば、 そのマシンに netcat で TCP でつなぐようにします
-dbg だけ指定したときは 'localhost 12345' をデフォルトの指定とします
指定先の<ホスト名>のマシン側では、ncコマンドで待ちかまえておきます
$ while true ; do date ; nc -l -p <ポート番号> ; done
同じマシンの別の端末で
$ while true ; do date ; nc -l -p 12345 ; done
でもOKです
プログラムを試す前に、まず手動で確認
$ echo hello | nc <ホスト名> <ポート番号> 同じマシンなら $ echo hello | nc localhost 12345
待ち受け側に
<日付、時刻> hello <日付、時刻>
と表示が出ればOKです
デバッグ用の追加関数 +------------+------------------------------------+ |初期化処理 |int cui_dbg_open(int ac, char **av) | | | ac, av はメイン関数の引数を指定 | | | 返り値 0 : 正常終了 | | | 返り値 -1 : エラー | +------------+------------------------------------+ |終了処理 |void cui_dbg_close(void) | +------------+------------------------------------+ |デバッグ出力|void cui_dbg(char *fmt, ...) | | | パラメータは printf の指定形式 | +------------+------------------------------------+
起動してすぐ Suspend する現象は、
試すのに不便なので、とりあえず先に片付けておきます。
子プロセスで、シェルを対話モードで起動したとき、
標準入力待ちになって、親にシグナルを送るためでした。
子プロセスで setsid して、セッションリーダーとしてシェルを実行すると、
親にシグナルを送らなくなり、多少改善しました
cui_test.c main() にデバッグ用の初期化/終了処理を追加し、 term.c の cui_terminal_init() でデバッグプリントを追加して試してみます
$ make 同じマシンの別端末で $ while true ; do date ; nc -l -p 12345 ; done <日付、時刻> 元の端末に戻って $ ./cui_test -dbg ================ cui_test ================ |/foo|/bar|/hoge|/term|/timer| | | | | File| [ ] Show (move) | | /bin/sh: 0: can't access tty | | ; job control turned off | | $ | | | : デバッグ出力用の別端末に cui_terminal_init pipr_r=6 , pipe_w=5 表示出てます Fileフォーカス ENTER Quitフォーカス ENTER で終了 デバッグ出力用の別端末側は、ループを回って <日付、時刻> 次の接続を待ってます 他のマシンから $ ./cui_test -dbg '<ホスト名> 12345' で実行してもデバッグ出力は同様です
デバッグプリントは問題なさそうです
シェルは Suspend しなくなりましたが... 入出力は pipe じゃなくて、pty を使わないとダメですね
という事で cui_terminal_init() からシェルの起動では、 シェルの入出力に、疑似端末 (pty) を繋ぐように変更してみます
man pty によると、近頃のLinuxでは、posix_openpt, granpt, unlockpt, ptsname を使えばいいみたいです
ぐぐると、このあたりBSD系と違って面倒とか、出てきますね。
とりあえずLinuxの方式で試してみます。
_XOPEN_SOURCE を 600以上に定義しないと、ヘッダファイルで posix_openpt
などの宣言が有効にならないので注意
$ make $ ./cui_test ================ cui_test ================ |/foo|/bar|/hoge|/term|/timer| | | | | File| [ ] Show (move) | | $ o ] | | | : タブのシート突き破ってプロンプトの表示が見えてますね... 上、下、termフォーカス ENTER ================ cui_test ================ |/foo|/bar|/hoge|/term|/timer| | | | |(test) | ||$ | | || | | : 下、terminal枠フォーカス ENTER 3秒以内に ls ENTER (3秒経過してしまったら、そのままENTERして、ls ENTER) ================ cui_test ================ |/foo|/bar|/hoge|/term|/timer| | | | |(test) | |/btn.o label.h radio.h / | |/scpanel.h timer.h / | |/cui.h esc.h focu/ | |/s.c label.o radio.o / | |/scpanel.o timer.o / | |/cui.o esc.o focu/ | |/s.h lblfix.c rect.c / | |/tab.c / | |/cui_test esc_test.c focu/ | |/s.o lblfix.h rect.h / | |/tab.h / | |/cui_test.c etext.c hand/ | |/ler.c lblfix.o rect.o / | |/tab.o / | |/$ + | | | | | | | +----------------------------------------+ むむむ llss の表示が流れてしまいましたが、確かにそう出てました terminal枠フォーカス ENTER uname ENTER ================ cui_test ================ |/foo|/bar|/hoge|/term|/timer| | | | |(test) | |/s.c label.o radio.o / | |/scpanel.o timer.o / | |/cui.o esc.o focu/ | |/s.h lblfix.c rect.c / | |/tab.c / | |/cui_test esc_test.c focu/ | |/s.o lblfix.h rect.h / | |/tab.h / | |/cui_test.c etext.c hand/ | |/ler.c lblfix.o rect.o / | |/tab.o / | |/$ uunnaammee / | |/ / | |/Linux / | |/$ + | | | | | | | +----------------------------------------+ 確かに uunnaammee fooフォーカス ENTER Fileフォーカス ENTER Quitフォーカス ENTER で終了
ptyつないで対話モードでシェル起動すると、 デフォルトでエコーバック有りになってるようです
その方が助かります。
term.c cui_terminal_hdr() CUI_EVT_KEY処理で、
自前でエコーバックしてた cui_term_putc() を削除しておきます
実行結果を見直すと、行末にカーソルの表示が残ってる箇所があります。
term.c hdr_update() 末尾でカーソル表示として、スペースを反転表示してますが、
このときリングバッファの中身は、ほとんどの場合'\0'のままのはずです
カーソルが移動した後、前のカーソル位置もアップデートの対象に追加するようにして、 アップデート処理でも、リングバッファの内容が '\0' なら、 スペースとして処理するようにしておきます
表示されてる (test) ボタンも役目を終えたので、 cui_test.c から削除しておきます
cui_test.c みなおして、termのシートに追加してたリサイズボックスも、
cui_terminal に既にリサイズボックスを追加してたので削除。
termのシートbs_termは、最初から大きめのサイズにしておきます
あと、これまでは古いFreeBSDマシンでもちょっと試したりしてたのですが、
pty まわりは当然最新の Linux環境とは異なります。
こちらでも動くようにしておきたいので、その対応を入れてみます。
古いFreeBSD(なんと 4.2 Release)では、
pty の処理は forkpty() 一発で何もかもやってくれるようです。
ヘッダファイルは libutil.h で ライブラリ libutil をリンクします
変更は term.c だけで、Linuxとの処理の違いは
#if defined(__linux__) や #if defined(__FreeBSD__)
で切り分ければよさそうです
ここで Makefile の問題。
使ってる古いFreeBSD環境のときだけ、
cui_test のリンクで、-lutil オプションを追加したいわけです。
makeコマンドの中身は、Linuxでgmake, FreeBSDでpmake。
Linux の gmake なら
: OS=${shell uname} ifeq ($(OS), FreeBSD) LFLAS += -lutil endif :
FreeBSDのpmake でも通したいところですが...
ここは目をつむって FreeBSDでは gmake コマンドを使う事にして、
統一する事にします
$ make $ ./cui_test 上、下、termフォーカス ENTER 下、cui_terminal枠フォーカス ENTER uname ENTER ================ cui_test ================ |/foo|/bar|/hoge|/term|/timer| | | | /$ uname / | /Linux / | |/$ / | |/ / | : エコーバックは、問題なし やっぱりまだ行末にカーソルが残ってます ルートのScPanelの枠の一部が欠けてます 起動時にシート突き抜けプロンプト表示問題も残ったままです fooフォーカス ENTER Fileフォーカス ENTER Quitフォーカス ENTER で終了 古いFreeBSD環境でも試しておきます % uname -sr FreeBSD 4.2-RELEASE % % gmake gcc -Wall -c -o cui_test.o cui_test.c gcc -Wall -c -o cui.o cui.c gcc -Wall -c -o focus.o focus.c gcc -Wall -c -o etext.o etext.c gcc -Wall -c -o menu.o menu.c gcc -Wall -c -o dialog.o dialog.c gcc -Wall -c -o timer.o timer.c gcc -Wall -c -o term.o term.c gcc -Wall -c -o scpanel.o scpanel.c gcc -Wall -c -o tbar.o tbar.c gcc -Wall -c -o rszbox.o rszbox.c gcc -Wall -c -o scbar.o scbar.c gcc -Wall -c -o fillbtn.o fillbtn.c gcc -Wall -c -o tab.o tab.c gcc -Wall -c -o radio.o radio.c gcc -Wall -c -o ckbox.o ckbox.c gcc -Wall -c -o button.o button.c gcc -Wall -c -o lblfix.o lblfix.c gcc -Wall -c -o fill.o fill.c gcc -Wall -c -o label.o label.c gcc -Wall -c -o panel.o panel.c gcc -Wall -c -o handler.o handler.c gcc -Wall -c -o esc.o esc.c gcc -Wall -c -o key.o key.c gcc -Wall -c -o rect.o rect.c gcc -Wall -c -o dbg.o dbg.c gcc -o cui_test cui_test.o cui.o focus.o etext.o menu.o dialog.o timer.o term.o scpanel.o tbar.o rszbox.o scbar.o fillbtn.o tab.o radio.o ckbox.o button.o lblfix.o fill.o label.o panel.o handler.o esc.o key.o rect.o dbg.o -lutil % % ./cui_test 同じ操作で ================ cui_test ================ |/foo|/bar|/hoge|/term|/timer| | | | /$ uname / | /FreeBSD / | |/$ / | |/ / | 同じ画面 シェルでのuname実行結果だけが違います fooフォーカス ENTER Fileフォーカス ENTER Quitフォーカス ENTER で終了
さてデバッグ
起動時にプロンプトの表示が見える問題
なぜ hide の状態で term.c hdr_update_flush() からの cui_draw_str() で
表示が出てるのか?
cui.c cui_draw_str() 冒頭のビジブル・レクトの判定で、
はねられる事を期待してるのに?
早速デバッグプリントを使ってみるべし
hdr_update_flush() からの呼び出しのときだけ、
cui_draw_str() でデバッグプリントしたいので、
dbg.c cui_dbg_open() を改造しておきます。
変数 cui_dbg_enable 初期値 1 を追加します。
この変数が 0 に設定されてる期間は、デバッグプリントを出さないようにします。
起動オプションで -dbg-en0 が指定されていると、
cui_dbg_open() の初期化時に、変数 cui_dbg_enable の値を 0 にセットします
dbg.h に cui.h の MSGマクロ的なのを追加しておきます
dbg.h : #define cui_dbg_here(s) cui_dbg("%s() %s L%d : %s\n", __func__, __FILE__, __LINE__, s) : term.c : hdr_update_flush(cui obj, int *attr, char *lp, int x, int y) { : *lp = '\0'; cui_dbg_enable = 1; cui_draw_str(obj, x, y, p->lbuf, *attr & ~UPDATE_FLG); cui_dbg_enable = 0; *attr = -1; } cui.c : cui_draw_str(cui obj, int x, int y, char *s, int attr) { struct cui_rect t1, t2; cui_rect r1 = &t1, r2 = &t2; int n = strlen(s); char *s2 = NULL; cui_dbg_here("ck1"); if(!cui_visible_rect(obj, r1)) return; cui_dbg_here("ck2"); cui_rect_init(r2, x, y, n, 1); if(!cui_rect_and(r1, r2, r2)) return; /* r1 and r2 --> r2 */ cui_dbg_here("ck3"); if(cui_clip){ *r1 = *cui_clip; r1->x = cui_lx(obj, r1->x); r1->y = cui_ly(obj, r1->y); if(!cui_rect_and(r1, r2, r2)) return; } cui_dbg_here("ck4"); :
デバッグプリント表示用マシンで $ while true ; do date ; nc -l 12345 ; done <日付、時刻> としておいて $ make $ ./cui_test -dbg-en0 -dbg '<ホスト名> 12345' デバッグプリント表示用マシンに cui_draw_str() cui.c L108 : ck1 cui_draw_str() cui.c L110 : ck2 cui_draw_str() cui.c L113 : ck3 cui_draw_str() cui.c L120 : ck4 なー 見事にすり抜けてます なんで? cui_visible_rect() の返り値 FALSE で "ck1" は出ないはずなのでは? Fileフォーカス ENTER Quitフォーカス ENTER で終了
いや、まてよ...
cui.c cui_visible_rect() みると、親方向の flags の判定、
特に入ってないですな。
flags を直接みてる人は、cui_is_visble()。
cui_draw()では cui_is_visible() 呼び出して、はねてるけど、
cui_draw_str() では、RECTの判定しかしてない...
cui_draw_str() でも、先祖の flags 全部判定したいが、
そうなってないのは、何か理由があっただろうか?
問題なければ、cui_draw_str() から呼び出してる cui_visible_rect() 側で、
cui_is_visible() の判定を追加したいところ。
どうだったか?
cui_visible_rect() 呼び出し箇所を確認 $ grep cui_visible_rect *.[ch] cui.c: if(!cui_visible_rect(obj, r1)) return; cui.c:cui_visible_rect(cui obj, cui_rect r) cui.c: if(!cui_visible_rect(obj->parent, t)) return FALSE; cui.c: return cui_visible_rect(obj, &tmp); cui.h:int cui_visible_rect(cui obj, cui_rect r); $
cui_is_visible でサーチ $ grep cui_is_visible_rect *.[ch] cui.c: if(f && (!cui_is_visible(f) || !cui_is_visible_rect(f))) cui_focus_set(NULL); cui.c:cui_is_visible_rect(cui obj) cui.c: if(obj == cui_focus_get() && !cui_is_visible_rect(obj)) cui_focus_set(NULL); cui.h:int cui_is_visible_rect(cui obj); focus.c: if(!cui_is_visible_rect(obj)) return old; menu.c: if(obj2 && !cui_is_visible_rect(obj2)){ $
とりあえず問題は無いものの...
呼び出し箇所見て回ってみると、cui_visible_rect() は、
このままにしておくべきかと。
cui_draw_str() の冒頭に、cui_is_visible() 判定を追加するのが妥当です
cui_draw() の底の処理を、cui_draw_str() として後で分離したような
格好になってて、cui_draw_str() も外から直接呼べるようにしたと。
そこで、直接呼び出した時に、visible判定が抜けている状態のようです。
# 自分で書いたくせに他人事のような口ぶり...
$ make $ ./cui_test ================ cui_test ================ |/foo|/bar|/hoge|/term|/timer| | | | | File| [ ] Show (move) | | [foo ] | | | : 上、下、termフォーカス ENTER 下、cui_terminal枠フォーカス ENTER uname ENTER ================ cui_test ================ |/foo|/bar|/hoge|/term|/timer| | | | /$ uname / | /Linux / | |/$ / | |/ / | |/ / | |/ / | |/ / | |/ / | |/ / | |/ / | |/ / | |/ / | |/ / | |/ / | |/ + | | | | | | | | | +----------------------------------------+ fooフォーカス ENTER Fileフォーカス ENTER Quitフォーカス ENTER
cui.c cui_draw_str() へ cuiis_visible() 判定追加。
これで起動時のシート突き破りは、なんとかなりました
シェルからunameコマンド実行で cui_test の枠の一部が欠けるのはなぜか?
シェルで改行しただけでも現象が出ます
タイミングからして term.c cui_terminal_hdr()
リード可能イベント処理からの cui_term_puts() の処理でしょう。
cui_term_puts() では、cui_term_puts_buf() と cui_term_update() を呼んでるだけ。
cui_term_puts_buf() はリングバッファの更新だけなので、
おそらく cui_term_update() から。
cui_term_update() は hdr_update() の呼び出しだけ。
cui_draw_str() してるのは、末尾のカーソル処理と、
hdr_udpate_flush() でラインバッファを吐き出すとき
またしても、ここに来てしまった
hdr_upadte_flush() で cui_draw_str() 呼び出しをコメントアウトしてみます
起動して、同じキー操作で unameコマンドを実行
カーソル表示の残骸が増えるだけで、枠が欠ける現象はでませんね
ENTER連打しても、現象でません
どういう事でしょうか? とりあえず cui_draw_str() に渡してる引数 x, y の値を表示してみます
term.c : hdr_update_flush(cui obj, int *attr, char *lp, int x, int y) { : cui_dbg("x=%d y=%d\n", x, y); cui_draw_str(obj, x, y, p->lbuf, *attr & ~UPDATE_FLG); :
$ make $ ./cui_test -dbg '<ホスト名> 12345' 上、した、termフォーカス ENTER ================ cui_test ================ |/foo|/bar|/hoge|/term|/timer| | | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || $
落ちた (T_T)
デバッグアウト待ち受け側を、あげなおして、リトライするといけました
================ cui_test ================ |/foo|/bar|/hoge|/term|/timer| | | | ||$ | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || + | | | | | | | | | +----------------------------------------+ デバッグアウト出力マシン x=0 y=0 x=0 y=0 x=0 y=1 x=0 y=2 x=0 y=3 x=0 y=4 x=0 y=5 x=0 y=6 x=0 y=7 x=0 y=8 x=0 y=9 x=0 y=10 x=0 y=11 x=0 y=12 x=0 y=13 x=0 y=14 最初は全画面更新で、y=0から14までと... 下、cui_terminal枠フォーカス ENTER ENTER ================ cui_test ================ |/foo|/bar|/hoge|/term|/timer| | | | /$ / | |/$ / | |/ / | |/ / | |/ / | |/ / | |/ / | |/ / | |/ / | |/ / | |/ / | |/ / | |/ / | |/ / | |/ + | | | | | | | | | +----------------------------------------+ 現象は再現しつつ デバッグアウト出力マシンの表示の追加分は x=0 y=0 x=0 y=1 x=0 y=2 x=0 y=3 x=0 y=4 x=0 y=5 x=0 y=6 x=0 y=7 x=0 y=8 x=0 y=9 x=0 y=10 x=0 y=11 x=0 y=12 x=0 y=13 x=0 y=14 x=2 y=0 x=0 y=1 とりあえず終了って、ひっかかってる ... ^C で終了
描画イベントで全画面アップデートが入り、 x=2 y=0 は ENTER入力前のカーソルを消すためで、 x=0 y=1 は リード可能イベントから、新しいカーソル位置へのプロンプトの出力のため
座標自体は、問題ないみたいです
ラインバッファの中身をみてみるべしか?
term.c : hdr_update_flush(cui obj, int *attr, char *lp, int x, int y) { : cui_dbg("x=%d y=%d '%s'\n", x, y, p->lbuf); cui_draw_str(obj, x, y, p->lbuf, *attr &tmp; ~UPDATE_FLG); : $ make $ ./cui_test -dbg '<ホスト名> 12345' 上、下、termフォーカス ENTER : 落ちた (>_<) $ ps ax : .... /bin/sh -i : psをとってみるとゾンビだらけ シェルの終了をちゃんと看取ってないから... それはそれで、後回しとして、 $ ps ax | grep '/bin/sh -i' | cut -d ' ' -f1 | xargs kill -9 として、デバッグ続行 だめ、termフォーカス ENTERで落ちます うーむ。なんかデバッグプリントが出たりでなかったりする...? どうも問題の箇所の cui_draw_str() 呼び出しが、影響してる? とりあえずcui_draw_str()コメントアウトしたら、ラインバッファの内容出ないだろうか? term.c : hdr_update_flush(cui obj, int *attr, char *lp, int x, int y) { : cui_dbg("x=%d y=%d '%s'\n", x, y, p->lbuf); //cui_draw_str(obj, x, y, p->lbuf, *attr &tmp; ~UPDATE_FLG); : だめ term.c hdr_update_flush() 関数定義を static をとってグローバルにすると、表示でた??? x=0 y=0 '$ ' 最適化がらみ? まぁ、とりあえずデバッグを進めます 上、下、termフォーカス ENTER 下、枠フォーカス ENTER ENTER デバッグ出力の全体は x=0 y=0 '$ ' x=0 y=0 '$ ' x=0 y=1 ' ' x=0 y=2 ' ' x=0 y=3 ' ' x=0 y=4 ' ' x=0 y=5 ' ' x=0 y=6 ' ' x=0 y=7 ' ' x=0 y=8 ' ' x=0 y=9 ' ' x=0 y=10 ' ' x=0 y=11 ' ' x=0 y=12 ' ' x=0 y=13 ' ' x=0 y=14 ' ' x=0 y=0 '$ ' x=0 y=1 ' ' x=0 y=2 ' ' x=0 y=3 ' ' x=0 y=4 ' ' x=0 y=5 ' ' x=0 y=6 ' ' x=0 y=7 ' ' x=0 y=8 ' ' x=0 y=9 ' ' x=0 y=10 ' ' x=0 y=11 ' ' x=0 y=12 ' ' x=0 y=13 ' ' x=0 y=14 ' ' '2 y=0 ' x=0 y=1 '$ ' あら? デバッグ出力なのに x=2 の箇所が変? 先頭に復帰コード CR (0x0d) でも入ってるかのような...! シェルからの出力の末尾に CRLF ついてたら...そういう事か 確かめよう : cui_dbg("x=%d y=%d %02x\n", x, y, (unsigned)p->lbuf[0]); : うう、また、落ちる (>_<)
だいたい問題の現象の原因について、推測がついてしまってますが、 デバッグ用の関数が思ったように動作しない事のデバッグに、切り替わってます
デバッグのためのデバッグ。
popen() の先の nc へと出力した瞬間、おちたりおちなかったり...?
判りました。
cui_test.c main()関数で cui_dbg_open() 2回呼び出してました (>_<)
うっかり過ぎる〜
繰り返さぬよう dbg.c cui_dbg_open() に open済の判定入れときます
これでようやく安定してラインバッファの中身を見れるようになりました
: x=2 y=0 70 x=3 y=0 77 x=4 y=0 64 x=5 y=0 0d x=0 y=1 2f x=0 y=2 0d x=0 y=3 24 x=0 y=0 24 x=0 y=1 2f x=0 y=2 0d x=0 y=3 24 x=0 y=4 20 x=0 y=5 20 x=0 y=6 20 x=0 y=7 20 x=0 y=8 20 x=0 y=9 20 x=0 y=10 20 x=0 y=11 20 x=0 y=12 20 x=0 y=13 20 x=0 y=14 20
これです。
y=2 の出力で、ラインバッファの先頭にCR(0x0d)
今さらですが...
デバッグプリント出すと、
Quitフォーカス ENTERで、終了時にどこかでひっかかる事があります。
cui_dbg_close() からの pclose() あたりでしょうか...あ!
試してみると、これで終了時にひっかからないようになりました
さて戻って、デバッグ出力から原因の分析
シェルのプロンプト表示の状態から、"uname\n" を喰わせたとき、
"\r\nLinux\r\n$ " が返ってきてると考えられます。
リード可能イベント処理で、リードしてリングバッファを更新。
ここで、cui_term_putc_buf_inner() で '\n', '\t' は対応してますが、
'\r' はそのまま他の文字と同様に扱ってます。
なので、リングバッファのカーソル位置に '\r' 書き込みし、
'\n' で改行処理でカーソル移動、"Linux\r" をリングバッファに書き込み、
'\n' でカーソル移動、"$ " をリングバッファに書き込みます
hdr_update() で、更新されたリングバッファの内容を画面へと反映。
ここで、更新された領域のリングバッファの文字列を、
ラインバッファにコピーして、hdr_update_flush() から、
行単位で cui_draw_str() で描画します。
'\r' はそのまま渡っていって、cui.c cui_draw_str() の中で、
printf() で画面出力。
ここで、プログラムが動作してる端末の行頭にカーソル位置が移動する動作となって表れます。
cui_draw_str() の中では、0x0d という1文字を出力して、カーソルは一つ右へと移動したと思ってるのに、
実際にはカーソルは行頭に移動してて、そこには枠の'|'が表示されてます。
直後に以前のカーソル表示を消すための ' ' の描画が入ったとき、関係ない箇所の枠を消してしまう事になるのでしょう。
きっとそうに違いない
さあ、どのように対応すべきでしょう?
やはり端末なので、CRとLFで、復帰と改行。
復帰と改行を別々の動作として、
term.c cui_term_putc_buf_inner() で対応するのが良さそうに思えます
cui_term_putc_buf_inner() では '\n' の処理は、
cui_term_nl() を呼び出して、復帰、改行をしてます。
cui_term_nl() は、'\t'の処理で、画面の右端まで到達したときや、
通常の文字出力でも、画面の右端まで到達したときのみ、呼び出してます
cui_term_nl() では、
カーソル表示ONなら、カーソル移動に備えて、
その位置をアップデート対象に設定。
カーソルX位置を0にして、カーソルY位置を+1。
カーソルY位置が画面の下端なら、スクロール処理
なので、前半のカーソルX位置を0にするまではが「復帰」の処理、 カーソルY位置 +1 移行の後半が「改行」の処理です
復帰は cui_term_cr(), 改行は cui_term_lf()に分割して、 これまでの cui_term_nl() は復帰改行として、 cui_term_cr() と cui_term_lf() を呼び出すようにしてみます
cui_term_putc_buf_inner() からは、'\r' なら cui_term_cr()、 '\n' なら cui_term_lf() を呼び出すようにします
$ make $ ./cui_test 上、下、termフォーカス ENTER 下、枠フォーカス ENTER uname ENTER ================ cui_test ================ |/foo|/bar|/hoge|/term|/timer| | | | |/$ uname / | |/Linux / | |/$ / | |/ / | |/ / | |/ / | |/ / | |/ / | |/ / | |/ / | |/ / | |/ / | |/ / | |/ / | |/ + | | | | | | | | | +----------------------------------------+ fooフォーカス ENTER Fileフォーカス ENTER Quitフォーカス ENTER 無事終了 古いFreeBSDでも確認 % gmake % ./cui_test ================ cui_test ================ |/foo|/bar|/hoge|/term|/timer| | | | |/$ uname / | |/FreeBSD / | |/$ / | |/ / | |/ / | |/ / | |/ / | |/ / | |/ / | |/ / | |/ / | |/ / | |/ / | |/ / | |/ + | | | | | | | | | +----------------------------------------+ fooフォーカス ENTER Fileフォーカス ENTER Quitフォーカス ENTER 無事終了
カーソル表示が残る問題も、無事解消されてます v^_^)
CR対応の問題と連動した問題でした
ここでデグレです!
なんか、表示が変です
$ make $ cui_test 上、下、下、左、Showフォーカス ENTER ================ cui_test ================ |/foo|/bar|/hoge|/term|/timer| | | | | File| [X] Show (move) | | [foo ] | | ============== title =============== | | |+--------------------+ +----------- | | || | |Radio = | | || Are you sure ? | | ( ) ON (= | | || | | = | | || (OK) (Cancel) | | = | | || | | = | | |+--------------------+ | = | | |+------------------+ +----------= | | || | | | | || How are you ? | | | | || | LONG| | | | || (^_^) (T_T) | | | | || | January| l| | | |+------------------+ - | | +|======================----------|+ | | | +----------------------------------------+ ENTER ================ cui_test ================ |/foo|/bar|/hoge|/term|/timer| | | | | File| [ ] Show (move) | | [foo ] | | ============== title =============== | | |+--------------------+ +----------- | | || | |Radio = | | || Are you sure ? | | ( ) ON (= | | || | | = | | || (OK) (Cancel) | | = | | || | | = | | |+--------------------+ | = | | |+------------------+ +----------= | | || | | | | || How are you ? | | | | || | LONG| | | | || (^_^) (T_T) | | | | || | January| l| | | |+------------------+ - | | +|======================----------|+ | | | +----------------------------------------+ ShowのチェックボックスOFFで消えない!?
遡ると cui89 からこの状態です。
cui88_89.patch の内容を確認
cui.c + if(!cui_is_visible(obj)) return;
それらしい変更は、もうこれしかありません。
起動時にシートがhideでも、表示が出てしまう不具合の対策として、
cui_draw_str() に、flagsによる非表示の判定を追加しました。
画面を消去する処理で、cui_draw_str()がどう絡んでるでしょうか?
void cui_clear(cui obj) { cui_clear_rect(obj, 0, 0, obj->w, obj->h); } void cui_clear_rect(cui obj, int x, int y, int w, int h) { cui_fill_rect(obj, x, y, w, h, " ", CUI_ATTR_NORMAL); } void cui_fill_rect(cui obj, int x, int y, int w, int h, char *s, int attr) { int ix, iy; int n = strlen(s); char *buf; if(w <=0 || h <= 0) return; if((buf = malloc(w + 1)) == NULL) ERR("No Mem"); for(ix=0; ix<w; ix+=n) buf[ix] = s[ix%n]; buf[w] = '\0'; for(iy=0; iy<h; iy++) cui_draw_str(obj, x, y+iy, buf, attr); free(buf); }
確かに。
cui_draw_str() につながってます
そして
void cui_hide(cui obj) { if(obj->flags & CUI_FLG_HIDE) return; obj->flags |= CUI_FLG_HIDE; if(cui_esc_cnt > 0) cui_clear(obj); }
flagsのHIDEのビットを設定してから、画面をクリア。
そら、クリアしませんな。
単純に順番を入れ換えて、クリアしてから、フラグを設定してみます
$ make $ ./cui_test 上、下、下、左、Showフォーカス ENTER ENTER ================ cui_test ================ |/foo|/bar|/hoge|/term|/timer| | | | | File| [ ] Show (move) | | [foo ] | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | +----------------------------------------+ 起動時のシート突き破りもなく、ShowのOFFでも無事消えました 上、右、termフォーカス ENTER 左、左、左、fooフォーカス ENTER ================ cui_test ================ |/foo|/bar|/hoge|/term|/timer| | | | File| [ ] Show (move) | [foo ] | | | | | | | | | | | | | | | | | + 枠の一部が消えました 今度は消えすぎる現象
この現象は cui88 で試しても、再現します。
cui86 では再現しません。
cui87 では再現します。
では cui86_87.patch の内容を確認
どうやら cui_test.c が怪しいです。
そう。term用のシート bs_term のサイズを拡大しました。
例えば cui86 から 次の変更だけしても、枠が消える現象が再現します
--- cui86/cui_test.c Sun Mar 30 22:00:00 2014 +++ cui86-/cui_test.c Tue Apr 1 23:10:00 2014 @@ -420,7 +420,7 @@ cui sc3 = cui_scbar_new(bs_bar, cui_x2(sc)+2, sc->y, 1, 16, 4, 2); cui sc_hoge = cui_scpanel_new(bs, 1, 2, 30, 15, NULL); cui in_hoge = cui_scpanel_sheet_get(sc_hoge); - cui bs_term = cui_base_new(bs, 1, 2, 0, 0); + cui bs_term = cui_base_new(bs, 1, 2, 100, 100); cui btn_term = cui_button_new(bs_term, 0, 1, "test"); //cui term = cui_term_new(bs_term, 0, 2, 30, 15); cui term = cui_terminal_new(bs_term, 0, 2, 30, 15); @@ -470,7 +470,7 @@ cui_hide(bs_term); cui_bind(btn_term, CUI_EVT_BUTTON, btn_term_hdr, term); - cui_wh_fit(bs_term); + //cui_wh_fit(bs_term); cui_rszbox_new(bs_term); cui_bind(bs_term, CUI_EVT_RESIZE, bs_term_hdr, term);
タブの切替え処理、どうしてただろうか?
tab.c : int cui_tab_btn_hdr(cui obj, int evt, int val, void *prm) { : case CUI_EVT_BUTTON: sheet = (cui)prm; if(val) cui_show(sheet); else cui_hide(sheet); return TRUE; :
show/hide してるだけ。
cui.c cui_hide() なら、さっきみたクリア処理の流れで
cui_clear() cui_clear_rect() cui_fill_rect() cui_draw_str()
cui_draw_str() で先祖を遡って cui_visible_rect() とってきて、 cui_rect_and() で見えてるところだけのクリア処理になるはず
cui_test.c termのタブのシート bs_term は、 root の スクロールパネル bs にのっかってます
このスクロールパネルは、タイトルバーつきのパネルとして使ってるだけで、
スクロールバーは表示してない状態。
このスクロールバーの構成は scpanel.h, scpanel.c ...
あぁ。わかった。
scpanel は panelから継承してて、パネルの上には、
これらの部品を配置
cui_test.c では、複数のタブのシートを、
cui_scpanel の view にのっけないで、パネル自身に直接のっけてました。
クリップ領域はパネルの枠一杯まで含まれます。
中に載せてる bs_term が大きいときに、枠が消える現象は、
入れものである scpanel の範囲を超えてクリアしてる訳ではなく、
scpanel のパネルの枠一杯まで使って、クリアされるためでした。
ちょうど、ぎりぎり枠の部分までクリアされてた訳です
という事で、cui_test.c で scpanel にのってる部品は、 scpanel の view にのっけるよう修正します
$ make $ ./cui_test 上、下、termフォーカス ENTER 左、左、左、fooフォーカス ENTER ================ cui_test ================ |/foo|/bar|/hoge|/term|/timer| | | | | File| [ ] Show (move) | | [foo ] | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | +----------------------------------------+ 一件落着です
ターミナルのシェルで exit を入力すると
================ cui_test ================ |/foo|/bar|/hoge|/term|/timer| | | | |/$ exit / | |/ ERR cui_terminal_hdr() term.c L530 : read
と落ちてしまいました。
表示されてる場所は、cui_terminal のイベントハンドラ内。
リード可能イベントの処理で read() が負の値を返し、落ちます
case CUI_EVT_READABLE: if((n = read(p->pipe_r, buf, 1024-1)) < 0) ERR("read");
子プロセスが終了したら、その口のfdを見張る select() の結果はどうでしょうか?
cui.c int cui_select(void) { int nfds = 0, fd; struct timeval tm; res_rfs = sel_rfs; for(fd=0; fd<FD_SETSIZE; fd++) if(FD_ISSET(fd, &res_rfs) && fd > nfds) nfds = fd; tm.tv_sec = 0; tm.tv_usec = 100*1000; return select(nfds+1, &res_rfs, NULL, NULL, &tm); } void cui_main(cui top_obj, cui init_focus) { : while(cui_running_get()){ cui_timer_work(); if(cui_select() <= 0) continue; cui_readable_work(); :
このコードを見るかぎり、select() の返り値は正の値を返してるはずです
static void cui_readable_work(void) { cui_readable_list p = readable_list; for(; p; p=p->next){ if(!cui_select_chk(p->fd)) continue; cui_handler_call(p->obj, CUI_EVT_READABLE, p->fd); } } int cui_select_chk(int fd) { return FD_ISSET(fd, &res_rfs); }
そして、グローバル変数 res_rfs に残された結果には、 シェルの口の fd が、リード可能と記録されているからこそ、 cui_terminal のイベントハンドラが呼びだされてるはずです
イベントハンドラでリードしてみると、 シェルは終了してるので、その口のfdはクローズされているという事でしょうか
ならばハンドラの read() の結果で対処すればいいのかも知れません。
read() で返る負の値と、errno を見てみます
term.c #include <errno.h> : case CUI_EVT_READABLE: //if((n = read(p->pipe_r, buf, 1024-1)) < 0) ERR("read"); if((n = read(p->pipe_r, buf, 1024-1)) < 0){ cui_dbg("n=%d %s", n, strerror(errno)); cui_readable_del(obj, p->pipe_r); return TRUE; } : などとして $ make デバッグ用の端末で $ nc -l -p 12345 元の端末に戻って $ ./cui_test -dbg termタブからシェルで exit ENTER デバッグ用の端末には n=-1 Input/output error
ふーむ。
ふと、古いFreeBSDで試してみると動作が異なりました。
なにやらカーソル位置の表示が、ひたすら更新され続けた状態になってます。
ということは、リード可能になっていて、しかも read() で 0 か正の値が返ってき続けてるはずです。
カーソルだけで何も表示が出てないということは 0 が返って、cui_term_puts() に空文字列を指定してる?
だとしたら、cui_term_puts_buf() の更新では何もしてなくて、
cui_term_update() から hdr_upadte()で 末尾のカーソル表示の処理だけがなされると。
どうやら、この状態っぽいです。
古いFreeBSDの場合、シェルの口のfdがクローズで、select()でリード可能になるのはLinuxと同じ。
リードしてみると 0 が返るところが違います。
エラーなら、その口はもう使えないのでは? ってなりますが、0 は微妙な気がします。
口の先にシェルが居てるかどうかの判定材料になりうる?
でもまぁ、それ以前に子プロセスが死ぬとシグナルが来るでしょうが。SIGCHLDが。
まずこちらを確かめるべきかと
SIGCHLDシグナルのハンドラを設定して、 wait()で看取って、リード可能イベント登録も解除するようにしてみました
シェルで exit した後 3 sec 待って、フォーカス戻ってから、
foo/File/Quit で終了。
この対応で、Linuxでも古いFreeBSDでも、とりあえずソンビは残らなくなりました
ここらで気分を変えて一息いれます
リストといっても、部品としてのリスト・アイテムとかではありません。
# いづれ作ろうかとは思いますが...
イベント・ハンドラのリスト構造体を始め、
直近では term.c のシェル実行中のobjのリスト。
どれも、add()して、サーチして使って、del()。その度に、同じようなコードが増えてきてます。
使いやすそうな汎用的なのを一つ用意して、全部それに置き換えてみる事にします
使う側で根本のポインタを用意して初期値NULLにします。
この根本のポインタのアドレスを引っ張りまわす関数を用意して、ノードの追加と削除を行ないます。
1つのノードとして、保持したいデータのサイズ分を含めてallocして、1方向のチェーンで繋ぐようにします。
中身の設定や検索は、使う側で個別にするとして、
使い終ったらノードを指定して、リストから削除するようにします
APIは次のような感じでしょうか typedef struct cui_list *cui_list; struct cui_list{ cui_list next; char data[0]; }; cui_list cui_list_add(cui_list *root, int data_size); void cui_list_del(cui_list *root, cui_list p);
list.c として実装してみます
cui_list cui_list_add(cui_list *root, int data_size) { cui_list p = malloc(sizeof(struct cui_list) + data_size); if(p == NULL) ERR("No Mem"); p->next = *root; *root = p; return p; } void cui_list_del(cui_list *root, cui_list p) { cui_list t = *root, *prev=root; for(; t; prev=&t->next, t=t->next) if(t == p) break; if(t == NULL) return; *prev = t->next; free(t); }
果たして便利に使えるものなのか?
まず直近の term.c の trm_list から置き換えて試してみます
あー
コーディングしてみると add() では、listの構造体のアドレスを返すより、
data部分の先頭アドレスの方が便利そうです。
ということで仕様変更
typedef struct cui_list *cui_list; struct cui_list{ cui_list next; char data[0]; }; void *cui_list_add(cui_list *root, int data_size); void cui_list_del(cui_list *root, void *data);
void * cui_list_add(cui_list *root, int data_size) { cui_list p = malloc(sizeof(struct cui_list) + data_size); if(p == NULL) ERR("No Mem"); p->next = *root; *root = p; return p->data; } void cui_list_del(cui_list *root, void *data) { cui_list p = *root, *prev=root; for(; p; prev=&p->next, p=p->next) if(p->data == data) break; if(p == NULL) return; *prev = p->next; free(p); }
動かしてみて特に問題なさそうです
他のリストを使ってそうな箇所は $ grep list *.[ch] | cut -d ':' -f1 | uniq cui.c cui.h dbg.c handler.c handler.h list.c list.h term.c timer.c timer.h $
次はタイマーのリストをcui_listを使うように変更してみます
動かしてみると問題ありました (>_<)
Segmentation faultで落ちます
timer.c void cui_timer_work(void) { cui_list lp = timer_list; for(; lp; lp=lp->next){ cui_timer p = (cui_timer)lp->data; int msec = cui_timer_now_msec(); if(msec < p->targ_msec) continue; cui_handler_call(p->obj, CUI_EVT_TIMER, msec); if(p->once) cui_list_del(&timer_list, p); else p->targ_msec += p->msec; } }
cui_list_del() で p を指定してますが、
実は lp ごと free() されているという罠!
そういう仕様にしましたが、分かりにくいかもしれません。
ついうっかり del() の後も lp->next で
使おうとしてました。
とりあえず仕様はそのままで、呼び出し側で対処
お次は cui.c の cui_readable_list
シェルで exit してみると、これまた、Segmentation fault で落ちました(>_<)
これは、むつかしかった...
cui.c cui_readable_work() の中の cui_handler_call() から、
term.c の cui_terminal_hdr() でリード可能イベントで、
シェルからの出力をreadして描画を更新してる時に、
シェルは exit で終了。
シグナル SIGCHLD で term.c sig_hdr() から、
cui.c cui_readable_del() 呼び出しで、readable_list から削除。
シグナル処理が終って、cui.c cui_readable_work() の forループに戻り、
リード可能ハンドラ呼び出しから返ってくると、現在の lp は削除済。
lp->next アクセスで Segfault
リストのループはもう、要注意です。
next参照時は、freeされてないか必ず確認しないとだめかも。
あるいはもう、最初に next のバックアップをとるようにしてしまうか...
残るは cui_handler_list
cui.c cui_free() では cui_handler_list_free() を使って、
ハンドラのリスト全部を解放してました。
list.[ch] に全削除のAPIを追加しておきます
ターミナルのシェルからコマンドの実行を試してると、 バックスペースが効いてない事に気付きました
================ cui_test ================ |/foo|/bar|/hoge|/term|/timer| | | | |/$ lss^H / | |//bin/sh: 1: ls: not found / | |/$ / | |/ / | :
よく使う ^U はどうでしょうか?
================ cui_test ================ |/foo|/bar|/hoge|/term|/timer| | | | |/$ lss^H / | |//bin/sh: 1: ls: not found / | |/$ hoge / | |/ / | : ここで ^U で取り消して ================ cui_test ================ |/foo|/bar|/hoge|/term|/timer| | | | |/$ lss^H / | |//bin/sh: 1: ls: not found / | |/$ / | |/ / | : カーソルが残りつつ、2つ表示されて ENTER ================ cui_test ================ |/foo|/bar|/hoge|/term|/timer| | | | |/$ lss^H / | |//bin/sh: 1: ls: not found / | |/$ / | |/$ / | |/ / | : カーソルが残ってるものの、エラーメッセージは出ません hogeの入力は取り消されているようです
sttyコマンドで確認してみます
: |/$ stty -a | grep kill / | |/intr = ^C; quit = ^\; erase / | |/= ^?; kill = ^U; eof = ^D; e/ | |/ol = <undef>; / | |/$ / | :
kill文字は ^Uで、erase文字は ^H になってません。
ということは...
: |/$ stty erase ^H / | |/$ lss / | : ^H は '^' 'H' とキー入力してます この状態から、バックスペース・キーで : |/$ stty erase ^H / | |/$ ls / | : カーソル表示が乱れるもの、末尾の 's' は消えました ENTERで : |/cui_test.o etext.o key./ | |/c menu.c rszbo/ | |/x.h tbar.o / | |/$ + | : ls が実行できました
実際のところ ^U や ^H の入力で、シェルからの出力は何が返ってるのか、みてみます
term.c cui_terminal_hdr() 末尾の箇所で : case CUI_EVT_READABLE: if((n = read(p->pipe_r, buf, 1024-1)) < 0){ return TRUE; } buf[n] = '\0'; cui_dbg(buf); cui_term_puts(p->term, buf); return TRUE; } return FALSE; } $ make デバッグ用の端末で $ nc -l -p 12345 | hd 元の端末で $ ./cui_test -dbg termタブで ================ cui_test ================ |/foo|/bar|/hoge|/term|/timer| | | | |/$ lss / | : と入力して バックスペース・キー ENTER ================ cui_test ================ |/foo|/bar|/hoge|/term|/timer| | | | |/$ lss^H / | |//bin/sh: 1: ls: not found / | |/$ / | : デバッグ用の端末に : 00000000 24 20 6c 73 73 5e 48 0d 0a 2f 62 69 6e 2f 73 68 |$ lss^H../bin/sh| 00000010 3a 20 31 3a 20 6c 73 73 08 3a 20 6e 6f 74 20 66 |: 1: lss.: not f| まだ出きってないですが バックスペース・キーのところは 0x5e'^'と0x48'H' 次に stty erase ^H を設定して同じことをすると : |/$ stty erase ^H / | |/$ lss / | : バックスペース・キー ENTER : |/c menu.c rszbo/ | |/x.h tbar.o / | |/$ / | : lsが実行できて デバッグ用の端末には : 00000340 2e 63 20 20 74 62 61 72 2e 68 0d 0a 63 75 69 5f |.c tbar.h..cui_| 00000350 74 65 73 74 2e 6f 20 20 65 74 65 78 74 2e 6f 09 |test.o etext.o.| 00000360 6b 65 79 2e 63 09 20 20 20 6d 65 6e 75 2e 63 20 |key.c. menu.c | 00000370 20 20 20 20 20 72 73 7a 62 6f 78 2e 68 20 20 74 | rszbox.h t| : しまった '| less' でつないどくべきでした $ nc -l -p 12345 | hd | less でリトライ 00000000 24 20 73 74 74 79 20 65 72 61 73 65 20 5e 48 0d |$ stty erase ^H.| 00000010 0a 24 20 6c 73 73 08 20 08 0d 0a 4d 61 6b 65 66 |.$ lss. ...Makef| 00000020 69 6c 65 20 20 20 20 64 62 67 2e 63 09 66 69 6c |ile dbg.c.fil| : 問題の箇所は 0x6c'l' 0x73's' 0x73's' 0x08'\b' 0x20' ' 0x08'\b' 0x0d'\r' 0x0a'\n' : となってます
'lss' から 0x08 でカーソル1つ左に戻して、スペース上書きで's'を消して、 さらに 0x08 でカーソル1つ左に戻してます
term.c のリングバッファへの出力と、リングバッファから画面 への描画はどうなってるかというと、 0x08 の '\b' に対応した覚えなどないので
こうなってるはずです
さて、term.c cui_term_putc_buf_inner() で '\b' 対応の追加ですが、 pty+シェルからの出力が、ここまでやってくれるなら、 カーソル移動だけで、リングバッファの削除は手抜きできる気がします ;-p)
^U もみてみます。
hoge^U ENTER のキー入力。
さてデバッグ用の端末が...、なかなか更新されません (T_T)
cui_test 側で ENTER 連打でもだめ。
'ls ENTER' を繰り返してみると flush されました
: 000006b0 0a 24 20 0d 0a 24 20 68 6f 67 65 08 20 08 08 20 |.$ ..$ hoge. .. | 000006c0 08 08 20 08 08 20 08 0d 0a 24 20 0d 0a 24 20 0d |.. .. ...$ ..$ .| :
問題の ^U 箇所です
なんと、'\b' + スペース + 'b' のセットを4回繰り返して、h,o,g,e の4文字を消してくれてます
これならますます cui_term_putc_buf_inner() で '\b' は、 カーソル移動だけの手抜きでよさそうですね
キー絡みといえば、term.c cui_terminal_hdr() CUI_EVT_KEY 処理では、
シェルへの出力として、 ENTERキー '\r' を'\n' に変換してました。
これは cui83_84.patch でそのようにしてました。
この段階では pty じゃなく、直接 pipe でつないでシェルを起動してたので、
この変換が必要だったのかも知れません。
pty をかましてる今の状態では、ENTERキーで '\r' 送れば大丈夫です。
変換の行を削除しておきます
さらにキー絡み。
端末限定じゃなくてCUI全体のキー入力についてになりますが、
矢印キーについて、emacsのように ^N, ^P, ^B, ^F も使いたいです。
一応 emacs というか、いまだに mule ばかり使ってたりします
key.h : #define CUI_KEY_ENTER 0x0d /* '\r' */ #define CUI_KEY_BS 0x08 /* '\b' */ #define CUI_KEY_UP 0x11 /* DC1 */ #define CUI_KEY_DOWN 0x12 /* DC2 */ #define CUI_KEY_LEFT 0x13 /* DC3 */ #define CUI_KEY_RIGHT 0x14 /* DC4 */ #define CUI_KEY_ESC 0x1b :
とりあえず矢印キーの割り当てを、このようにしてました。
この割り当てを、^N, ^P, ^B, ^F のコードにしてしまえば、
そのまま矢印キーも有効で、^N, ^P, ^B, ^F も有効になりそうです
コードを調べてみます
$ make key_test 懐かしい... $ ./key_test で起動して、^N, ^P, ^B, ^F を入力すると 0e 10 02 06 と表示。'q'キーで終了
^A が 0x01、^B が 0x02、.... という事ですね
key.h で、割り当てを変えてみます : #define CUI_KEY_CTRL(c) (0x01+(c)-'A') #define CUI_KEY_UP CUI_KEY_CTRL('P') #define CUI_KEY_DOWN CUI_KEY_CTRL('N') #define CUI_KEY_LEFT CUI_KEY_CTRL('B') #define CUI_KEY_RIGHT CUI_KEY_CTRL('F') :
term.c で cui_terminal_init() でシェルを起動後、
親の方から "stty erase ^H" の実行を追加してみます。
# 何かもっといい方法があるような気がしますが...
$ make $ ./cui_test termタブのシートを表示すると ================ cui_test ================ |/foo|/bar|/hoge|/term|/timer| | | | |/stty erase ^H / | |/$ $ / | |/ / | : ENTER、hogehogeと入力して : |/$ hogehoge / | |/ / | : バックスペース・キー : |/$ hogehog / | |/ / | : よしよし。^H でも : |/$ hogeho / | |/ / | : ^U だと : |/$ / | |/ / | : 3秒まってフォーカスが枠になってから ^P ================ cui_test ================ |/foo|/bar|/hoge|/term|/timer| | | | ||stty erase ^H | | ||$ $ | | : ^B ^F ^N でフォーカス移動します もちろん、これまで通り矢印キーでも移動します
さて、ターミナルを整備して、そろそろ準備が整ってきました
実は試してみたかったのは、ターミナルの中から cui_test コマンドの実行です
$ make $ ./cui_test ================ cui_test ================ |/foo|/bar|/hoge|/term|/timer| | | | | File| [ ] Show (move) | | [foo ] | | | : 少しサイズを広げておきます 下、リサイズボックス フォーカス ENTER 下 x 4、右 x 8 ENTER ==================== cui_test ==================== |/foo|/bar|/hoge|/term|/timer| | | | | File| [ ] Show (move) | | [foo ] | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | +------------------------------------------------+ 上、左 termフォーカス ENTER ターミナルのサイズも広げておきます 下、下、ターミナルのリサイズボックス フォーカス ENTER 下 x 8、右 x 16 ENTER ==================== cui_test ==================== |/foo|/bar|/hoge|/term|/timer| | | | ||stty erase ^H | | ||$ $ | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || + | +------------------------------------------------+ 上、ターミナルの枠フォーカス ENTER ./cui_test ENTER ==================== cui_test ==================== |/foo|/bar|/hoge|/term|/timer| | | | |/================ cui_test ================ / | |/|/foo|/bar|/hoge|/term|/timer| | / | |/| | / | |/| File| [ ] Show (move) | / | |/| [foo ] | / | |/| | / | |/| | / | |/| | / | |/| | / | |/| | / | |/| | / | |/| | / | |/| | / | |/| | / | |/| | / | |/| | / | |/| | / | |/| | / | |/| | / | |/| | / | |/| | / | |/| | / | |/+----------------------------------------+ + | +------------------------------------------------+ おっしゃー! でたでた 表示はネストしてますが、別プロセスによる実行です キー入力の3秒ルールが、じれったいですが、落ち着いてENTERを押せば、続きの操作ができます ENTER 上、下、左、これで中の方のcui_testの Showフォーカス ENTER ==================== cui_test ==================== |/foo|/bar|/hoge|/term|/timer| | | | |/================ cui_test ================ / | |/|/foo|/bar|/hoge|/term|/timer| | / | |/| | / | |/| File| [X] Show (move) | / | |/| [foo ] | / | |/| ============== title =============== | / | |/| |+--------------------+ +----------- | / | |/| || | |Radio = | / | |/| || Are you sure ? | | ( ) ON (= | / | |/| || | | = | / | |/| || (OK) (Cancel) | | = | / | |/| || | | = | / | |/| |+--------------------+ | = | / | |/| |+------------------+ +----------= | / | |/| || | | | / | |/| || How are you ? | | | / | |/| || | LONG| | | / | |/| || (^_^) (T_T) | | | / | |/| || | January| l| | / | |/| |+------------------+ - | / | |/| +|======================----------|+ | / | |/| | / | |/+----------------------------------------+ + | +------------------------------------------------+ さらに中の人のtermを開いて、さらに./cui_testを実行 ==================== cui_test ==================== |/foo|/bar|/hoge|/term|/timer| | | | |/================ cui_test ================ / | |/|/foo|/bar|/hoge|/term|/timer| | / | |/| | / | |/|/================ cui_test ==/ | / | |/|/==============how (move) / | / | |/|/| [foo ] | / | / | |/|/| | / | / | |/|/ / | / | |/|/+ + / | / | |/|/ + / | / | |/|/ + / | / | |/|/ / | / | |/|/ / | / | |/|/ / | / | |/|/ / | / | |/|/ / | / | |/|/ / | / | |/|/ + | / | |/| | / | |/| | / | |/| | / | |/| | / | |/+----------------------------------------+ + | +------------------------------------------------+ うーむ。なんとなくフラクタル リサイズで広げてなかったので、中の中の人の表示は、乱れてます 外側のfooフォーカス ENTER Fileフォーカス ENTER Quitフォーカス ENTER 無事終了できました
丁度 cui100 なので、ここまでのソースコードをかためておきます
ここまでのソースファイル cui100.tgz
ちょっと 「SMFを読み込み音の波形データを作るプログラム (C言語)」 の方から、使って試してみたかったのでライブラリを作るようにしておきます
と言っても大した事ありません。Makefile をほんの少し修正してるだけです
「SMFを読み込み音の波形データを作るプログラム (C言語)」 で CUIから音出ししてみる を少し試してみたところで、 整数を入力する操作がとても面倒だと判りました
etext.[ch] cui_etext を元にして、 num.[ch] cui_num として、 整数の数値入力が便利に出来る部品を用意してみます
とりあえず符号付き整数で試してみます
cui_etext に文字列が入力され ENTER で決定したタイミングで、 ボタン・イベントがくるので、そこで数値として妥当な文字列か判定し、 設定されてる、最小値、最大値の範囲内に収まるよう、 補正をかけてみます
cui_etext の両側に -1, +1 するためのボタンを、
右側にスクロールバーをポップアップ表示するためのボタン'|'を追加してみます。
スクロールバーの表示を消して戻るには、スクロールバーの上に追加してる'x'ボタンを押します
最小値、最大値として、同じ値を指定したら、
int 全体の範囲をカバーする仕様としたかったのですが、
スクロールバーの最大値の指定が、符号付き整数の正の最大値までしか
できないなどの制限がありました。
さらに、広い範囲を指定すると、スクロールバーの処理の中のどこかで、
オーバーフローしてるようで、おかしな挙動がみられました。
なので、同じ値を指定したら、デフォルトの設定として、
最小値 INT_MIN / 32、最大値 INT_MAX / 32 を指定した事にします (T_T)
num.c から cui_etext の履歴リストを操作するAPI、 cui_etext_hist_del() を使おうと思ったら、 etext.h にプロトタイプ宣言がなかったので、追加しておきます
$ make $ ./cui_test 左、上、numフォーカス ENTER ================ cui_test ================ |/foo|/bar|/hoge|/term|/timer|/num| | | | | <[0 ]>| | | | | <[0 ]>| | | | | <[0 ]>| | | | :
入力範囲は上から
としてます
'<' や '>' にフォーカス ENTER で、-1、+1 の操作 エディットテキストにフォーカスして ENTER、ENTER で文字列として編集 ENTER で、文字列を判定して補正をかけます 例えば、真中のエディットテキストにフォーカスして ENTER、ENTER : | <[0 ]>| | | | | <[0 ]>| | | | | <[0 ]>| | : '2' '0' と入力して : | <[0 ]>| | | | | <[200 ]>| | | | | <[0 ]>| | : ENTER : | <[0 ]>| | | | | <[127 ]>| | | | | <[0 ]>| | | | | 127 | | | : と、最大値の127に補正されます 右、右、で '|' フォーカスで ENTER ================ cui x st ================ |/foo|/bar|/hoge|/te - /timer|/num| | | = | | <[0 ] | | | | | | <[127 ] | | | | | | <[0 ] | | | | | | 127 | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | - | | | +----------------------------------------+ スクロールバーのthumにフォーカスがきてる状態で開きます ENTER、上下キーで、値を変更します ================ cui x st ================ |/foo|/bar|/hoge|/te - /timer|/num| | | | | | <[0 ] | | | | | | <[37 ] | | | | | | <[0 ] | | | = | | 37 | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | - | | | +----------------------------------------+ ENTER で確定し、 上、上、上、'x'フォーカス ENTER で閉じます エディットテキストの編集モードで、"0x" で始まる 16進数を入力すると、 -1、+1ボタンや、スクロールバーの操作で値を変更しても、 16進数で表示しようとします エディットテキスト自身は、そのままなので、 入力した履歴や、'0'から'9'の文字にカーソルを合わせ、 上キーで、'0'から'9'のポップアップメニューの機能も、そのまま使えます ================ cui_test ================ |/foo|/bar|/hoge|/term|/timer|/num| | | | | <[0 ]>| | | 0| | | <[37 1| ]>| | | 2| | | <[0x123| ]>| | | 4| | | 291 5| | | 6| | | 7| | | 8| | | 9| | | | :
数値入力用の部品を
「SMFを読み込み音の波形データを作るプログラム (C言語)」
の
「CUIから音出ししてみる (その2)」
で、
実際に使ってみると色々と改良点などを思いつきました。
機能の追加などしてみます
まず cui_num に、現在の値の取得と設定のAPIを追加しておきます。
設定では最小値、最大値の範囲内に補正はしますが、
UIを操作して値が変更された訳では無いので、
ハンドラは呼び出さない事にします
UIの操作で値が更新される度に、設定したハンドラを呼び出して、 更新された値を外部へと反映してますが、もっと単純な仕組みも追加してみます
部品に外部の変数のアドレスを与えておいて、
値の更新時にその場で、アドレスの先の変数の値を更新するようにします。
アドレスの初期値はNULLとして、NULLの場合は従来通りとします。
アドレスを設定するAPIを追加して、追加時には部品の値も更新します
最小値、最大値も途中で変更できるようにAPIを追加しておきます。
変更時に、値が範囲内になるように補正もかける事にします。
なので、変数のアドレスを切替えて、範囲も変更する時は、
先に範囲を変えてしまうと、値が補正されるかも知れないので要注意です
次に、数値入力部品で浮動小数(double)の値にも対応してみます
スクロールバーの処理などは、内部では従来通り整数で扱っておいて、 APIで外部とやりとりするところで、整数と小数の対応の変換を かければよさげです
これならば、継承して別の部品を作らずとも、 cui_num に小数用の API を追加して、 内部で小数を使ってるフラグを設けて判別するだけでなんとかなりそうです
さて、小数の場合のインクリメント、デクリメントの処理はどうすべしか?
ステップ幅の値を指定する事にすると、 インクリメント、デクリメント操作時に、 最大値、最小値が、ステップの間に埋もれてしまう可能性があります
最大値、最小値の範囲を分割する分割数を指定する事にすると、 逆に初期値が、間に埋もれる可能性があります
スクロールバーなどの操作では、
最大値、最小値に合わせたい場面はありますが、
初期値に戻したい場面は、それ程ないように思えます。
なので、分割数の指定にしておいて、初期値に戻したければ、
エディットテキストで文字列で入力出来るのでよしとしましょう
いやいやそれでも、分割数で指定するよりは、
ステップ幅で指定する方が、使う側は直感的に判りやすい用に思えます。
なので、APIとしてはステップ幅で指定して、
内部で分割数に換算して処理する事にしてみます
$ make $ ./cui_test 右、上、numフォーカス ENTER 下 x 4 で 4つ目の部品の '|'にフォーカスで ENTER スクロールバーが開くので ENTER、上下キー -1 から 1 の範囲で変更できて 初期値 0 に戻れないのは、目をつむります ================ cui x st ================ |/foo|/bar|/hoge|/te - /timer|/num| | | | | | <[0 ] | | | | | | <[0 ] | | | | | | <[0 ] | | | | | | <[0.06 ] | | | = | | <[12.345 ] | | | | | | 0.060000 | | | | | | (addr set) | | | | | | | | | | | | | | | - | | | +----------------------------------------+ ENTER で決定して、上 x 3 で x フォーカス ENTER 左で '>' フォーカス ENTER、ENTER、... 右、右で '<' フォーカス ENTER、ENTER、... 0.01 刻みで更新されてOKです 左、エディットテキスト フォーカス ENTER、バックスペース '-' , '0' , '.' , '2' , '3' , '4' ENTER ================ cui_test ================ |/foo|/bar|/hoge|/term|/timer|/num| | | | | <[0 ]>| | | | | <[0 ]>| | | | | <[0 ]>| | | | | <[-0.234 ]>| | | | | <[12.345 ]>| | | | | -0.234000 | | | | (addr set) | | | | | | | | | | | | | +----------------------------------------+ 下、下、で (addr set) フォーカス ENTER ================ cui_test ================ |/foo|/bar|/hoge|/term|/timer|/num| | | | | <[0 ]>| | | | | <[7 ]>| | | | | <[0 ]>| | | | | <[-0.234 ]>| | | | | <[45.768 ]>| | | | | -0.234000 | | | | (addr set) | | | | | | | | | | | | | +----------------------------------------+ これで上から2つめの部品と、5つめの部品に、 変数のアドレスが設定され、表示も変数の値に更新されました 2つめと5つめの部品の値を更新すると、 確認用のラベルに、変数の方の値の表示もでてます ================ cui_test ================ |/foo|/bar|/hoge|/te x /timer|/num| | | - | | <[0 ] | | | | | | <[7 ] | | | | | | <[0 ] | | | | | | <[-0.234 ] | | | | | | <[17.6 ] | | | | | | 17.600000 (17.600000) | | | | | (addr set) | | | | | | = | | | | | | | | | | | - | +----------------------------------------+ foo , File , Quit で終了
狭い範囲の整数を指定するときは、 数値入力用の部品よりも、ポップアップメニューの方が、 シンプルでよさそうです
ポップアップメニューから継承して、 cui_menu_int として、機能を追加してみます
数値を扱うので、cui_num に合わせて、 上側に大きな値、下側に小さな値を表示するようにしておきます
APIはできるだけcui_num と合わせるようにします
アイテム数は範囲の指定次第なので、動的に割り付けます なので解放用のAPIも用意しておきます
あとついでに、cui.c の cui_wh_fit() の反対で、 右下を親の入れものの位置に合わせる関数 cui_wh_exp() を追加しておきます
さらに、メニューのデザインの下線の長さが気にいらなかったので、 少し短くしておきます
実装してみると、ハンドラ周りがかなり込み入ってきて、
機能の上書きに難儀してしまいました。
苦肉の策で、cui_menu_popup 構造体の方に
call_btn_hdr_flag などと変なフラグを追加してしまいました。
もうちょっと、なんとかしてスッキリさせたいところです...
$ make $ ./cui_test 右、上、numフォーカス ENTER 下 x 6 で 追加した整数用のポップアップメニューにフォーカスで ENTER ================ cui_test ================ |/foo|/bar|/hoge|/term|/timer|/num| | | | | <[0 ]>| | | | | <[0 ]>| | | | | <[0 ]>| | | 5 | | | 4 | ]>| | | 3 | | | 2 |2.345 ]>| | | 1 | | | 0 | | | | -1| | | -2| | | -3| | | -4|dr set) | | -5| | | | | | | | +----------------------------------------+ 上下キーで移動して ENTER で選択 下 で (addr set) ENTER これで、変数のアドレスが設定されて、値も '-2' に更新されます 以降、ポップメニューの操作で、変数の値の表示も追加されます ================ cui_test ================ |/foo|/bar|/hoge|/term|/timer|/num| | | | | <[0 ]>| | | | | <[7 ]>| | | | | <[0 ]>| | | | | <[0 ]>| | | | | <[45.768 ]>| | | | | 3 | | | | | 3 (3) | | | | (addr set) | | | | | | | | | +----------------------------------------+ foo , File , Quit で終了です
このHTMLファイルに、画面の図を載せてきました。
CUIなので文字の部分は簡単にもってこれるのですが、
下線と反転の装飾部分はあきらめて手動でタグを追加してきました。
だんだん装飾が増えてきて、かなりの手間になってきました。
そこで、プログラム自身にHTMLのタグ付きでも出力させるようにしてみます
出力先はデバッグ表示用のものを使えばいいとして、 さて、コピーをとるトリガをどうしたものか
コピーなので ^C の入力をきっかけにしてみます。
メインループのキー入力で ^C を見張るとして、
例えば、cui_terminal からシェルを開いてたりすると、^C の入力が欲しいかも知れません
ここはひとつ ^C メニューを作ってみる事にします。
^C が入力されると、ポップアップメニューを開きます。
そこで ^C の項目を選ぶと、本当に ^C として処理します。
HTML出力の項目を選ぶと、画面出力を HTML に変換したものを、
デバッグ用の cui_dbg() に出力します。
Cancelの項目を選ぶと、メインループをcontinueして、
^C の入力自体がなかった事にします。
あと、Quitの項目でも用意して
cui_quit() を呼び出すのも良いかも知れません
ちょっと試してると、menu.c cui_menu_popup を使うところで不具合発見。
ポップアップメニューを開くとき、rootの入れものに載せて追加するので、
rootの入れものを解放するまで、解放忘れに陥りやすい状態になります。
既に、etext.c cui_etext_hist_menu(), cui_etext_digit_menu() の
2箇所で、解放忘れしてました
pm = cui_menu_popup_new(p->view_cursor, 0, 0, 0, lst, p->buf[p->cursor] - '0'); : cui_del(pm); cui_free(pm); などとしてますが、本当は pm = cui_menu_popup_new(p->view_cursor, 0, 0, 0, lst, p->buf[p->cursor] - '0'); : cui_del(((cui_menu_popup)pm)->menu); cui_free(((cui_menu_popup)pm)->menu); cui_del(pm); cui_free(pm); としなければなりませんでした
ややこしいので cui_menu_popup_del_free() を追加して、それを使うようにします cui.c にも cui_del_free() 追加します
さて、画面のコピーについて
rootの部品を cui_draw() で全描画すると、
エスケープシーケンスでカーソルを移動させながら、
入れもの親から子供へと画面への描画が進みます。
この順では困るわけで、画面左上から右へと走査、
右下へと向かう順にしてもらわないとダメです。
出力用の cui_term が、画面のリングバッファを扱ってたので、
これを利用する事にします
ルートの部品を含むサイズの cui_term を用意し、
cui_draw() からの出力先を、一時的に cui_term に向けるようにして、
一度だけルートの部品を描画します。
これで cui_term のリングバッファに絵が描かれた状態になります。
cui_draw() の出力先は元に戻しておいて、
リングバッファの内容を、左上から走査して見ながら、
HTMLのタグを付加しつつ、cui_dbg() で出力します
さて cui_draw() の出力先を、どうやって切替えるべしか?
描画の底は cui.c cui_draw_str() の末尾の部分
: cui_esc_loc(cui_gx(obj) + x, cui_gy(obj) + y); if(attr & CUI_ATTR_ULINE) cui_esc_attr(CUI_ESC_ULINE); if(attr & CUI_ATTR_REVERSE) cui_esc_attr(CUI_ESC_REVERSE); printf("%s", s); if(attr != CUI_ATTR_NORMAL) cui_esc_attr(CUI_ESC_NORMAL); fflush(stdout); :
文字はここからの printf() が全てで、
装飾はここからの esc.c cui_esc_xxx() の呼び出しだけです。
esc.c から画面への出力は、esc.c 冒頭の
void cui_esc(char *s) { printf("\033[%s", s); fflush(stdout); }
ここに集約されてます。
この2箇所の printf を、cui_term_puts_buf() に切替えればよさそうです。
cui.c に cui_puts(char *s) を追加して、
esc.c からは、一旦 cui_puts() を呼び出して、一本化しておきます。
cui_puts() の中で切替えるようにします。
cuic. では cui_puts() の出力用として、
複数の出力関数を登録できるようにしておきます。
関数が登録されていたら、引数の文字列を指定して、順次呼び出していきます。
1つも登録されてなければ、printf() して fflush(stdout) する事にします。
登録する出力関数には、ハンドラのようにパラメータを1つ渡せるようにしておきます
はまりました (>_<)
cui_term_new() した後、リングバッファさえ使えればと、
そのまま使ってましたが、cui_hide() で cui_term 自身の描画を止めておかねばなりませんでした。
そうしとかないと、せっかく cui_term_puts_buf() でリングバッファに
描画しても、自身の描画イベントでハンドラが呼ばれて、全描画の更新がかかります。
描画ルーチンの冒頭で、全描画更新なら、cui_clear() で、一旦消去してから、描画します。
一見問題ないようですが、この cui_clear() の先は、
cui_draw_str() から cui_puts() ときて、そう、今だけの一時的な状況ですが、
自身のリングバッファへと繋がってます。
このため、せっせとリングバッファへと描画した内容が、全て水のアワ。
その後の、リングバッファの内容の出力部分では、
リングバッファの内容を読んで、まわりにまわって、
自身のリングへと書き込むという、状況になってました。
(骨の折れるデバッグだった...)
$ make 別端末で $ nc -l -p 12345 元の端末に戻って $ ./cui_test -dbg 適当に操作して、ポップアップメニューでも開いた状態にして ^C を入力 Cancl|========== cui_test ================ ^C |/bar|/hoge|/term|/timer|/num| | HTML | | Quit | [X] Show (move) | | [foo ] | | ============== title =============== | | |+--------------------+ +----------- | | || | |Radio = | | || Are you sure ? | | ( ) ON (= | | || | | = | | || (OK) (Cancel) | | = | | || | | = | | |+--------------------+ January | = | | |+------------------+ February |-= | | || | March | | | | || How are you ? | April | | | | || | May | | | | || (^_^) (T_T) | June | | | | || | July |l| | | |+------------------+ August | - | | +|======================September||+ | | October | | +--------------------------November |----+ 左上隅に ^Cメニューが表示されます HTML を選んで ENTER 画面が消えてしまいますが... 別端末には <pre> ================ cui_test ================ |/foo|<u>/bar|/hoge|/term|/timer|/num|</u> | | | | <u>File</u>| [X] Show (move) | | [foo ] | | ============== title =============== | | |+--------------------+ +----------- | | || | |Radio = | | || Are you sure ? | | ( ) ON (= | | || | | = | | || (OK) (Cancel) | | = | | || | | = | | |+--------------------+ <u>January </u>| = | | |+------------------+ <u>February </u>|-= | | || | <u>March </u>| | | | || How are you ? | <u><font color="#FFFFFF" style="background-color:#000000">April </font></u>| | | | || | <u>May </u>| | | | || (^_^) (T_T) | <u>June </u>| | | | || | <u>July </u>|l| | | |+------------------+ <u>August </u>| - | | +|======================<u>September</u>||+ | | <u>October </u>| | +--------------------------<u>November </u>|----+ </pre>
出てます
まぁ、この上記の表示もまた、手動でHTMLに変換してる訳ではありますが...
この出力をHTMLファイルに貼って、ブラウザで見れば、
下線、反転、'&'、'<'、'>' が、ちゃんと見えるわけです。
別端末の出力をまんま貼りつけたのが、次の絵です
================ cui_test ================ |/foo|/bar|/hoge|/term|/timer|/num| | | | | File| [X] Show (move) | | [foo ] | | ============== title =============== | | |+--------------------+ +----------- | | || | |Radio = | | || Are you sure ? | | ( ) ON (= | | || | | = | | || (OK) (Cancel) | | = | | || | | = | | |+--------------------+ January | = | | |+------------------+ February |-= | | || | March | | | | || How are you ? | April | | | | || | May | | | | || (^_^) (T_T) | June | | | | || | July |l| | | |+------------------+ August | - | | +|======================September||+ | | October | | +--------------------------November |----+
^Cメニューは閉じてますが、 January.. のポップアップメニューを開いたまま画面が消えてます とりあえず ENTER でメニューを閉じます と、再度描画されて、表示が復活しました ================ cui_test ================ |/foo|/bar|/hoge|/term|/timer|/num| | | | | File| [X] Show (move) | | [foo ] | | ============== title =============== | | |+--------------------+ +----------- | | || | |Radio = | | || Are you sure ? | | ( ) ON (= | | || | | = | | || (OK) (Cancel) | | = | | || | | = | | |+--------------------+ | = | | |+------------------+ +----------= | | || | | | | || How are you ? | | | | || | LONG| | | | || (^_^) (T_T) | | | | || | April | l| | | |+------------------+ - | | +|======================----------|+ | | | +----------------------------------------+ どこでこうなったか? なんかうまくいったみたいです まぁ補助的な機能なので、追求せずにそっとしておきます ;-p) ^C Quit選択で終了できるようにもなりました あーラクだ〜
ここでまた、いくつかの部品をブラッシュアップしておきます
整数用のポップアップメニューにならって、
普通のポップアップメニューでも、vp_set() APIを追加しておきます。
整数の変数のアドレスを設定しておいて、
ハンドラを呼び出さずとも、選択してるインデックス値が即反映されるようにします
cui_num の表示がごてごてしてるので、 中のエディットテキストの'['と']'を隠してみます
エディットテキストの全選択モードがいまいち使いづらいです。
フォーカスきてる状態から、ENTER で全選択に入って、
そのまま通常のキー入力すると、全消去して
編集モードに入ってキー入力されるようにします。
あと、全選択モードから、下矢印キーでヒストリーを開き、
ENTER で選択したら、フォーカスモードまで抜けるようにします
エディットテキストの上矢印による'0'-'9'のポップアップメニューは、 整数用のポップアップメニューを使うように入れ替えておきます
整数用のポップアップメニューにも、ポップアップメニューと同様に、 終了処理の用の_del_free()を追加しておきます
cui_tab の表示が詰まりすぎてる感があるので、少々広げておきます
画面コピー用のcui_termのサイズを広くとりすぎてたのを修正
^Cメニューは、必ずroot部品に対して呼出してましたが、
cui_main() 実行中の部品に対して呼出すように変更しました。
その方が"Quit"選択でどの cui_main() に対するQuitか、別りやすいはずです。
ただし"HTML"によるハードコピーは、以前のままroot部品に対して実行するようにしておきます。
あと、^Cメニューに再描画用の項目を追加して、cui_draw() を呼出すようにしておきます
$ make $ ./cui_test =================== cui_test =================== |/ foo |/ bar |/ hoge |/ term |/ timer |/ num || | | | File| [ ] Show (move) | | [foo ] | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | +----------------------------------------------+ num選択で =================== cui_test =================== |/ foo |/ bar |/ hoge |/ term |/ timer |/ num || | | | <0 >| | | | | <0 >| | | | | <0 >| | | | | <0 >| | | | | <12.345 >| | | | | 0 | | | | | | | | | (addr set) | | | | | | | | | +----------------------------------------------+ 少しは見苦しさがましになりました =================== cui_test =================== |/ foo |/ bar |/ hoge |/ term |/ timer |/ num || | | | File| [X] Show (move) | | [foo ] | | ============== title =============== | | |+--------------------+ +----------- | | || | |Radio = | | || Are you sure ? | | ( ) ON (= | | || | | = | | || (OK) (Cancel) | | = | | || | January | = | | |+--------------------+ February | = | | |+------------------+ March |-= | | || | April | | | | || How are you ? | May | | | | || | June | | | | || (^_^) (T_T) | July | | | | || | August |l| | | |+------------------+ September| - | | +|======================October ||+ | | November | | +--------------------------December |----------+
ネットワーク対応を考えてみます
さすがCUI。超簡単ですねっ!
以上、おわり
なーんて嘘です ;-p)
しかしまぁ今の状態のCUIでも、netcat (ncコマンド) が使えれば、 標準入力と標準出力をncコマンドに繋いで、 簡単にTCPで遠隔操作できてしまいます
host_a は手前のマシン host_b は遠くにあるマシン とします host_a $ make host_a $ nc -l -p 9000 -e ./cui_test これで、手前のマシンで ./cui_test を実行して標準入出力を nc コマンドに繋いで、 host_a のポート番号 9000 で TCP 接続を待ち受けます host_b $ stty raw -echo ; nc host_a 9000 ; stty -raw echo これで、遠くにあるマシンから、nc コマンドで host_a の ポート番号 9000 に TCP で繋ぎにいきます nc コマンドの前の stty コマンドでは、端末をrawモード、エコーなしに設定し、 nc コマンドの後の stty コマンドでは、逆に元の設定に戻しています 結果、host_b の端末画面にCUIの画面が表示され、host_b の端末のキー入力を受け付けて動作します 逆に host_b で待ち受けて、host_a の cui_test 側から接続しに行く事もできます host_b $ stty raw -echo ; nc -l -p 9001 ; stty -raw echo これで、遠くにあるマシンでncコマンドを実行し、ポート番号9001で待ち受けます host_a $ nc host_b 9001 -e ./cui_test これで、手前のマシンで cui_test を実行し、入出力を nc コマンドにつないで、 遠くのマシンのポート 9001 に繋ぎにいきます 結果はやはり、host_b の端末画面にCUIの画面が表示され、host_b の端末のキー入力を受け付けて動作します
-e オプションや -c オプションが使えない ncコマンドの場合は、man nc を見てみると例が載ってました。
それに習って次のように試してみるとOKでした
host_a $ rm -f /tmp/f ; mkfifo /tmp/f host_a $ cat /tmp/f | ./cui_test | nc -l -p 9002 > /tmp/f host_b $ stty raw -echo ; nc host_a 9002 ; stty -raw echo 待ち受けと繋ぎにいく関係が逆の場合は host_b $ stty raw -echo ; nc -l -p 9003 ; stty -raw echo host_a $ cat /tmp/f | ./cui_test | nc host_b 9003 > /tmp/f
これだけでは芸が無いので、ここはひとつ、複数からの接続を受付るサーバを用意してみます
mc.c簡単なsocketプログラムのサーバの例みたいなものです
--> 標準入力 mc | +------> クライアント 1 +------> クライアント 2 : : +------> クライアント n <-- 標準出力 mc ^ |<----- クライアント 1 |<----- クライアント 2 : : +------ クライアント n
この mc コマンドの標準入力、標準出力の先に CUI を繋いでおけば、
同じ画面を複数のクライアントに表示できて、
複数のクライアントからのキー入力も受け付けれます。
(キー操作の争奪戦になるかも知れませんが...)
host_a は手前のマシン host_b は遠くにあるマシン host_c は遠くにあるマシンその2 とします 例えば遠くのマシンその2で host_c $ gcc -o mc mc.c host_c $ nc -l -p 9004 -e './mc -p 9005' これで、mcコマンドはサーバとしてポート番号 9005 で待ちうけて、 その標準入出力は nc コマンドに繋ぎます。 ncコマンドはポート番号 9004 で待ち受けます 近くのマシンで host_a $ nc host_c 9004 -e ./cui_test これで、遠くのマシンその2のポート番号 9004 に繋ぎにいきます 遠くのマシンから host_b $ stty raw -echo ; nc host_c 9005 ; stty -raw echo これで、遠くのマシンその2の mcのサーバポート9005 に繋ぎにいきます 残念ながら、そのままでは画面は表示されません。 host_b から ^C を入力すると、^C メニューだけは表示されます。 ここで 'Redraw' を選択すると、晴れて全体の表示が出ます。 その状態のまま host_d (遠くのマシンその3) からも host_d $ stty raw -echo ; nc host_c 9005 ; stty -raw echo ^C で Redraw 選択 で同じ画面が表示されるはずです。 host_b と host_d でキー操作した結果は、互いの画面にも反映されるはずです
nc コマンドで -e オプションが使えない環境でも、 先述の要領で書き換えれば、大丈夫かもしれません
$ nc -l -p <port> -e '<cmd>' は $ rm -f /tmp/f ; mkfifo /tmp/f しておいて $ cat /tmp/f | <cmd> | nc -l -p <port> > /tmp/f とします また $ nc <host> <port> -e '<cmd>' は $ rm -f /tmp/f ; mkfifo /tmp/f しておいて $ cat /tmp/f | <cmd> | nc <host> <port> > /tmp/f とします
遠くのマシンの端末では nc コマンドで待ち受けておいて、
手前のマシンではCUIの標準入出力をncコマンドに繋ぎ、
ncコマンドから、遠くのマシンのncコマンドへと接続するパターン。
この場合の、CUIと直接接続してる手前のマシンの方 ncの機能を、CUIの中に取り込んでみます
CUIの中でクライアントsocketを作って、 指定のホストのポートへとconnect し、 そのsocketからの入力をキー入力として処理し、 画面表示をsocketへと出力すればよさそうです
こうしておけば、標準入出力が空くので、 データのやりとりに使えそうです
という事で、次のコマンドライン・オプションを追加してみます
-conn [ホスト名:]ポート番号 'ホスト名:' が省略された時はデフォルト設定を 'localhost:' とします このオプションが指定されたときは、標準入出力を使いません ただし -stdio オプションが指定されているときは、標準入出力も使う事にします
まず、キー入力箇所をどうすべしか?
cui.c cui_select_init() で KEY_FD を決めうちしてるのと、
cui_main() でも KEY_FD を決めうちしてます。
あとは、key.c の中で KEY_FDを使ってます
はて、key.c よく見ると、なんだかまずそうなコードがあります。
cui_key_get2() で CUI_KEY_ESC のときに、
cui_key_get() を呼び出して read() してますが、
これは単独でエスケープ・キーを押したときは、readがブロックしてしまうのでは?
おそらく select を cui.c 側に移したときに、こうなってしまったはずです。
なので、 cui81_82.patch があやしいです。
cui72.tgz をひもといて key.c をみてみると、
read の前で select してタイムアウトならば、
ブロックせずに 0 を返してました。
cui_main() からの select とは別に、
key.c の cui_key_get2() と cui_key_get() の間で、
fd が 100 msec 以内にread 可能になるかどうかだけ、
確認する処理を select で追加して、元に動作になるように修正しておきます
これでメニューを開いたときに ESCキーでちゃんんと取り消せるようになり、元の動作に戻りました
さて、socket からのキー入力対応に戻って
とりあえず key.[ch] にキーとして扱う fd の集合 fd_set を追加し公開してみます。
この集合に、追加、削除するAPIを設けておきます
cui.c cui_select_xxx() 関連は select.[ch] として別ファイルに、のれん分けしておきます
cui_select() から抜けてくると、
cui_key_get2() の中で、登録されてる fd を順にリード可能か改めて判定する事にします。
リード可能な fd を見付けたら、その回はその fd について処理する事にします。
他にリード可能な fd があったとしても、次回の select で処理する事にします
ここまで、なるべくCUI全体の初期化処理を用意せずに、なんとかやってきました。
当初、cui_main() でメインループの前で初期化して、メインループに入るようにしてましたが、
途中から cui_main() をメニューの処理などで、モーダル的にネストして呼び出すようになり、
なんとかごまかしてきましたが、そろそろ込み入ってきて限界です。
あきらめて、cui_init() cui_fini() を設けることにします
そして、描画の出力側をどうするか。
画面のハードコピーのHTML出力 のところで
cui.c cui_puts() のしくみを導入してました。
cui_puts_add() で出力関数を登録すると、printf() を呼ばずに、
登録した関数を呼び出します。
これを使えばよさそうかな? っと思って実装してみると、
オーバーライドする側の関数にも、HTML出力がきてしまうなど、
cui_puts() の仕様に変な癖がありました。
ここは一旦、cui_puts() の中で、printf() してる箇所自体を、
置き換えて対応する事にします
あと、^CメニューのRedrawの処理に cui_clear() 追加しておきます
host_a は手前のマシン host_b は遠くにあるマシン とします 遠くにあるマシンから host_b $ stty raw -echo ; nc -l -p 9010 ; stty -raw echo で待ち受けておいて、 手前のマシンから host_a $ make host_a $ ./cui_test -conn host_b:9010 で繋ぎにいきます 遠くにあるマシンにCUIの画面が表示され操作できます 遠くにあるマシンから ^C Quit 選択で終了 もう一度、遠くにあるマシンから host_b $ stty raw -echo ; nc -l -p 9011 ; stty -raw echo で待ち受けておいて、 手前のマシンから host_a $ ./cui_test -conn host_b:9011 -stdio で繋ぎにいきます 遠くにあるマシンにCUIの画面が表示され操作できて、 かつ、手前のマシンでも表示されて操作できます
次は ネットワーク対応 (その2) で作ってみた mc コマンド mc.c の機能を、CUIに取り込んでみます
という事で、次のコマンドライン・オプションを追加してみます
-srv ポート番号 このオプションが指定されたときも、標準入出力を使わない事にします ただし -stdio オプションが指定されているときは、 -conn の時と同様に、標準入出力も使う事にします
複数のマシンから nc コマンドで繋ぎにいったとして、 個別に切断する時はどうすべしか?
困ったときの^Cメニュー (^_^)
メニュー項目に "Disconn" を追加して、
最後に ENTERキー を押した人がソケットなら、
close() する仕様にしてみます
host_a は手前のマシン host_b は遠くにあるマシン host_c は遠くにあるマシンその2 とします 手前のマシンから host_a $ make host_a $ ./cui_test -srv 9020 と起動して待ち受けておきます 遠くのマシンから host_b $ stty raw -echo ; nc host_a 9020 ; stty -raw echo で繋ぎにいくと、画面が表示されて操作できます さらに遠くのマシンその2 からも host_c $ stty raw -echo ; nc host_a 9020 ; stty -raw echo で繋ぎにいくと、画面が表示されて操作できます 遠くにあるマシンから ^C で Disconn を選択すると、 接続が切断されます 遠くにあるマシンその2 の方は接続を保ってます 手前のマシンで起動するときに -stdio オプションを追加すると、 手前のマシンにも表示されて操作可能です
端末を提供する側のサーバ・アプリケーションを作ってみます
GUIで言うところの Xserver のようなアプリケーションです。
起動すると指定のポート番号で待ち受けておいて、
クライアントが接続してくると、
端末を開いてキー入力と画面表示の処理をします。
複数のクライアントの画面を管理せねばなりませんが、
まずは tabシートを使って切替えるようにしてみます。
ボタン0個でtabを作っておいて、
クライアントが繋ぎにきたら、
端末の部品を作って、cui_tab_add() で追加するようにします
tabの追加動作を cui_test でちょっと試してみたら、
問題がありました。
既にボタンがある状態からの追加だけを想定してたので、
ボタン0個の状態からの追加の場合、NULLアクセスしてSegfault発生。
修正ついでに、追加したtabのボタンとシートを、
アクティブにするかどうか指定できるようAPIに引数を追加しておきます
あと cui.c srv_accept() で警告が出てたので、つぶしておきます
まずは、tab部品の修正の確認
$ make $ ./cui_test tab test を選んで (add) (add) ========================= cui_test ========================= |/ foo |/ bar |/ hoge |/ term |/ timer |/ num |/ tab test || | | |(add) [X] init_act | | | |/ 0 |/ 1 | | | | | 1 | | | : init act のチェックを外して (add) (add) ========================= cui_test ========================= |/ foo |/ bar |/ hoge |/ term |/ timer |/ num |/ tab test || | | |(add) [ ] init_act | | | |/ 0 |/ 1 |/ 2 |/ 3 | | | | | 1 | | | : 2のタブを選ぶと ========================= cui_test ========================= |/ foo |/ bar |/ hoge |/ term |/ timer |/ num |/ tab test || | | |(add) [ ] init_act | | | |/ 0 |/ 1 |/ 2 |/ 3 | | | | | 2 | | | : シートも切り替わります OKです
その他の修正として、 cui.c srv_accept() から cui_esc_enter() の呼び出しでカーソルを消してるつもりでしたが、 esc.c cui_esc_enter() で初回判定してて、はねられてました
この初回かどうかのカウンタは、
cui.c cui_hide() cui_show() cui_xywh_set() で参照してました。
これは、cui_init() を導入する以前の時代に導入したしくみです。
cui_main() で、cui_esc_enter() してから、メインループに入るので、
このカウンタをみて、初回のメインループ呼び出しの前の時点か、
メインループに入ってからの呼び出しなのか判定して、不要な描画を避けてました。
アプリケーション冒頭で、まず cui_init() を呼ぶ仕様に変えたので、
cui_init() の中から cui_esc_enter() を呼び出すようになりました。
なのでこのカウンタは、アプリケーション冒頭ですぐ +1 されて、
初期値 0 の期間はほとんどなくなりました。
一旦、不要な描画を避けるのはあきらめて、カウンタ参照箇所を削除しておきます。
そして、esc.c cui_esc_enter() は、
cui.c の cui_init() の他、srv_accept() からしか呼び出しはありません。
対の cui_esc_exit() も、
cui.c の cui_init() と、disconn() からのみです。
cui_main() から呼び出される事はなくなりました。
よって、同じ端末に重複して cui_esc_enter() や cui_esc_exit() が呼び出される事は無く、
カウンタも不要になりました。
カウンタを削除します
あと、変数の値で cui_show() と cui_hide() を呼び分けてる箇所が、 よく目についてきたので、糖衣構文として cui_show_hide(cui obj, int v) を追加しておきます
なかなか本題に辿りつきませんが、ここで一旦確認です
host_a $ make host_a $ ./cui_test -srv 9030 -stdio などと実行しておいて host_b $ stty raw -echo ; nc localhost 9030 ; stty -raw echo host_b の画面でも、端末としてのカーソル表示は消えた状態で、 CUIの画面が表示されてます OK です
次にクライアントに繋ぐ端末の部品を用意します
term.c には表示用の部品 cui_term があります。
さらにその cui_term を使って、/bin/sh を別プロセスで実行して、
キー入力をなんとかいなしてる部品 cui_terminal があります (^_^;
今回の部品としては /bin/sh 実行までは不要ですが、 キー入力の面倒をみて欲しいので、 丁度 cui_term と cui_terminal の中間くらいの機能が必要です
cui_termin として追加してみます
cui_terminal では、キー入力にタイムアウト制を導入するという「はなれ技」で、 キー入力の切替えをなんとかしてました
しかし今や^Cメニューを手に入れてます
例えば、部品にフォーカス後 ENTERキーで、その部品を cui_main() でネストして実行したとしましょう。
その部品のキーハンドラで、キーイベントを独占していても、
^Cメニューから Quit を選択すれば、その cui_main() から抜けてくるはずです
キー入力の切替えも、このしくみを使ってみます。
^Cメニューは当然、^Cキーを使いますが、メニュー項目から^Cを選べば、部品の中へ^Cキー入力を渡せます。
よって^Cが渡せないという問題はありません
それでは cui_termin を実装してみます (やっとだ)
cui_termin を cui_test で試すために、ソケットを用意したいのですが、 cui.c conn() がそのまま使えそうながらも、static で内部関数にしてました cui_conn(char *s) として公開して、cui_test から使ってみます
おっと、ここでバグ発見!
cui.c arg_idx() の返り値が!
たまたま害が出ない使い方しかしてなかったようです...修正 m(__)m
cui_test.c からは、起動オプション '-termin ポート番号' で、 localhostの指定ポート番号に接続しにいくようにして、試してみます
$ make まず、cui_test自身を、-srv でポート番号9030で待ち受けるようにして、 バックグランドで起動しておきます $ ./cui_test -srv 9030 & つづいて、試したい方の cui_test を起動して、 バックグランドで動いてる cui_test につなぎにいきます $ ./cui_test -termin 9030 ============================== cui_test ============================== |/ foo |/ bar |/ hoge |/ term |/ timer |/ num |/ tab test |/ termin || | | | File| [ ] Show (move) | | [foo ] | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | +--------------------------------------------------------------------+ タブのterminにフォーカスして ENTER ============================== cui_test ============================== |/ foo |/ bar |/ hoge |/ term |/ timer |/ num |/ tab test |/ termin || |============================== cui_test ============================| ||/ foo |/ bar |/ hoge |/ term |/ timer |/ num |/ tab test |/ termin | || | || File| [ ] Show (move) | || [foo ] | || | || | || | || | || | || | || | || | || | || | || | || | || | || | || | +--------------------------------------------------------------------+ でました 下側の表示はバックグランドで動いてる方の cui_test の画面です 下キーで右下隅のリサイズボックス '+' にフォーカスして ENTER、右 x 4、下 x 4、ENTER でサイズを広げておきます ================================ cui_test ================================ |/ foo |/ bar |/ hoge |/ term |/ timer |/ num |/ tab test |/ termin | | |============================== cui_test ============================== | ||/ foo |/ bar |/ hoge |/ term |/ timer |/ num |/ tab test |/ termin || | || | | || File| [ ] Show (move) | | || [foo ] | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | |+--------------------------------------------------------------------+ | | | +------------------------------------------------------------------------+ 左キーで、cui_termin にフォーカスしました ================================ cui_test ================================ |/ foo |/ bar |/ hoge |/ term |/ timer |/ num |/ tab test |/ termin | | |////////////////////////////////////////////////////////////////////////| |// foo |/ bar |/ hoge |/ term |/ timer |/ num |/ tab test |/ termin || /| |/ | /| |/ File| [ ] Show (move) | /| |/ [foo ] | /| |/ | /| |/ | /| |/ | /| |/ | /| |/ | /| |/ | /| |/ | /| |/ | /| |/ | /| |/ | /| |/ | /| |/ | /| |/ | /| |/ | /| |/ | /| |/ | /| |/ | /| |/--------------------------------------------------------------------+ /| |////////////////////////////////////////////////////////////////////////| +------------------------------------------------------------------------+ ENTER で、cui_termin のキー入力モードに入って、 cui_main() をネストして実行します 以降のキー入力は、全てバックグランドの cui_test に送られるので、 バッググランド側の cui_test の相手をします ================================ cui_test ================================ |/ foo |/ bar |/ hoge |/ term |/ timer |/ num |/ tab test |/ termin | | |============================== cui_test ============================== | ||/ foo |/ bar |/ hoge |/ term |/ timer |/ num |/ tab test |/ termin || | || | | || File| [X] Show (move) | | || [foo ] | | || ============== title =============== | | || |+--------------------+ +----------- | | || || | |Radio = | | || || Are you sure ? | | ( ) ON (= | | || || | | = | | || || (OK) (Cancel) | | = | | || || | January | = | | || |+--------------------+ February | = | | || |+------------------+ March |-= | | || || | April | | | | || || How are you ? | May | | | | || || | June | | | | || || (^_^) (T_T) | July | | | | || || | August |l| | | || |+------------------+ September| - | | || +|======================October ||+ | | || November | | | |+--------------------------December |--------------------------------+ | | | +------------------------------------------------------------------------+ ただし、^C だけは手前の cui_termin が拾ってメニューを表示します このときの^Cメニューの表示位置は、 いつもと違ってterminの左上端にきてるはずです そして Quit を選択 ================================ cui_test ================================ |/ foo |/ bar |/ hoge |/ term |/ timer |/ num |/ tab test |/ termin | | |Cancel |====================== cui_test ============================== | |^C |/ bar |/ hoge |/ term |/ timer |/ num |/ tab test |/ termin || | |Redraw | | | |Disconn| [X] Show (move) | | |HTML | ] | | |Quit |========= title =============== | | || |+--------------------+ +----------- | | || || | |Radio = | | || || Are you sure ? | | ( ) ON (= | | || || | | = | | || || (OK) (Cancel) | | = | | || || | | = | | || |+--------------------+ | = | | || |+------------------+ +----------= | | || || | | | | || || How are you ? | | | | || || | LONG| | | | || || (^_^) (T_T) | | | | || || | April | l| | | || |+------------------+ - | | || +|======================----------|+ | | || | | |+--------------------------------------------------------------------+ | | | +------------------------------------------------------------------------+ これで、内側の cui_main() から抜けて、 termin のキー入力モードから抜け、 termin にフォーカスしてる状態に戻ります ... しまった、フォーカス表示に戻らない モードを抜けたときに cui_draw が抜けてます まぁ後で、修正するとして 矢印キーの操作で、フォーカスは外側の部品に移動可能に戻ってます ================================ cui_test ================================ |/ foo |/ bar |/ hoge |/ term |/ timer |/ num |/ tab test |/ termin | | | | | File| [X] Show (move) | | [foo ] | | ============== title =============== | | |+--------------------+ +----------- | | || | |Radio = | | || Are you sure ? | | ( ) ON (= | | || | | = | | || (OK) (Cancel) | | = | | || | | = | | |+--------------------+ | = | | |+------------------+ +----------= | | || | | | | || How are you ? | | | | || | LONG| | | | || (^_^) (T_T) | | | | || | January| l| | | |+------------------+ - | | +|======================----------|+ | | | | | | | | | | | +------------------------------------------------------------------------+ なんとなく使えそうな感触です
あと実装では、クライアントでcloseされたときに、readで失敗か0が返るはずで、 どう処理すべきかを保留してます
これは cui_terminal からの /bin/sh で exit した時も同様で、 保留のままほったらかしてました
GUIで言うところの Xserver のようなアプリケーションにしたいので、 クライアントの口が close したら、そっと表示を消して、 そのクライアントに対応してるタブのボタンも、 削除したいところです
cui_termin 部品は close を検出したら、以降用なしになるので、
親の入れものに対して、「お亡くなり」イベントを送る事にします。
という事で CUI_EVT_DEAD を追加します。
あとは、親の入れもの側で、cui_del_free() するなり、
煮るなり焼くなり好きにしてもらいましょう
イベントを受け取る親の入れもの側では、
まずどの子が亡くなったか判からねばなりません。
イベントの値に、cui_index(子供の部品) で得られるインデックス値を
指定する事にします。
cui_index() は、親の入れものに登録した順で 0 からの値が返ります。
親側のハンドラでは、引数 val から、
cui_index_to_child(親の部品, val) で、
亡くなった子供の部品を取得できます
となると、close されたら親で cui_del_free() は可能ですが、
タブのボタンが問題です。
cui_tab_add() はありますが、
cui_tab_del() はまだ作ってませんでした。
追加して試してみます
$ make $ ./cui_test tab test 選択して (add) (add) (add) init_act のチェックを外して (add) (add) (add) ============================== cui_test ============================== |/ foo |/ bar |/ hoge |/ term |/ timer |/ num |/ tab test |/ termin || | | |(add) [ ] init_act (del) | | | |/ 0 |/ 1 |/ 2 |/ 3 |/ 4 |/ 5 | | | | | 2 | | | : (del) ============================== cui_test ============================== |/ foo |/ bar |/ hoge |/ term |/ timer |/ num |/ tab test |/ termin || | | |(add) [ ] init_act (del) | | | |/ 0 |/ 1 |/ 3 |/ 4 |/ 5 | | | | | 0 | | | さらに (del) ============================== cui_test ============================== |/ foo |/ bar |/ hoge |/ term |/ timer |/ num |/ tab test |/ termin || | | |(add) [ ] init_act (del) | | | |/ 1 |/ 3 |/ 4 |/ 5 | | | | | 1 | | | : タブの 5 を選択して ============================== cui_test ============================== |/ foo |/ bar |/ hoge |/ term |/ timer |/ num |/ tab test |/ termin || | | |(add) [ ] init_act (del) | | | |/ 1 |/ 3 |/ 4 |/ 5 | | | | | 5 | | | : (del) ============================== cui_test ============================== |/ foo |/ bar |/ hoge |/ term |/ timer |/ num |/ tab test |/ termin || | | |(add) [ ] init_act (del) | | | |/ 1 |/ 3 |/ 4 | | | | | 1 | | | : OKです
それでは「GUIで言うところの Xserver のようなアプリケーション」 を実装してみます
cui_srv として追加します
ポート番号の指定は、コマンド引数 -port ポート番号 とします (端末側がつないで来るのを待ちうけるときは -srv ポート番号 なので注意)
cui_conn() と同様、 サーバのときのソケットの処理も、使い回したいので、 cui_srv_port() cui_srv_accpet() として公開し、 cui_srv.c から利用する事にします
term.c cui_termin から cui_term を使ってるので、 cui_term のリング・バッファ解放処理を含めた、 cui_termin の削除解放用のAPIを追加しておきます
host_a は手前のマシン host_b は遠くにあるマシン とします 手間のマシンで cui_srv を起動してポート番号9040で待ち受けます host_a $ make host_a $ ./cui_srv -port 9040 ========================= cui_srv ========================== | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | +----------------------------------------------------------+ デフォルトのサイズが少々、小さかったようです リサイズボックスにフォーカスして、サイズを広げておきます ENTER、左 x 20、下 x 10、ENTER =================================== cui_srv ==================================== | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | +------------------------------------------------------------------------------+ 同じ手前のマシンの別の端末から、 cui_test を繋いでみます 別の端末でビルドしたディレクトリに移動して $ ./cui_test -conn 9040 cui_srv の画面に表示が出ます =================================== cui_srv ==================================== |/ 0 | | |============================== cui_test ============================== | ||/ foo |/ bar |/ hoge |/ term |/ timer |/ num |/ tab test |/ termin || | || | | || File| [ ] Show (move) | | || [foo ] | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | |+--------------------------------------------------------------------+ | | | | | | | | | +------------------------------------------------------------------------------+ 上矢印で、フォーカスをリサイズボックスから、cui_termin に移動 =================================== cui_srv ==================================== |/ 0 | | |//////////////////////////////////////////////////////////////////////////////| |// foo |/ bar |/ hoge |/ term |/ timer |/ num |/ tab test |/ termin || /| |/ | /| |/ File| [ ] Show (move) | /| |/ [foo ] | /| |/ | /| |/ | /| |/ | /| |/ | /| |/ | /| |/ | /| |/ | /| |/ | /| |/ | /| |/ | /| |/ | /| |/ | /| |/ | /| |/ | /| |/ | /| |/ | /| |/ | /| |/--------------------------------------------------------------------+ /| |/ /| |/ /| |/ /| |//////////////////////////////////////////////////////////////////////////////| +------------------------------------------------------------------------------+ ENTER 以降はのキー入力は cui_termin 中の世界へと送られます 矢印キーでフォーカスを移動し、cui_test を操作できます =================================== cui_srv ==================================== |/ 0 | | | | | | | ============================== cui_test ============================== | | |/ foo |/ bar |/ hoge |/ term |/ timer |/ num |/ tab test |/ termin || | | | | | | | File| [X] Show (move) | | | | [foo ] | | | | ============== title =============== | | | | |+--------------------+ +----------- | | | | || | |Radio = | | | | || Are you sure ? | | ( ) ON (= | | | | || | | = | | | | || (OK) (Cancel) | | = | | | | || | | = | | | | |+--------------------+ | = | | | | |+------------------+ +----------= | | | | || | | | | | | || How are you ? | | | | | | || | LONG| | | | | | || (^_^) (T_T) | | | | | | || | January| l| | | | | |+------------------+ - | | | | +|======================----------|+ | | | | | | | +--------------------------------------------------------------------+ | | | | | +------------------------------------------------------------------------------+ ^C Quit 選択で、キー入力は cui_srv 自体へと戻ります 遠くにあるマシンからも繋いでみます host_b $ make host_b $ ./cui_test -conn host_a:9040 cui_srv の画面に表示が出ます =================================== cui_srv ==================================== |/ 0 |/ 1 | | |============================== cui_test ============================== | ||/ foo |/ bar |/ hoge |/ term |/ timer |/ num |/ tab test |/ termin || | || | | || File| [ ] Show (move) | | || [foo ] | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | |+--------------------------------------------------------------------+ | | | | | | | | | +------------------------------------------------------------------------------+ 繋がって、タブのボタンが増えて表示がでます タブの切替えで、 手前のマシンで実行してる方の cui_test の画面と、 遠くのマシンで実行してる方の cui_test の画面を切り替え可能です =================================== cui_srv ==================================== |/ 0 |/ 1 | | | | | | | ============================== cui_test ============================== | | |/ foo |/ bar |/ hoge |/ term |/ timer |/ num |/ tab test |/ termin || | | | | | | | File| [X] Show (move) | | | | [foo ] | | | | ============== title =============== | | | | |+--------------------+ +----------- | | | | || | |Radio = | | | | || Are you sure ? | | ( ) ON (= | | | | || | | = | | | | || (OK) (Cancel) | | = | | | | || | | = | | | | |+--------------------+ | = | | | | |+------------------+ +----------= | | | | || | | | | | | || How are you ? | | | | | | || | LONG| | | | | | || (^_^) (T_T) | | | | | | || | January| l| | | | | |+------------------+ - | | | | +|======================----------|+ | | | | | | | +--------------------------------------------------------------------+ | | | | | +------------------------------------------------------------------------------+ 手前のマシンの cui_test を実行してる端末で、 ^C で cui_test を停止させてみると =================================== cui_srv ==================================== |/ 1 | | |============================== cui_test ============================== | ||/ foo |/ bar |/ hoge |/ term |/ timer |/ num |/ tab test |/ termin || | || | | || File| [ ] Show (move) | | || [foo ] | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | |+--------------------------------------------------------------------+ | | | | | | | | | +------------------------------------------------------------------------------+ cui_srv の画面では、タブの0のボタンが消えて、 遠くのマシンの方の cui_test の画面に切り替わります さてここで、ふと気付きました キー入力を中の世界に取られている状態から、 新しいクライアントが繋ぎにきたら、どうなるでしょうか... いやな予感がしますが、試してみます まず中の世界にキー入力できる状態にして、cui_test を色々と操作しておきます =================================== cui_srv ==================================== |/ 1 | | |============================== cui_test ============================== | ||/ foo |/ bar |/ hoge |/ term |/ timer |/ num |/ tab test |/ termin || | || | | || File| [X] Show (move) | | || [foo ] | | || ============== title =============== | | || |-----------+ +-------------------+- | | || | | |Radio |= | | || | sure ? | | (O) ON ( ) OFF |= | | || | | | ( ) ABC 1143 kHz|= | | || | (Cancel) | | (O) KBS |= | | || | | | ( ) MBS |= | | || |-----------+ | ( ) OBC |= | | || |---------+ +-------------------+= | | || | | | | | || | you ? | | | | || | | LONG| | | | || | (T_T) | | | | || | | January| last 1/31 | | | || |---------+ - | | || +|----------======================|+ | | || | | |+--------------------------------------------------------------------+ | | | | | | | | | +------------------------------------------------------------------------------+ 先ほど cui_test を停止させた、手前のマシンの別の端末から、 再度 cui_test を繋いでみます =================================== cui_srv ==================================== |/ 1 |/ 2 | | |============================== cui_test ============================== | ||/ foo |/ bar |/ hoge |/ term |/ timer |/ num |/ tab test |/ termin || | || | | || File| [ ] Show (move) | | || [foo ] | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | |+--------------------------------------------------------------------+ | | | | | | | | | +------------------------------------------------------------------------------+ とりあえず、画面は出ました しかしキー操作は、タブ1側の中の世界に取られたままなので... cui_srv で矢印キーを操作しても、何も反応がありません (>_<) ^C でメニューは出ました Quit 選択で cui_srv にキーフォーカスが戻りました うーむ。 既にタブボタンが存在してる時に、クライアントが繋ぎにきたら、 タブボタンは選択してない状態で、後ろにこっそり追加すべきですね クライアントの cui_test を、正規の手順で停止できるか試してみます タブ2を選んで cui_test の中の世界を操作 =================================== cui_srv ==================================== |/ 1 |/ 2 | | | | | | | ============================== cui_test ============================== | | |/ foo |/ bar |/ hoge |/ term |/ timer |/ num |/ tab test |/ termin || | | | | | | | File| [X] Show (move) | | | | [foo ] | | | | ============== title =============== | | | | |+--------------------+ +----------- | | | | || | |Radio = | | | | || Are you sure ? | | ( ) ON (= | | | | || | | = | | | | || (OK) (Cancel) | | = | | | | || | | = | | | | |+--------------------+ | = | | | | |+------------------+ +----------= | | | | || | | | | | | || How are you ? | | | | | | || | LONG| | | | | | || (^_^) (T_T) | | | | | | || | January| l| | | | | |+------------------+ - | | | | +|======================----------|+ | | | | | | | +--------------------------------------------------------------------+ | | | | | +------------------------------------------------------------------------------+ ^C で メニューが出ます =================================== cui_srv ==================================== |/ 1 |/ 2 | | |Cancel | | |^C | | |Redraw |========================= cui_test ============================== | |Disconn|o |/ bar |/ hoge |/ term |/ timer |/ num |/ tab test |/ termin || | |HTML | | | |Quit |e| [X] Show (move) | | | | [foo ] | | | | ============== title =============== | | | | |+--------------------+ +----------- | | | | || | |Radio = | | | | || Are you sure ? | | ( ) ON (= | | | | || | | = | | | | || (OK) (Cancel) | | = | | | | || | | = | | | | |+--------------------+ | = | | | | |+------------------+ +----------= | | | | || | | | | | | || How are you ? | | | | | | || | LONG| | | | | | || (^_^) (T_T) | | | | | | || | January| l| | | | | |+------------------+ - | | | | +|======================----------|+ | | | | | | | +--------------------------------------------------------------------+ | | | | | +------------------------------------------------------------------------------+ これは、タブ2のシートである cui_termin を実行してる cui_main() のメインループからの、 ^Cメニューの表示です なので、ここでは ^C の項目を選んで、中の cui_test の世界へと ^C を送りつけます =================================== cui_srv ==================================== |/ 1 |/ 2 | | | | | | | Cancel |====================== cui_test ============================== | | ^C |/ bar |/ hoge |/ term |/ timer |/ num |/ tab test |/ termin || | | Redraw | | | | Disconn| [X] Show (move) | | | HTML | ] | | | Quit |========= title =============== | | | | |+--------------------+ +----------- | | | | || | |Radio = | | | | || Are you sure ? | | ( ) ON (= | | | | || | | = | | | | || (OK) (Cancel) | | = | | | | || | | = | | | | |+--------------------+ | = | | | | |+------------------+ +----------= | | | | || | | | | | | || How are you ? | | | | | | || | LONG| | | | | | || (^_^) (T_T) | | | | | | || | January| l| | | | | |+------------------+ - | | | | +|======================----------|+ | | | | | | | +--------------------------------------------------------------------+ | | | | | +------------------------------------------------------------------------------+ これで、cui_test の世界で ^C が押された事になり、 cui_test の cui_main() から ^C メニューが表示されます メニューの表示位置が、唯一のたよりです ここで Quit を選べば、cui_test が終了します cui_test が終了すれば、 cui_termin がそれを検知して、亡くなったよイベントをあげて、 タブ2 が削除されて、タブ1だけの画面になります キー操作をすると、 突然 Segmentation fault (core dumped) うーむ なんかまだまだ問題あるか〜...
そうか、termin のメインループ実行中の状態で、
termin が亡くなったよイベントあげて、
親の入れもののハンドラから、termin を削除、解放してました (T_T)
親の入れもののハンドラから戻って、termin のハンドラに戻って、
さらに、メインループに戻ったところで、
メインループの主役 termin が、すでに解放されていると。
さて、どうするか?
検知したとき、メインループ中かどうかは、メンバの in_key_mode の値でわかります。
メインループでなれば、こまでの処理でいいとして、
メインループならば、どこかに亡くなったことを記録して、cui_quit() で、メインループを抜けさせます。
メインループから返ったところで、かならず亡くなったかどうかを確認して、
イベントあげればよさそうです。
どこかに亡くなったことを記録するのは、ソケットを close して -1 に書きかえる事にします
タブとシートである termin を削除するときは、 フォーカスがきてないか確認する処理も追加しておきます
先のタブ追加で選択状態にしない修正とあわせて更新します
host_a $ make host_a $ ./cui_srv -port 9041 画面を広げておいて host_b $ make バックグランドで3つ起動してみます host_b $ ./cui_test -conn host_a:9041 & host_b $ ./cui_test -conn host_a:9041 & host_b $ ./cui_test -conn host_a:9041 & host_a 側に表示でます =================================== cui_srv ==================================== |/ 0 |/ 1 |/ 2 | | |============================== cui_test ============================== | ||/ foo |/ bar |/ hoge |/ term |/ timer |/ num |/ tab test |/ termin || | || | | || File| [ ] Show (move) | | || [foo ] | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | |+--------------------------------------------------------------------+ | | | | | | | | | +------------------------------------------------------------------------------+ 最初に追加されたタブが選択された状態を保ってます タブ0のシートフォーカスでENTER タブ0の中の世界に入って cui_test のタイトルバーを操作して中央へ移動しておきます 上矢印で、タイトルバーにフォーカス ENTER、矢印キーを操作して、ENTER で移動完了です =================================== cui_srv ==================================== |/ 0 |/ 1 |/ 2 | | | | | | | | | ============================== cui_test ============================== | | |/ foo |/ bar |/ hoge |/ term |/ timer |/ num |/ tab test |/ termin || | | | | | | | File| [ ] Show (move) | | | | [foo ] | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | +--------------------------------------------------------------------+ | | | +------------------------------------------------------------------------------+ ^C でメニューを出して、項目^Cを選択して、またメニューを出して、項目Quitを選択します =================================== cui_srv ==================================== |/ 0 |/ 1 |/ 2 | | | | | | | | | Cancel |====================== cui_test ============================== | | ^C |/ bar |/ hoge |/ term |/ timer |/ num |/ tab test |/ termin || | | Redraw | | | | Disconn| [ ] Show (move) | | | HTML | ] | | | Quit | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | +--------------------------------------------------------------------+ | | | +------------------------------------------------------------------------------+ これで、タブ 0 は終了で消えます =================================== cui_srv ==================================== |/ 1 |/ 2 | | |============================== cui_test ============================== | ||/ foo |/ bar |/ hoge |/ term |/ timer |/ num |/ tab test |/ termin || | || | | || File| [ ] Show (move) | | || [foo ] | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | |+--------------------------------------------------------------------+ | | | | | | | | | +------------------------------------------------------------------------------+ 矢印キーで...OK フォーカス移動します
cui_srv はクライアントを待つだけですが、 cui_test 側も -srv オプションで待ち受けた場合、 どちらも待っていてラチがあきません
そんなときは、ncコマンドで、仲介してもらいます
host_a $ ./cui_srv -port 9042 host_b $ ./cui_test -srv 9043 どちらも接続を待ってます host_c $ nc host_a 9042 -e 'host_b 9043' の2つのncコマンドで仲介すると host_a の cui_srv の画面に ========================= cui_srv ========================== |/ 0 | | |============================== cui_test ==================| |============ Show (move) | || [foo ] | | |+ + | || + | || + | || | | || | | | | | | | | | | | | | | | | | | | | +----------------------------------------------------------+ おっと、cui_srvのサイズが小さいままでした リサイズボックスで広げます これだけでは、中の世界は何も判ってませんので、 中の世界に再描画させます まず、シートにフォーカスしてENTER ^C でシートに対するメニューから、項目^Cを選択 これで、中の世界のメニューが表示されるので、 Redrawを選択 =================================== cui_srv ==================================== |/ 0 | | |============================== cui_test ============================== | ||/ foo |/ bar |/ hoge |/ term |/ timer |/ num |/ tab test |/ termin || | || | | || File| [ ] Show (move) | | || [foo ] | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | |+--------------------------------------------------------------------+ | | | | | | | | | +------------------------------------------------------------------------------+ 問題なさそうです
さて、cui_srv でキー入力と画面を提供してる「てい」になってますが、
cui_srv もまた、libcui.a で作ってます。
ということは、他の端末に繋ぎにいけるし、複数の他の端末が繋ぎにくるのを待ち受ける事もできます
host_a $ ./cui_srv -port 9044 -srv 9045 -stdio 画面のサイズを広げておいて host_b $ ./cui_test -conn host_a 9044 さらに host_c $ stty raw -echo ; nc host_a 9045 ; stty -raw echo これで、全然別のマシン host_c の端末に、 host_a の cui_srv に host_b の cui_test が繋がってる様が、 そのまま表示されて、キー操作も可能です host_c から host_a の cui_srv を通して、host_b の cui_test を操作する事ができます ^C メニューがネストして使えるので、 host_c から cui_test を終了させる事も、 cui_srv を終了させる事もできてしまいます host_c からは ^C メニューで Disconn を選ぶと、 cui_srv との接続が切れるので、nc コマンドを終了できます なんなら初めに host_c から cui_srv を起動しておいて そこに host_a の cui_srv を繋ぐことも可能です host_c $ ./cui_srv -port 9046 サイズを多めに広げておきます host_a $ ./cui_srv -port 9047 -conn host_c:9046 -stdio host_c の画面は ==================================== cui_srv ===================================== |/ 0 | | |========================= cui_srv ========================== | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | |+----------------------------------------------------------+ | | | | | | | | | | | | | | | | | | | +--------------------------------------------------------------------------------+ host_a で操作して画面を広げて、host_b から host_a のサーバにつなぎます host_b $ ./cui_test -conn host_a:9047 & host_c の画面は ==================================== cui_srv ===================================== |/ 0 | | |================================== cui_srv =================================== | ||/ 0 | | | ||============================== cui_test ============================== | | |||/ foo |/ bar |/ hoge |/ term |/ timer |/ num |/ tab test |/ termin || | | ||| | | | ||| File| [ ] Show (move) | | | ||| [foo ] | | | ||| | | | ||| | | | ||| | | | ||| | | | ||| | | | ||| | | | ||| | | | ||| | | | ||| | | | ||| | | | ||| | | | ||| | | | ||| | | | ||| | | | ||| | | | ||| | | | ||| | | | ||+--------------------------------------------------------------------+ | | || | | || | | |+----------------------------------------------------------------------------+ | | | +--------------------------------------------------------------------------------+ host_b から host_c の cui_srv に直接つなぐには host_b $ ./cui_test -conn host_a:9046 & host_c の画面は ==================================== cui_srv ===================================== |/ 0 |/ 1 | | |================================== cui_srv =================================== | ||/ 0 | | | ||============================== cui_test ============================== | | |||/ foo |/ bar |/ hoge |/ term |/ timer |/ num |/ tab test |/ termin || | | ||| | | | ||| File| [ ] Show (move) | | | ||| [foo ] | | | ||| | | | ||| | | | ||| | | | ||| | | | ||| | | | ||| | | | ||| | | | ||| | | | ||| | | | ||| | | | ||| | | | ||| | | | ||| | | | ||| | | | ||| | | | ||| | | | ||| | | | ||+--------------------------------------------------------------------+ | | || | | || | | |+----------------------------------------------------------------------------+ | | | +--------------------------------------------------------------------------------+ タブ1ボタンが密かに増えてるので、選択すると ==================================== cui_srv ===================================== |/ 0 |/ 1 | | |============================== cui_test ============================== | ||/ foo |/ bar |/ hoge |/ term |/ timer |/ num |/ tab test |/ termin || | || | | || File| [ ] Show (move) | | || [foo ] | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | |+--------------------------------------------------------------------+ | | | | | | | | | | | | | +--------------------------------------------------------------------------------+ host_b で後から起動した方の cui_test が見えてます
ああ、ややこしや。
だんだん訳が判らなくなってきました (@_@)
ここまできたら、次は待ち受けてるクライアントに対して、 cui_srv から繋ぎにいく機能を追加してみます
クライアントもcui_srvも「待ち」のとき、 ncコマンド2つで仲介するのを試してみましたが、 この機能があれば、ncコマンドの力を借りずになんとかなります
指定は、起動時のコマンドラインからにすると、 さすがに不便すぎるので、 CUIの画面で、ホスト名とポート番号を指定するダイアログを作ってみます
host_a $ make host_a $ ./cui_test -srv 9050 -stdio リサイズボックスでサイズを小さくしておきます ============= cui_test ============== |/ foo |/ bar |/ hoge |/ term |/ tim| | | | File| [X] Show (move) | | [foo ] | | ============== title ============| | |+--------------------+ +--------| | || | |Radio | | || Are you sure ? | | ( ) ON| | || | | | | || (OK) (Cancel) | | | | || | | | | |+--------------------+ | | | |+------------------+ +--------| | || | | | || How are you ? | | | || | LONG| | | || (^_^) (T_T) | | +-----------------------------------+ 別のマシンから host_b $ make host_b $ ./cui_srv -port 9051 ========================= cui_srv ========================== |(conn) | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | +----------------------------------------------------------+ (conn)ボタンがつきました cui_test も cui_srv も待ちの状態になってて、 cui_srv から (conn)ボタンで繋ぎにいきます ========================= cui_srv ========================== |(conn) | | +-------------+ | | |[localhost ] | | | |<12345 >| | | | | | | | |(OK) (Cancel)| | | +-------------+ | | | | | | | | | | | | | | | | | | | | | | | +----------------------------------------------------------+ ホスト名のデフォルトが 'localhost' ポート番号のデフォルトが '12345' になってます host_a のポート9050を指定します まず ENTER で、ホスト名が全選択状態になるので、 host_a とキー入力してENTER 下キーでフォーカスを12345に移動して ENTERで全選択状態 9050 とキー入力してENTER (OK)にフォーカス移動して ========================= cui_srv ========================== |(conn) | | +-------------+ | | |[host_a ] | | | |<9050 >| | | | | | | | |(OK) (Cancel)| | | +-------------+ | | | | | | | | | | | | | | | | | | | | | | | +----------------------------------------------------------+ ENTER で、host_a に繋ぎにいきます ========================= cui_srv ========================== |(conn) / 0 | | |============= cui_test ============== | |+/ foo |/ bar |/ hoge |/ term |/ tim+ | || | | || File| [X] Show (move) | | || [foo ] | | || ============== title ============| | || |+--------------------+ +--------| | || || | |Radio | | || || Are you sure ? | | ( ) ON| | || || | | | | || || (OK) (Cancel) | | | | || || | | | | || |+--------------------+ | | | || |+------------------+ +--------| | || || | | | || || How are you ? | | | || || | LONG| | | +----------------------------------------------------------+ 繋がりました host_a の画面と多少表示が違うのは、 cui_srv の縦方向のサイズが小さかったからです host_a の cui_test は、端末の接続をいくらでも待ち受けてますから、 cui_srv から再度 (conn) ボタンで繋ぎにいっても、大丈夫です ========================= cui_srv ========================== |(conn) / 0 |/ 1 | | |============= cui_test ============== | |+/ foo |/ bar |/ hoge |/ term |/ tim+ | || | | || File| [X] Show (move) | | || [foo ] | | || ============== title ============| | || |+--------------------+ +--------| | || || | |Radio | | || || Are you sure ? | | ( ) ON| | || || | | | | || || (OK) (Cancel) | | | | || || | | | | || |+--------------------+ | | | || |+------------------+ +--------| | || || | | | || || How are you ? | | | || || | LONG| | | +----------------------------------------------------------+ これで、host_a の cui_test と host_b の cui_srv は、 二重につながっててます 二重だから頑丈という訳でもなく、 あまり意味はなさそうです ;-p)
ここらでブラッシュアップかけておきます
結構な大改造になってしまい、かなり互換性が無くなってしまいました。 「SMFを読み込み音の波形データを作るプログラム (C言語)」 の方で試してるアプリも、修正せねば...
コマンドラインから -conn host:port を指定し忘れても、 後で^Cメニューから端末へ接続しにいけるように追加してみました
cui_srv から(conn)ボタンでクライアントに接続しにいく要領で、 ^Cメニューの項目'Conn'を選択すると、同じダイアログを表示し、 端末のホスト名とポート番号を指定して、端末へと接続しにいきます
指定用のダイアログは共用するため、dialog.[ch] にのれんわけしました。
接続に失敗したときのメッセージダイアログも追加しておきます
host_a $ make host_a $ ./cui_srv -port 9060 ========================= cui_srv ========================== |(conn) | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | +----------------------------------------------------------+ host_b $ make host_b $ ./cui_test -srv 9061 -stdio リサイズボックスでサイズを狭めておきます ============== cui_test =============== |/ foo |/ bar |/ hoge |/ term |/ timer| | | | File| [ ] Show (move) | | [foo ] | | | | | | | | | | | | | +-------------------------------------+ host_a の cui_srv から、host_b の cui_test に接続してみます host_a の cui_srv から (conn)ボタン ENTER host_b ポート 9601 を指定して ========================= cui_srv ========================== |(conn) | | +-------------+ | | |[host_b ]| | | |<9601 >|| | | | | | | |(OK) (Cancle)| | | +-------------+ | | | | | | | | | | | | | | | | | | | | | | | +----------------------------------------------------------+ (OK) ENTER ========================= cui_srv ========================== |(conn) / 0 | | |============== cui_test =============== | ||/ foo |/ bar |/ hoge |/ term |/ timer| | || | | || File| [ ] Show (move) | | || [foo ] | | || | | || | | || | | || | | || | | || | | |+-------------------------------------+ | | | | | | | | | | | +----------------------------------------------------------+ つづいて host_c でも cui_srv を起動します host_c $ make host_c $ ./cui_srv -port 9062 リサイズボックスで少しサイズを広げておきます ============================ cui_srv ============================ |(conn) | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | +---------------------------------------------------------------+ host_a の cui_srv を操作して、 host_b の cui_test から ^C メニューの Conn で host_c の cui_srv に接続してみます (ああ、ややこしい) host_a の cui_srv から、フォーカスをcui_terminに移動して ========================= cui_srv ========================== |(conn) / 0 | | |//////////////////////////////////////////////////////////| |// foo |/ bar |/ hoge |/ term |/ timer| /| |/ | /| |/ File| [ ] Show (move) | /| |/ [foo ] | /| |/ | /| |/ | /| |/ | /| |/ | /| |/ | /| |/ | /| |/-------------------------------------+ /| |/ /| |/ /| |/ /| |/ /| |//////////////////////////////////////////////////////////| +----------------------------------------------------------+ ENTER で host_b の cui_testへキー入力するモードへ host_a の cui_srv から ^Cメニューで ^C項目を選択すると host_b の cui_test の ^Cメニューが表示されます ========================= cui_srv ========================== |(conn) / 0 | | |Cancel |====== cui_test =============== | |^C |/ bar |/ hoge |/ term |/ timer| | |Redraw | | | |Conn | [ ] Show (move) | | |Disconn| ] | | |HTML | | | |Quit | | | || | | || | | || | | || | | |+-------------------------------------+ | | | | | | | | | | | +----------------------------------------------------------+ Conn を選択して、ホストとポートを指定するダイアログを表示 ========================= cui_srv ========================== |(conn) / 0 | | |============== cui_test =============== | ||+-------------+ hoge |/ term |/ timer| | |||[localhost ]| | | |||<12345 >||w (move) | | ||| | | | |||(OK) (Cancle)| | | ||+-------------+ | | || | | || | | || | | || | | |+-------------------------------------+ | | | | | | | | | | | +----------------------------------------------------------+ host_c の 9062 を指定 ========================= cui_srv ========================== |(conn) / 0 | | |============== cui_test =============== | ||+-------------+ hoge |/ term |/ timer| | |||[host_c ]| | | |||<9062 >||w (move) | | ||| | | | |||(OK) (Cancle)| | | ||+-------------+ | | || | | || | | || | | || | | |+-------------------------------------+ | | | | | | | | | | | +----------------------------------------------------------+ (OK) ENTER host_c の cui_srv の画面に表示が出ます ============================ cui_srv ============================ |(conn) / 0 | | |============== cui_test =============== | ||/ foo |/ bar |/ hoge |/ term |/ timer| | || | | || File| [ ] Show (move) | | || [foo ] | | || | | || | | || | | || | | || | | || | | |+-------------------------------------+ | | | | | | | | | | | | | | | | | | | | | +---------------------------------------------------------------+ 続いて host_a の cui_srv の画面を、 host_c の cui_srv へと繋いでみます まず host_a は、今タブ0の中の世界にキー入力を取られているので、 ^C メニュー Quit 選択して、フォーカスを戻します ========================= cui_srv ========================== |(conn) / 0 | | |//////////////////////////////////////////////////////////| |// foo |/ bar |/ hoge |/ term |/ timer| /| |/ | /| |/ File| [ ] Show (move) | /| |/ [foo ] | /| |/ | /| |/ | /| |/ | /| |/ | /| |/ | /| |/ | /| |/-------------------------------------+ /| |/ /| |/ /| |/ /| |/ /| |//////////////////////////////////////////////////////////| +----------------------------------------------------------+ ^Cメニューで Conn を選択して host_c 9062 を指定 ========================= cui_srv ========================== |+-------------+ | ||[host_c ]|cui_test =============== | ||<9062 >||/ hoge |/ term |/ timer| | || | | | ||(OK)e(Cancle)|ow (move) | | |+-------------+ | | || | | || | | || | | || | | || | | || | | |+-------------------------------------+ | | | | | | | | | | | +----------------------------------------------------------+ (OK) ENTER で host_c の画面にタブ1が追加されます host_c でタブ1 ENTER で ============================ cui_srv ============================ |(conn) / 0 |/ 1 | | |========================= cui_srv ========================== | ||(conn) / 0 | | | ||//////////////////////////////////////////////////////////| | ||// foo |/ bar |/ hoge |/ term |/ timer| /| | ||/ | /| | ||/ File| [ ] Show (move) | /| | ||/ [foo ] | /| | ||/ | /| | ||/ | /| | ||/ | /| | ||/ | /| | ||/ | /| | ||/ | /| | ||/-------------------------------------+ /| | ||/ /| | ||/ /| | ||/ /| | ||/ /| | ||//////////////////////////////////////////////////////////| | |+----------------------------------------------------------+ | | | | | +---------------------------------------------------------------+ host_c の画面に、host_a の cui_srv の画面が表示されます ここまでの操作をまとめると host_a で cui_srv 起動 host_b で cui_test 起動 host_a の cui_srv から host_b の cui_test へ接続 host_a (cui_srv) -- (conn)ボタン --> host_b (cui_test) host_c で cui_srv 起動 host_b の cui_test から host_c の cui_srv へ接続 host_a (cui_srv) -- (conn)ボタン --> host_b (cui_test) -- Connメニュー --> host_c (cui_srv) host_a の cui_srv から host_c の cui_srv へ接続 host_a (cui_srv) -- (conn)ボタン --> host_b (cui_test) -- Connメニュー --> host_c (cui_srv) | ^ | | +-- Connメニュー ----------------------------------------------------+ どちらが待ち受けて、どちらから接続しに行ってるかを気にしないで考えると、 単なるアプリケーションのサーバとして動作してるのは host_b の cui_test です アプリケーション cui_test の入出力を host_a の cui_srv が端末となって、 画面表示とキー入力を処理します 同様に host_c の cui_srv も cui_test へと接続して、 画面表示とキー入力を処理します 最後に、host_a の cui_srv について、今度はアプリケーションとして、 host_c の cui_srv に接続してます これで、host_a の アプリケーションとしての cui_srv の入出力を、 host_c の cui_srv が端末となって、 画面表示とキー入力を処理します いよいよ、訳がわかりませんね (>_<)
cui_srv でタブがどんどん増えてくると、いつかは画面からはみ出します。 今のところ、はみ出たら放ったらかしてるので、これを何とかしてみます。
まず1行だけの入れものの部品 scline を作ってみます。
中には1行だけの部品を入れる事にします。 中に入れてる部品の幅が、自身の幅を超えてたら、 スクロール用のボタンを左右に表示して、スクロール可能にします。
まずは cui_test で試してみます。
(move)ボタン、Fileメニュー、その下のエディットテキストについて、 結果を表示するラベル mv_lb があります。
このmv_lbを、scline に入れてみて、 長い文字列が設定された場合に、スクロールボタンが出てくるか、 試してみます。
$ make $ ./cui_test ============================== cui_test ============================== |/ foo |/ bar |/ hoge |/ term |/ timer |/ num |/ tab test |/ termin || | | | File| [ ] Show (move) | | [foo ] | | | : Fileメニューから ============================== cui_test ============================== |/ foo |/ bar |/ hoge |/ term |/ timer |/ num |/ tab test |/ termin || | | | File| [ ] Show (move) | | Open | ] | | Save | | | Close| | | Quit | | | | : Saveを選択 ============================== cui_test ============================== |/ foo |/ bar |/ hoge |/ term |/ timer |/ num |/ tab test |/ termin || | | | File| [ ] Show (move) Save | | [foo ] | | | : mv_lb の位置に従来通り、表示は出ます [foo ] のエディットテキストにフォーカス ENTER ============================== cui_test ============================== |/ foo |/ bar |/ hoge |/ term |/ timer |/ num |/ tab test |/ termin || | | | File| [ ] Show (move) Save | | [foo ] | : 右矢印で、カーソルを末尾に移動して、 0123456789 と入力して追加し ENTER ============================== cui_test ============================== |/ foo |/ bar |/ hoge |/ term |/ timer |/ num |/ tab test |/ termin || | | | File| [ ] Show (move) 'foo012345678> | | [foo012] | | | : mv_lbラベルの末尾に、スクロール用のボタンが表示されます このボタンにフォーカスを移動してENTER ============================== cui_test ============================== |/ foo |/ bar |/ hoge |/ term |/ timer |/ num |/ tab test |/ termin || | | | File| [ ] Show (move) <oo0123456789' | | [foo012] | | | : スクロールして、今度は先頭にスクロール用のボタンが表示されます もっと長い文字列にするために、 エディットテキストで、さらに abcdefg を追加してみると ============================== cui_test ============================== |/ foo |/ bar |/ hoge |/ term |/ timer |/ num |/ tab test |/ termin || | | | File| [ ] Show (move) <oo0123456789> | | [foo012] | | | : スクロールボタンでスクロールして、 先頭や末尾に達すると、不要なボタンは消えます ============================== cui_test ============================== |/ foo |/ bar |/ hoge |/ term |/ timer |/ num |/ tab test |/ termin || | | | File| [ ] Show (move) <56789abcdefg' | | [foo012] | | | : OKです
それでは、scline の機能をタブに追加してみます。
タブ生成時に、内部で scline を生成して、 自身を scline に入れこむようにしてみます。
スクロールボタンを押したときの、スクロール幅は、
関数を指定して動的に与える事ができるようにしてます。
次のタブの区切りまでの幅を返す関数を作って、指定してみます
scline.c cui_scline_hdr()のスクロールボタンでの処理と、 adj() からの処理で、重複してる箇所があったので削除しておきます
$ make $ ./cui_test ============================== cui_test ============================== |/ foo |/ bar |/ hoge |/ term |/ timer |> | | | | File| [ ] Show (move) | | [foo ] | | | : はやくも、タブにスクロールが出てます タブのスクロールボタンにフォーカスして ENTER ============================== cui_test ============================== |< bar |/ hoge |/ term |/ timer |/ num |> | | | | File| [ ] Show (move) | | [foo ] | | | : 次のタブのnumの区切りまで表示されてます さらに ENTER で 'tab test' を表示させて選択 ============================== cui_test ============================== |< |/ term |/ timer |/ num |/ tab test |> | | | |(add) [X] init_act (del) | | | : (add)でENTER 連打していくと ============================== cui_test ============================== |< |/ term |/ timer |/ num |/ tab test |> | | | |(add) [X] init_act (del) | | | |/ 0 |/ 1 |/ 2 |/ 3 |/ 4 |/ 5 |/ 6 |/ 7 > | | | : 8のタブではハミだして、スクロールボタンが出ました スクロールボタン ENTER で ============================== cui_test ============================== |< |/ term |/ timer |/ num |/ tab test |> | | | |(add) [X] init_act (del) | | | |<0 |/ 1 |/ 2 |/ 3 |/ 4 |/ 5 |/ 6 |/ 7 |> | | | : まず、タブ7の区切りまで表示 さらに、スクロールボタン ENTER で ============================== cui_test ============================== |< |/ term |/ timer |/ num |/ tab test |> | | | |(add) [X] init_act (del) | | | |< 1 |/ 2 |/ 3 |/ 4 |/ 5 |/ 6 |/ 7 |/ 8 | | | | : タブ8まで表示 逆方向のスクロールボタンで ENTER 連打で ============================== cui_test ============================== |< |/ term |/ timer |/ num |/ tab test |> | | | |(add) [X] init_act (del) | | | |/ 0 |/ 1 |/ 2 |/ 3 |/ 4 |/ 5 |/ 6 |/ 7 > | | | : タブ0 が見える状態まで戻れます 適当にタブを選んで (del) すると... (>_<) ============================== cui_test ============================== |< |/ term |/ timer |/ num |/ tab test |> | | | |(add) [X] init_act (del) | | | |/ 2 |/ 4 |/ 5 |/ 6 |/ 7 |/ 8 | > | | | : tab.c cuitab_del() から cui_scline へと、 サイズが変わった事が伝わってないようです
cui_tab_add() では cui_wh_fit() でボタンの追加分を調整してましたが、 cui_tab_del() では 削除したボタン分のサイズについて、何もしてませんした (T_T)
修正しておきます
cui_test の 幅が、無駄に広いので、狭めておきます
次は、数値入力用の cui_num のスクロールバーについて
'|'ボタンでスクロールバーを出したあと、 閉じるのに、一番上の'x'ボタンまでフォーカスを移動するのが、どうも面倒です
スクロールバーの表示が出てるときは、モーダルに入っていて、
スクロールバー以外の部品にフォーカスは移れません。
縦長のバーなので、左右キーやESCキーを押したときに、
'x'ボタンで閉じるのと同じ振舞いをさせてみます
「SMFを読み込み音の波形データを作るプログラム (C言語)」 の CUIから音出ししてみる (その5) の方で、楽器の鍵盤の部品 cui_kbd が出来てきたので、 ライブラリに取り込んでおきます
cui_srv に (close)ボタンを追加しました。
選択してるタブのterminを殺してソケットをcloseします
dialog.[ch] cui_host_port_dialog() について、
以前の指定結果がヒストリとして残ってる方が便利そうなので、
仕様を変えました。
初回の呼び出しで作成したダイアログを、以降も使い回すようにします。
ホスト名とポート番号の初期値は、dialog.c の中で、
'localhost' 12345 に決めうちで、呼び元からは設定しません
term.[ch] の cui_terminal を、cui_termin から継承するようにして、 書き直しました
menu.c に プルダウンメニューを追加しました
cui_srv.c の (conn) (close) ボタンはプルダウンメニューに変更しました
プルダウン・メニューの項目に(shell)を追加して、 cui_terminal を生成して /bin/sh を実行するようにしました
^CメニューからHTMLを選択すると、rootの部品をhide()するときに、 クリアされてしまい、HTML出力後も表示が出てなかったので、 cui_draw() を追加しておきます
$ make $ ./cui_srv ========================= cui_srv ========================== |menu| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | +----------------------------------------------------------+ (conn) (close) ボタンはメニューになりました ========================= cui_srv ========================== |menu| | |conn | | |shell| | | | : menu を開くと conn と shell があります shell を選んでみると ========================= cui_srv ========================== |menu| / sh-0 | | |stty erase ^H | |$ $ | | | : sh-0 のタブが追加されて /bin/sh が走ります 下矢印キーでフォーカスして ========================= cui_srv ========================== |menu| / sh-0 | | |//////////////////////////////////////////////////////////| |/ $ /| |/ /| |/ /| |/ /| |/ /| |/ /| |/ /| |/ /| |/ /| |/ /| |/ /| |/ /| |/ /| |/ /| |/ /| |//////////////////////////////////////////////////////////| +----------------------------------------------------------+ ENTER でshellにキー入力可能になります cui_test を起動してポート9080で待ち受けてみます ========================= cui_srv ========================== |menu| / sh-0 | | |stty erase ^H | |$ $ | |$ | |$ ./cui_test -srv 9080 | | | | | : ^Cでメニューを開いて Quit を選んで、キーフォーカスを戻します ========================= cui_srv ========================== |menu| / sh-0 | | |//////////////////////////////////////////////////////////| |/ $ /| |/ /| |/ ./cui_test -srv 9080 /| |/ /| : menuからconnを選んでダイアログを開きます ========================= cui_srv ========================== |menu| / sh-0 | | |s+-------------+ | |$|[localhost ]| | |$|<12345 >|| | |$|./cui_test -s|v 9080 | | |(OK) (Cancel)| | | +-------------+ | | | : (うう、透けてる...) ポート番号 9080 を指定して shell から実行してる cui_test に繋いでみます ========================= cui_srv ========================== |menu| / sh-0 | | |s+-------------+ | |$|[localhost ]| | |$|<9080 >|| | |$|./cui_test -s|v 9080 | | |(OK) (Cancel)| | | +-------------+ | | | : OK ENTER で ========================= cui_srv ========================== |menu| / sh-0 |/ 1 | | |stty erase ^H | |$ $ | |$ | |$ ./cui_test -srv 9080 | | | | | : タブ 1 が追加されました タブ 1 を選ぶと ========================= cui_srv ========================== |menu| / sh-0 |/ 1 | | |================ cui_test ================ | ||/ foo |/ bar |/ hoge |/ term |/ timer |>| | || | | || File| [ ] Show (move) | | || [foo ] | | |+----------------------------------------+ | || | | || | | : 表示の乱れはサイズのせいです リサイズボックスで、縦方向を +6 拡大して termin にフォーカスして ========================= cui_srv ========================== |menu| / sh-0 |/ 1 | | |//////////////////////////////////////////////////////////| |// foo |/ bar |/ hoge |/ term |/ timer |>| /| |/ | /| |/ File| [ ] Show (move) | /| |/ [foo ] | /| |/----------------------------------------+ /| |/ | /| |/ | /| : ENTER ^Cメニューだして、項目^Cを選択 これで cui_test の中の ^Cメニューが表示されるので、Redrawを選択 ========================= cui_srv ========================== |menu| / sh-0 |/ 1 | | |================ cui_test ================ | ||/ foo |/ bar |/ hoge |/ term |/ timer |>| | || | | || File| [ ] Show (move) | | || [foo ] | | || | | : これでまともっぽい表示になります ^Cメニュー Quit で、フォーカスを手前に戻して menu から close を選ぶと ========================= cui_srv ========================== |menu| / sh-0 |/ 1 | | |================ cui_test ================ | ||/ foo |/ bar |/ hoge |/ term |/ timer |>| | || | | || File| [ ] Show (move) | | || [foo ] | | || | | || | | || | | || +-----------------+ | | || | close tab '1' ? | | | || | | | | || | (OK) (Cancel) | | | || +-----------------+ | | || | | : 確認のダイアログが入ります (OK) ENTER で ========================= cui_srv ========================== |menu| / sh-0 | | |stty erase ^H | |$ $ | |$ | |$ ./cui_test -srv 9080 | | | | | : タブ1を閉じます さらに menu から close で ========================= cui_srv ========================== |menu| / sh-0 | | |stty erase ^H | |$ $ | |$ | |$ ./cui_test -srv 9080 | | | | | | | | | | +--------------------+ | | | close tab 'sh-0' ? | | | | | | | | (OK) (Cancel) | | | +--------------------+ | | | : (OK) ENTER で ========================= cui_srv ========================== |menu| | | | : そして誰もいなくなった ^Cメニュー Quit で cui_srv を終了します
ここまでのソースファイル cui126.tgz
さっそく
CUIから音出ししてみる (その6)
の方で使ってみると、
cui_kbd のプロトタイプ宣言が一つ抜けてました。
修正しておきます
さらに cui_kbd バグ修正です。
オクターブの値が -1 に達するとダメでした。
オクターブの値が -1 のとき C より低い音を選んだときの動作を修正しました
スクロールパネルのリサイズハンドラで、 スクロールバーの表示/非表示動作を修正しました
使って試してみると、色々不便な点が目につきます。 現状では、ハンドラからオブジェクトを判定するのに、 引数のパラメータに工夫する必要がつきまとってます。 ここは一つ、オブジェクトを名前で検索できると少しは楽にならないかと考えました。 その仕組みを追加してみます。
cui_base のメンバに文字列 name を追加します。 デフォルトNULLで、使いたい人が文字列を設定します
検索用に
cui cui_name_child(cui obj, char *name)
という関数を追加しておきます。 objおよび、その子供達をサーチして、 メンバ name が引数 name と一致するか判定します。 ハンドラには、根元の方の入れものをパラメータとして渡しておいて、 ハンドラの中では名前で検索すれば、オブジェクトの判定の場面で使えそうです。 さぁこれで果たして便利になるかどうか?
部品を並べるのに、直前に追加した部品の直後や、 真下に追加する場面が多くあります。 その都度、直前の部品のメンバx,yを参照したり、 cui_x2(), cui_y2()関数を使ってきました。 こんなとき、 直前に配置した部品は、必ずparentのchildrenメンバの 先頭に並んでるはずです。 ならば生成時では、直前に配置したの部品を変数に覚えておいて、指定しなおす必要もないはずです。 ここは座標x,yの指定側で、ちょっと工夫してみます。
まず使用しないような非現実的な大きな値の領域を、 マジックナンバーとして使います。 CUI_CHILD_X という値を指定したら、 直前に追加した部品と同じx座標の値を表す事にします。 CUI_CHILD_Y は同様に同じy座標の値。 CUI_CHILD_X2 は、直前に追加した部品に対して、 cui_x2() を適用した値、つまりx座標+幅wの値。 CUI_CHILD_Y2 は、同様にcui_y2()値とします。 そのマジックナンバーの4種類の値から、+100までは予約しておいて、 オフセットを指定するのに使うようにします
ポップアップメニューや、数値入力部品では、 直前にラベル、直後にラベルを配置する場面が多くあります。 その用途に応えて、cui_wlb_new() を追加します。
cui cui_wlb_new(cui parent, int x, int y, char *s1, cui obj, char *s2)
入れものを生成して、 文字列s1のラベル、obj、文字列s2のラベルを配置します。 s1やs2がNULLなら、そのラベルは生成しません。 objはとりあえずparentをNULLとして生成しておいて渡してもいいし、 どこかの入れものに入っていてもcui_del()で引き剥がして配置し直します
と思ったら、ポップアップメニューの生成で親の入れものにNULLを指定すると、 メニューアイテムリストの生成がうまくいきませんでした。 ハンドラでアイテムの親の入れものを確認して、設定しなおすように修正してみます
ポップアップメニューを生成するとき、 これまではwに負の値を指定すると、 初期状態で選択されてるアイテムの幅にしてました。 でも後で、もっと大きな幅のアイテムを選ぶと、結局表示しきれません。 なので負の値を指定した時は、最大幅のアイテムの幅に合わせるように変更しておきます
「SMFを読み込み音の波形データを作るプログラム (C言語)」 の ではVCOから でも実装しようかとあれこれ見てると、 あれこれ機能の不足を感じてしまい、ついつい更新してしまいます
cui_wlb_new() では、まだ手間がかかり過ぎます。
cui_wlb_name_new() などとして別途追加してみます
cui cui_wlb_name_new(cui parent, char *s1, char *name, char *s2)
parent に最後に追加した部品をとりはずし、
新たなら入れものを生成して、そこに入れなおします。
生成した入れもの中では、その部品の前後にs1,s2からのラベルを配置します。
生成した入れものは、もともとの部品と同じ x, y 位置へと配置します
メニュー関連で parent NULL 指定時について、pulldown の場合を含む修正分です
ではVCOから で、試してるとどんどん変更入ります。 今度はバグ修正です。 用意してた menu.c cui_menu_int_min_max_set() を初めて使ってみたら、 まともに動きませんでした。 cui_menu_popup() 側と合わせて修正です
etext.c cui_etext の描画ハンドラの処理の修正です
cui_srv で cui_termin を使ってますが、 キー入力の切替えが面倒です
現状では、cui_termin 自体にフォーカスしてからENTERで、
cui_termin へキー入力可能なモードに入り、
独自の cui_main() ループを実行します。
^Cメニューから Quit 選択で、cui_quit() が呼び出され、
cui_termin 独自の cui_main() ループから抜け、
元のモードに戻ります
あまりモードを意識せずに操作できるように、 なんとか改良してみます
cui_termin のキー入力可能なモードに入る方の自動化は簡単です。
cui_termin にフォーカスがくると、val == CUI_DRAW_FOCUS で
描画ハンドラが呼び出されるので、
そこで判定してモードに入れてしまえば良さそうです
モードに入った後、どうやって自動的に抜けるかが問題です。
思いつくのはまず、
cui_termin の中の接続先でのキー処理で、
矢印キーが押されたものの、すでに端っこの部品にフォーカスしてて、
これ以上フォーカス移動が発生しない状況のときです。
このタイミングで、cui_termin がキー入力可能なモードから抜ければ、
それなりに連続した感覚で、手前の世界に戻ってこれそうです
では、接続先で発生した、矢印キーでフォーカス移動しなかった状況を、
どのように手前の cui_termin に伝えるべしか?
接続先からの出力は、ひたすら画面出力の文字列や、
エスケープシーケンスの文字列が送られてくるのみです
専用の仕組みを設けるのもなんなので、
エスケープシーケンスを勝手に使ってみる事にします。
esc (0x1b) H (0x48) は、カーソルをホームポジションへの移動です。
これを勝手に使ってみます。
cui_termin から、出力に使ってる cui_term ここで、
エスケープ処理が入ってます。
esc + 'H' がきたら、カーソルをホームポジションへ移動しつつ、
こっそり cui_quit() を呼び出して、cui_main() のループを一つ
抜ける事にします。
cui_termin じゃなくて、いわゆる世間の端末で見てる分には、
一瞬カーソルがホームポジションに移動しますが、
CUIで使ってる場合、カーソル表示を消してるので、
それすら判らないはずです。
CUIの画面の描画は、頻繁にカーソル移動して、文字列を描画するので、
途中で、一時的にホームポジションに移動しようが、
大勢に影響ないはずです
$ make $ ./cui_srv -port 9096 別端末から $ ./cui_test -conn 9096 実行 cui_srv では ========================= cui_srv ========================== |menu| / 0 | | |================ cui_test ================ | ||/ foo |/ bar |/ hoge |/ term |/ timer |>| | || | | || File| [ ] Show (move) | | || [foo ] | | |+----------------------------------------+ | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | +----------------------------------------------------------+ 表示が乱れてますが 上矢印キーで cui_srv のタイトルバーにフォーカスが移動 ========================= cui_srv ========================== |menu| / 0 | | |================ cui_test ================ | ||/ foo |/ bar |/ hoge |/ term |/ timer |>| | || | | : 左矢印でタブ0にフォーカス移動 ========================= cui_srv ========================== |menu| / 0 | | |================ cui_test ================ | ||/ foo |/ bar |/ hoge |/ term |/ timer |>| | : さらに下矢印でフォーカスが消えたようになりますが、 cui_test を表示してる cui_termin がフォーカスをとりました 上矢印で、cui_test のタイトルバーにフォーカスします ========================= cui_srv ========================== |menu| / 0 | | |================ cui_test ================ | ||/ foo |/ bar |/ hoge |/ term |/ timer |>| | || | | : 矢印キーで cui_test の中をフォーカス移動可能です。 cui_test のタイトルバーにフォーカスを戻して、 さらに上矢印で、もう端っこなので移動できずに、表示はそのまま。 でも、これで cui_termin のメインループからは抜けてます。 さらに上矢印で、cui_srv のタイトルバーにフォーカス出来ます。 ========================= cui_srv ========================== |menu| / 0 | | |================ cui_test ================ | ||/ foo |/ bar |/ hoge |/ term |/ timer |>| | || | | :
それでもまだ、反応が無い部分が判りにくいです。
まぁ矢印キーだけで出入りできるようになったので、
一旦よしとします
ここでまた 「SMFを読み込み音の波形データを作るプログラム (C言語)」 の 続いてエンベロープ以降 からの修正を取り込みます
パネル関係の修正と、タイトルつきパネルの追加です
さらにスクロールパネルの修正です。
サイズ(0,0)とかん小さいサイズでスクロールパネルを生成して、
後でリサイズするとスクロールバーがちゃんと表示されませんでした。
スクロールバーは生成時に指定のサイズの縦横比をみて、
自分が縦タイプか横タイプか決めてるので、
サイズ(1,1)とか指定されると、おかしくなってました。
スクロールパネルから、スクロールバーを生成するところで、
サイズが小さいときは、スクロールバーのサイズを大きく指定するよう修正しました
チェックボックスのAPIにも、ポップアップメニューや数値入力部品と同様に、 vp_set() を追加しました
このところ子育てと本業が忙しくもあり、実に一年以上の間があきました。
久しぶりにMIDIの方でCUIをいじってみて、
あまりにフォーカス移動の操作性がままならず...
フォーカス移動の改良しようと思い立ちました
このあたり以前にもいじってたはずで...、
このあたりで改善にトライしてました。
このときは、結局中心の座標に回帰して終ってしまったもよう
うーむむむ。どうすべしかと、うなってみて...
「最短距離で一番近いもの」
これでいいんじゃないのだろうか?
底では2つの部品の距離の算出に落とし込んでるはずで
focus.c int cui_focus_glen(cui o1, cui o2, int key)
o1 が検査対象の部品で移動先の候補、o2 が今フォーカスがきてる移動元です。 keyには入力されたキーの情報で、上下左右の種別が指定されています。
この処理を、一から見直してみます。 考えを巡らしてみて、色々浮かんだ事を整理してみると
距離のY成分ねぇ...
こんなところでしょうか
あとは、実装してデバッグして手直し。
実際にMIDIの方で試してみると、かなり改善された感があります。
ですが、あと今ひとつ違和感のある動きをしてます
いじってみると、数値入力の部品あたりにフォーカスしたときに、 左右のインクリメント、デクリメントのボタンにはすぐフォーカスが来るのに、 肝心のテキストボックスには、なかなかフォーカスがあたりません
はて?
コードを見直してみると、なるほどです。
num.c p->fb_dec = cui_fillbtn_new(obj, 0, 0, 1, 1, "<"); eview = cui_base_new(obj, 1, 0, w-3, 1); p->etx = cui_etext_new(eview, -1, 0, w-1, buf); p->fb_inc = cui_fillbtn_new(obj, cui_x2(p->etx), 0, 1, 1, ">"); p->fb_bar = cui_fillbtn_new(obj, cui_x2(p->fb_inc), 0, 1, 1, "|");
テキストボックスの左右のラベル"[", "]"の表示を嫌って、その上に重ねて"<", ">"のボタンを配置してました。
このため、ボタンにフォーカスが来てる状態から、左右矢印キーではテキストボックスは候補に挙がらない訳です。
ここで安易に、重複してても候補に挙がるように変更して試してみると、ダメでした。
例えばインクリメント・ボタンフォーカス状態から上下キーで移動しようとしても、
左右方向にあるテキストボックスが距離0で存在してるため、そっちが優先になります。
さらに、上下キーで移動しようとしても、元のインクリメント・ボタンに移動してしまい、この連鎖から抜けれなくなりました
小手先の対応ではダメ。
諸悪の根源は、重ねて配置してる数値入力用の部品の側のはず。
ということで、数値入力側を改良します。
数値入力部品で使うテキストボックスとしては、左右の"[", "]"ラベルは不要なので、
テキストボックスで、左右のラベルなしの指定が出来ればよさそうです。
テキストボックスのAPI
etext.h cui cui_etext_new(cui parent, int x, int y, int w, char *init_s); void cui_etext_init(cui obj, cui parent, int x, int y, int w, char *init_s);
のあたり...
左右のラベルの文字列 "[", "]" を好きなように指定可能にした上で、
不要のときは NULL を指定すれば、ラベルを生成しないと。
こんな感じでいってみましょう
数値入力用の num.c からは、cui_etext_new() しか使ってなく、 従来のコードをサーチしても cui_etext_init() は使ってませんな...
ならば、cui_etext_init() は必ずラベル指定をする事にして、 cui_etext_new() 側だけバリエーションを増やします
// 従来通り cui cui_etext_new(cui parent, int x, int y, int w, char *init_s); // ラベルなし cui cui_etext_new_no_lb(cui parent, int x, int y, int w, char *init_s); // ラベル指定可能 cui cui_etext_new_lb(cui parent, int x, int y, int w, char *init_s, char *lb_l, char *lb_r); // init() は誰も直接使ってないから、ラベル指定ありに変更 void cui_etext_init(cui obj, cui parent, int x, int y, int w, char *init_s, char *lb_l, char *lb_r);
これらを実装した結果のパッチ
久々の更新で懐かしや...
うーむ。まだ、ちょっと変な動きがあるなぁ...
良く見ると、ここ
focus.c cui_focus_glen() if(o2x + o2w > o1x) return -1; dx = o1x - (o2x + o2w); if(o1y > o2y){ SWAP(o1y, o2y, tmp); SWAP(o1h, o2h, tmp); } dy = o1y <= o2y && o2y <= o1y + o1h ? 0 : o2y - (o1y + o1h);
部品がぴったり隣あってるときも、くいこんでるときも距離の成分は0になると。
なので、一見離れてる方に移動してしまう場合があるのでは
[from][not ] ______[to ]
fromからnotの方がtoより近いように見えるが、計算上はどちらも距離0。
なので、生成順の問題でtoにフォーカス移動する場合もあると
ぴったりの隣接のときは距離の成分1、重なってるときは0というルールにすればよさそう
[a][b] __[c] ___[d]
の場合
これでちょっとは良くなりました
それでも、まだ、狙ったところに行かぬ、はがゆさが...
|====================== cui_tone ======================= | ||ch 2 | note <69 >| adj_vol <1 >| | | ||prog=48 tone_n=1 idx 0| strings | | ||+-- VCO -------------------------------+ | | |||wave1 saw | wave2 saw | | | | このchにフォーカスきてる状態から、右矢印キーを連打していくと |====================== cui_tone ======================= | ||ch 2 | note <69 >| adj_vol <1 >| | | ||prog=48 tone_n=1 idx 0| strings | | ||+-- VCO -------------------------------+ | | |||wave1 saw | wave2 saw | | | | |||[ ] alias_noise1 [ ] alias_noise2 | | | 斜め下に移動して |====================== cui_tone ======================= | ||ch 2 | note <69 >| adj_vol <1 >| | | ||prog=48 tone_n=1 idx 0| strings | | ||+-- VCO -------------------------------+ | | |||wave1 saw | wave2 saw | | | | |||[ ] alias_noise1 [ ] alias_noise2 | | | 次は上に戻ったけど、noteの右端のインクリメント・ボタンにフォーカスきて |====================== cui_tone ======================= | ||ch 2 | note <69 >| adj_vol <1 >| | | ||prog=48 tone_n=1 idx 0| strings | | ||+-- VCO -------------------------------+ | | |||wave1 saw | wave2 saw | | | | |||[ ] alias_noise1 [ ] alias_noise2 | | | 次は右横のスクロール・バーを出すボタン これはOK |====================== cui_tone ======================= | ||ch 2 | note <69 >| adj_vol <1 >| | | ||prog=48 tone_n=1 idx 0| strings | | ||+-- VCO -------------------------------+ | | |||wave1 saw | wave2 saw | | | | |||[ ] alias_noise1 [ ] alias_noise2 | | | 下の idx のメニューにきて |====================== cui_tone ======================= | ||ch 2 | note <69 >| adj_vol <1 >| | | ||prog=48 tone_n=1 idx 0| strings | | ||+-- VCO -------------------------------+ | | |||wave1 saw | wave2 saw | | | | |||[ ] alias_noise1 [ ] alias_noise2 | | | さらに下の wave2 のメニュー |====================== cui_tone ======================= | ||ch 2 | note <69 >| adj_vol <1 >| | | ||prog=48 tone_n=1 idx 0| strings | | ||+-- VCO -------------------------------+ | | |||wave1 saw | wave2 saw | | | | |||[ ] alias_noise1 [ ] alias_noise2 | | | ||| tune <12 >| cent| | | ||| mix <0.5 >| | | | ||| [ ] ring | | | さらに下の tume のスクロール・バーを出すボタン |====================== cui_tone ======================= | ||ch 2 | note <69 >| adj_vol <1 >| | | ||prog=48 tone_n=1 idx 0| strings | | ||+-- VCO -------------------------------+ | | |||wave1 saw | wave2 saw | | | | |||[ ] alias_noise1 [ ] alias_noise2 | | | ||| tune <12 >| cent| | | ||| mix <0.5 >| | | | ||| [ ] ring | | | 上に戻って adj_vol のインクリメント・ボタン
縦横比の問題から、見た目よりもY方向を2倍遠くに扱っているはずなのに、
それでも直感的なX方向への移動になってないような...
ということはX方向が見た目より遠い扱いになっているの?
ポップアップ・メニューのあたりで、吸い寄せられているような?
例えば
|====================== cui_tone ======================= | ||ch 2 | note <69 >| adj_vol <1 >| | | ||prog=48 tone_n=1 idx 0| strings | | ||+-- VCO -------------------------------+ | | |||wave1 saw | wave2 saw | | | | |||[ ] alias_noise1 [ ] alias_noise2 | | | 下の idx のメニューにきて |====================== cui_tone ======================= | ||ch 2 | note <69 >| adj_vol <1 >| | | ||prog=48 tone_n=1 idx 0| strings | | ||+-- VCO -------------------------------+ | | |||wave1 saw | wave2 saw | | | | |||[ ] alias_noise1 [ ] alias_noise2 | | |
この場合
idxメニューとadj_volの左端のディクリメント・ボタンの距離は
X方向 : 5 Y方向 : 1 --> 2倍扱いで 2 なので sqrt(5*5 + 2*2) = sqrt(29)
一方
idxメニューとwave2メニューの距離は
X方向 : 2 Y方向 : 2 --> 2倍扱いで 4 なので sqrt(2*2 + 4*4) = sqrt(19)
確かに、距離が短い方に正しく移動してるのか〜
でも、右矢印キーを押してるのだから、 例え遠くても、感覚的には同じ行である adj_vol 側に移動して欲しいところ
このくらいの調整でどうだろうか?
focus.c cui cui_focus_move_judge(cui obj, int key, cui old, int *val) 呼び出してるのは cui.c cui_main() 一箇所からのみ
について、
引数 axis とかを追加して、それがONなら 「keyの方向の距離の成分が 0 のものだけが候補」という縛りをつけよう
これで試してみよう
うう、focus.c の関数全部に引数を追加してまわるはめになった ...
とりあえず、この方式で、X方向の移動は狙い通りに
全然更新してなかったです。
久々にMacの環境でちょっと試してみたら、おかしかったので修正してみます。
実に半年ぶりの更新。
cui14の「14」は2014年のつもりでしたが、もうすっかり2015年。
工事中...