Curl Global Community
フォーカスを受け付けないボタン押下時に、別テキストフィールドの入力確定を行う - Printable Version

+- Curl Global Community (https://communities.curl.com)
+-- Forum: Discussions (https://communities.curl.com/forumdisplay.php?fid=1)
+--- Forum: General Curl questions (https://communities.curl.com/forumdisplay.php?fid=2)
+--- Thread: フォーカスを受け付けないボタン押下時に、別テキストフィールドの入力確定を行う (/showthread.php?tid=897)



フォーカスを受け付けないボタン押下時に、別テキストフィールドの入力確定を行う - umemura - 05-15-2013

あるテキストフィールドに入力された内容にしたがって、
画面の情報を書き換えるような処理はよくあると思います。

また、画面上のボタンを押して、画面情報をサーバに送信して登録する、
という処理もよくあると思います。

このボタンが、フォーカスを受け付けない、もしくはショートカットキーが割り振られている場合に、
テキストフィールドへの入力直後にボタン押下イベントを起こすと、
ボタン押下イベントが、テキストフィールドの入力確定イベントより先に走ると思います。

ユーザは、入力した内容で更新された状態(入力確定イベント後)に、
ボタン押下イベントが走ることを期待します。

フォーカスを持たないボタンの押下時に、
先に入力確定イベントを起こすためには、どうすればよいのでしょうか。




RE: フォーカスを受け付けないボタン押下時に、別テキストフィールドの入力確定を行う - dyoshida - 05-16-2013

GUI関連のイベント処理順を自力で制御するのは難易度、リスク共に高そうなので、
ボタン押下時と同様のイベントを疑似的に発生させて各種イベントの処理順は
システムにおまかせ、という方法が無難ではないかと思います。

プログラムでボタン押下時と同じような動作を行わせるのは、次の手順でできそうです。

 1. フォーカスをボタンに移動させる
   →入力中のテキストフィールドからフォーカスが外れて入力が確定する

 2.ボタン押下イベントをイベントキューに積む
   →イベントキューに積んでいるので、先に行うフォーカス移動に付随して発生する
    イベントが処理されてからボタン押下イベントハンドラが呼ばれる

一例として、テキストフィールドにIMEで日本語入力中、変換を確定していない状態で、
ショートカットキーで処理を実行した場合でも、GUIのボタンを押した場合と同様に
日本語変換を確定して文字列を取得できるコードを書いてみました。

Code:
{value
    || コピー元TextField
    def src-tf =
        {TextField
            width=3cm
        }
    || コピー先TextField
    def dst-tf =
        {TextField
            width=3cm,
            editable? = false
        }
    || コピー実行コマンドボタン
    def copy-button =
        {CommandButton
            label = "&Copy",
            style = "standard",
            {on Action at btn:CommandButton do
                || TextFieldに入力されて確定されている値をコピー
                {set dst-tf.value = src-tf.value}
            }
        }

    || Ctrl-cキー押下時イベント
    def on-ctrl-c-key-proc =
        {proc {}:void
            || コマンドボタンへフォーカスを移動する
            {copy-button.become-active-from-traversal}

            || ※ 入力中の TextField からフォーカスが外れた際に発生するイベントが
            || 全て処理された後にコマンドボタンのActionイベントハンドラを実行させる為、
            || handle-eventメソッドで直接Actionイベントハンドラを呼び出さずに
            || イベントキューにコマンドボタンの"Action"イベントを積む
            {copy-button.enqueue-event {Action}}
        }

    || キーボードショートカットを登録
    {{get-gui-manager}.add-event-handler
        {on kp:KeyPress do
            {if kp.ctrl? and kp.value == 'c' then
                {on-ctrl-c-key-proc}
            }
        }
    }

    ||
    {VBox
        halign="center",
        {VBox
            {text 上段のテキストを下段へコピーします},
            {text 「Copy」ボタンまたはCtrl+cキーを押してください}
        },
        src-tf,
        copy-button,
        dst-tf
    }
}



RE: フォーカスを受け付けないボタン押下時に、別テキストフィールドの入力確定を行う - umemura - 05-17-2013

説明不足で申し訳ありません。
「ボタンがフォーカスを受け付けない」というのがポイントです。
そのため、ボタン自身のActiveTraverser では、
現在入力されているコントロールからフォーカスを奪えないと思っています。

おそらく、FocusManager を取得し、FocusManager.keyboard-focus-target に対して、
FocusOut イベントを handle-event で発行すればよいのかな、と思っています。

そのためには、ボタンに対して Action イベントが発生した際には、必ず、上記処理を行うように
ボタンのhandle-event をオーバーライドする必要があるという認識です。


さらには、FocusOutによって実行させた処理が Dailog を表示するような処理で、
ボタン押下イベントも同様にDialog を表示する処理の場合、
各イベントが個別に走ると、通常ひとつしか表示されないDialogが、複数重なって表示される、
ということがありえるため、
先に実行された処理が終わるまで、ボタンの処理を止めたい、という要望もあると思います。

この問題に対応するためには、dispatch-events が利用できるのではないかと思うのですが、
正確な実装方法がイメージできません。
(そもそも、フォーカスを受け付けないボタンが鬼っ子な気もするのですが・・・)



RE: フォーカスを受け付けないボタン押下時に、別テキストフィールドの入力確定を行う - dyoshida - 05-19-2013

失礼しました、例では普通のコマンドボタン使ってしまいましたが状況が違いすぎますね…

ボタンがフォーカスを受け付けないというのがどのような状況かわかっていないのですが、
ご説明ではテキストフィールドの入力確定イベントはそのボタンを押せば起きているよう
ですので、おそらくそのボタンはキーボードイベントをハンドルしていないグラフィック
オブジェクトなのではないかと思います。

この理解があっていれば、テキストフィールドの外側の背景をクリックしたときと同じ
ように、とにかく別のオブジェクトにフォーカスが移動するようにすれば事足りそうな
気もします。(全然、見当違いのとこを言っていたらすみません…)

先のコードのコマンドボタンの代わりにVBoxを使うように直してみました。
勉強不足なもので、お作法的に正しいのかちょっと自信がないですがとりあえず
期待しているようには動きました。
Code:
{value
    || コピー元TextField
    def src-tf =
        {TextField
            width=3cm
        }
    || コピー先TextField
    def dst-tf =
        {TextField
            width=3cm,
            editable? = false
        }
    || コピー実行ボタン代わりのグラフィックオブジェクト
    def copy-button =
        {VBox
            halign="center",
            height = 1cm,
            width = 2cm,
            background = {FillPattern.get-orange},
            border-color = {FillPattern.get-red},
            border-width = 2pt,
            {Fill},
            {text Copy},
            {Fill},

            {on e:PointerPress at vb:VBox do
                {if e.button == left-button then
                    {copy-button.enqueue-event {Action}}
                }
            },
            {on Action do
                || TextFieldに入力されて確定されている値をコピー
                {set dst-tf.value = src-tf.value}
            }
        }

    || Ctrl-cキー押下時イベント
    def on-ctrl-c-key-proc =
        {proc {}:void
            || 入力中のTextFieldのキーボードフォーカスを外すため、適当なVisualに
            || フォーカスをセットするようにリクエスト(ここではボタン代わりのVBoxへ)
            {copy-button.request-key-focus}

            || ※ 入力中の TextField からフォーカスが外れた際に発生するイベントが
            || 全て処理された後にコマンドボタンのActionイベントハンドラを実行させる為、
            || handle-eventメソッドで直接Actionイベントハンドラを呼び出さずに
            || イベントキューにコマンドボタンの"Action"イベントを積む
            {copy-button.enqueue-event {Action}}
        }

    || キーボードショートカットを登録
    {{get-gui-manager}.add-event-handler
        {on kp:KeyPress do
            {if kp.ctrl? and kp.value == 'c' then
                {on-ctrl-c-key-proc}
            }
        }
    }

    ||
    {VBox
        halign="center",
        {VBox
            {text 上段のテキストを下段へコピーします},
            {text 「Copy」ボタンまたはCtrl+cキーを押してください}
        },
        src-tf,
        copy-button,
        dst-tf
    }
}

>(そもそも、フォーカスを受け付けないボタンが鬼っ子な気もするのですが・・・)

単に見栄えの良いボタン代わりのものをグラフィックオブジェクトで作るとかでしたら
普通にやるだろうなぁと思うのですが、むしろ鬼っ子はこちらのような…

 >さらには、FocusOutによって実行させた処理が Dailog を表示するような処理で、

Curlではなく他のウィンドウシステムで経験した話になってしまいますが、フォーカス
Outのイベントハンドラ内で、同期完了の関数を呼び出してその中でユーザに確認を
求めるダイアログを出すという仕様は鬼門だった覚えが…

こちら、ちょっと実験してみます。


RE: フォーカスを受け付けないボタン押下時に、別テキストフィールドの入力確定を行う - umemura - 05-20-2013

「フォーカスを受け付けない」というのは、「takes-focus?=false」のことを言っていました。
回りくどくてすいません。

handle-event をオーバーライドして、Action イベント時に、
画面内のフォーカスを持っているコントロールに対して、強制的にFocusEventを発行させ、
関連する処理がすべて流れてから Actionの処理を流す、という意図のコードを書いてみました。

dispatch-events がこれで意図通り動くのか、 get-focus-manager で
フォーカスマネージャが取得できなかったときにどうすべきかなど、
いろいろ不十分な点がありそうです。


また、たしかに、ボタンでなく、グラフィックでボタンと同様の機能を作るときには、
別途方法を考えなくてはいけないかもですね。



Code:
{define-proc public {clean-screen-focus g:Graphic}:void
    ||ファンクションキーなど、フォーカスを持たないボタンが押された時に、
    ||画面内のチェックを行う場合などで、
    ||コントロールの値確定時に画面の内容を修正する機能が実行されない。
    ||上記の問題を対応するために、ボタンが押されたら、
    ||一度、画面内のフォーカスをコントロールからフォーカスアウトをさせ値確定をさせる
    {if-non-null fm = { {g.get-graphical-root}.get-focus-manager }then
        {if-non-null target = fm.keyboard-focus-target then
            {type-switch target
             case cui:ControlUI  do
                ||グリッド内のセルなどで、UIにフォーカスがある場合は、
                ||コントロールで FocusOut イベント発生させる
                ||{cui.control.handle-event {ValueFinished}}
                {cui.control.handle-event {FocusOut}}
             else
                {target.handle-event {FocusOut}}
            }
        }
    }
}
{define-class public CustomCommandButton {inherits CommandButton}
  {constructor public {default ... }
    {construct-super.CommandButton
        {splice ...}
    }
  }
  {method public {handle-event event:Event}:void
    {type-switch event
     case a:Action do
        ||フォーカスを受け付けないボタンの場合、
        ||ValueFinishedなどでイベントを起こすコントロールのイベントを起こすために
        ||現在フォーカスを持っているコントロールをフォーカスアウトさせる
        {if not self.takes-focus? then
            {clean-screen-focus self}
        }
        ||上記 clean-screen-focus で発生させた FocusOut に関連する処理を先に流す
        {dispatch-events true}
        
        {super.handle-event a}
     else
        {super.handle-event event}
    }
  }
}

{value
    || コピー元TextField
    def src-tf =
        {TextDisplay
            width=3cm
        }
    || コピー先TextField
    def dst-tf =
        {TextDisplay
            width=3cm
        }
    ||ValueFinishedで値を反映する
    def input-tf =
        {TextField
            width=3cm,
            {on ValueFinished at tf:TextField do
                set src-tf.value = tf.value
            }
        }

    || コピー実行ボタン代わりのグラフィックオブジェクト
    def copy-button =
        {CustomCommandButton
            label = "&Copy",
            takes-focus? = false,
            {on Action do
                || TextFieldに入力されて確定されている値をコピー
                {set dst-tf.value = src-tf.value}
            }
        }
    ||
    {VBox
        halign="center",
        {VBox
            {text 上段のテキストを下段へコピーします},
            {text 「Copy」ボタンまたはCtrl+cキーを押してください}
        },
        input-tf,
        src-tf,
        copy-button,
        dst-tf
    }
}



RE: フォーカスを受け付けないボタン押下時に、別テキストフィールドの入力確定を行う - dyoshida - 05-20-2013

フォーカスマネージャを取得する例、いろいろ参考になりました!

察するに、他にも様々な制約が絡んでいる場合の対策を検討されているのですね・・・
「テキストフィールドの入力確定」というのも自分が考えていたようなIMEの変換中などという単純なことではなさそうな感が・・・
(IMEの変換ならtakes-focus?=falseを設定したボタンでも押せば確定するようですし)

もしかすると、ボタンに takes-focus?=false を設定しているのは、Curlドキュメントの「テキスト コントロール内での選択」の
項にあるように、テキストフィールドからフォーカスを移動させないためでしょうか?
ボタンがフォーカスを受け付けない、ではなく、テキストフィールドからフォーカスを移動させられない、という事情であれば、
なにか特別な操作が必要になるのも納得です。


(かなりCurlを熟知されているようなので自分なんかの思いつきを書くのは釈迦に説法な感がありますが、以下スレッドタイトルを
 見て来た人向けに一応…)

自分が想定していたような、テキストフィールドからフォーカスを移動してもよく、IMEの変換を確定させるだけの単純なケース
でしたら、他のオブジェクトにフォーカスを移す方法が簡単で安全な方法な気がします。

takes-focus?=falseに設定されているボタンでも、マウスでクリックすればIMEの変換中でもテキストフィールドは入力確定
するようなので、とくに対策は必要なく、ショートカットキーで呼び出すプロシージャの中でボタンのActionイベントをキューへ
積む前に他のオブジェクトにフォーカス移すだけでよさそうですね。

具体的には、最初に挙げたソースの例でいうと、

|| コマンドボタンへフォーカスを移動する
{copy-button.become-active-from-traversal}

これを次のような感じに変えればよいかと

|| テキストフィールドの親(VBox等のグラフィックコンテナ)がフォーカスを
|| 得るようにリクエストして、テキストフィールドのフォーカスを外す
{src-tf.visual-parent.request-key-focus}

なにか適切でなさそうな所がありましたらご指摘頂けるとありがたいです。


RE: フォーカスを受け付けないボタン押下時に、別テキストフィールドの入力確定を行う - Kaneko - 05-21-2013

はじめまして。
わたしは、umemuraさんとほぼ同じですが、clean-screen-focus の部分を
こんな感じで実装しています。
なんとなく。ですが、ValueFinishedとFocusOutを明示的に呼び出すよりも、
manager-release-key-focus を使って、その先の処理はCurlAPIに
任せた方が、後のバージョンアップで問題になりにくそうな気持ちです。
その後、manager-request-key-focus をしているのは、
元の項目に、フォーカスを戻すためで、
TextField入力中に、ショートカットでボタンをActionさせて、
また同じTextFieldで入力が続けられるようにしています。

前提になっているバックグラウンドは、
ショートカットキーでActionが起動されるときに先にFocusOutを発生させたい!
というための実装をしていて、
dyoshidaさんのいうところの
|| イベントキューにコマンドボタンの"Action"イベントを積む
{copy-button.enqueue-event {Action}}

の代わりに呼び出されるメソッドです。

Code:
def cb = [ActionされるCommandButtonです]
    {if-non-null focus-manager = {cb.get-focus-manager} then
        {if-non-null target = focus-manager.keyboard-focus-target then
            || FocusOutでイベント処理(入力チェックなど)を行っている場合があるため、
            || focusを一瞬奪い取り、コントロールでFocusOutイベントを発生させる
            {focus-manager.manager-release-key-focus target = target}
            {focus-manager.manager-request-key-focus target}
        }
        
        || FocusOutイベントの処理を待って処理を実行する
        {after 0s do
            || 念のためFocusOutイベントの処理中にafter0s を使っていても良いように、
            || それも待って処理を実行する
            {after 0s do
                || FocusOutイベント処理中に、popupが表示された場合(エラーメッセージなど)は
                || FunctionKeyの処理を行わない。
                {if focus-manager.have-keyboard-focus? then
                    {if cb.enabled? then
                        {cb.take-action}
                    }
                }
            }
        }
    }

After 0s の使い方と回数は、これでいいのか?という感じもしますが、
うちではこれで動いてます^^;




RE: フォーカスを受け付けないボタン押下時に、別テキストフィールドの入力確定を行う - dyoshida - 05-23-2013

処理の順番を守らせるためにイベントキューへ積むのが目的なら、Kaneko さんの例のように
{after 0s do} を使った方がシンプルかつ応用が効きそうですね、参考になりました。

自分の挙げた例ではFocusOutの後に実行する処理をイベントキューに積むのに、わざわざ
グラフィックに Actionイベントハンドラを追加して積んでいましたが、必要なかったですね・・・



RE: フォーカスを受け付けないボタン押下時に、別テキストフィールドの入力確定を行う - umemura - 05-23-2013

dyoshidaさん、kanekoさん、ありがとうございます。
コメントをいただけると非常にうれしいです。

私の前回のソースでは、ボタンをクリックした後、「マウスを動かさない」ままだと、
処理がそこで止まってしまいました。
dispatch-events true となっているため、クリックした直後に、マウスを動かすなどのイベントがない限り、
次のイベントがくるのを待ち続けてしまう、というのが原因だと思います。

dispatch-events false にすれば動くようにはなったのですが、
「もし、先に流すべきイベントがあれば、そのイベントに関連する処理を先に実行してから、
ボタンのActionイベントに入る」という私の意図に沿った処理になっているか少し不安です。

GuiManager.event-handlers が取得できれば、「現時点で先に流すべきイベントがたまっているかどうか」が
わかるのかもしれませんが、このアクセサはプロテクトされているため取得できません。

また、kanekoさんの FocusOut などを利用するのは危ないのでは、というご指摘をヒントに、
コマンドボタンのオーバーライドメソッドを下記のように修正してみました。
こちらのほうが、FocusManager などを取得する必要がないので、シンプルかもしれないですね。

Code:
{method public {take-action}:void
    ||ショートカットキーなどで実行された際に
    ||前段の処理が終了するまで待つ
    def dispatch? = {dispatch-events false}
    {super.take-action}
  }
  {method public {handle-event event:Event}:void
    {type-switch event
     case a:Action do
        {if-non-null v = {self.get-view} then
            ||takes-focus?=false でも、フォーカスが奪えるように
            ||一時的にフォーカスを受け付ける
            {with self.takes-focus? = true do
                ||入力コントロールからフォーカスを奪う
                {self.request-key-focus}
                ||上記のフォーカスを奪ったイベントに関連する処理の実行を待つ
                {dispatch-events false}
                {super.handle-event a}
            }
        }
     else
        {super.handle-event event}
    }
  }