スリープモードAPI
新しい API を検討する前に、検証用アプリケーションを C# で作ってみました。
Visual Studio 2013 で実装した C# Windows WPF アプリケーションのソースコード:
https://bitbucket.org/nishimotz/nvdademoapp
実行時に bin\Debug または bin\Release に nvdaControllerClient32.dll および nvdaControllerClient64.dll が必要。
新しい API の動作検証をするためにどういうデモアプリが必要か、引き続き検討してみます。
なお、Visual Studio 2013 は 2012 と共存できるので、インストールしても新しい NVDA のソースコードは問題なくビルドできます。
チケット #29342 で提供している ControllerClient のパッケージをビルドするバッチファイルをリポジトリに追加しました。
To ssh://git@bitbucket.org/nvdajp/nvdajp.git 83d4198..658c544 jpbranch -> jpbranch > cd jptools > buildControllerClient.cmd 作成される場所 jptools\nvdajpClient\(VERSION).zip
7z コマンドにパスを通しておく必要があります。
現状の test_isSpeaking.py の動作を NVDA 2013.3jp と Windows 8.1 で確認し、NVDA が起動していないときのエラーメッセージの不具合と、MessageBeep でうまく音が出ない不具合を修正しました。
To ssh://git@bitbucket.org/nvdajp/nvdajp.git 658c544..00e7c95 jpbranch -> jpbranch
アプリケーションごとのスリープモードの切り替え(ラップトップ配列でNVDA+Shift+Z)を「コマンドプロンプト」に対して行ったところ、下記のような動作になっています。
本チケットの「動的に読み上げのオン・オフを制御するAPI」の仕様について、以下の検討が必要と思われます。
もっと具体的なデモプログラムを書きながら、仕様の妥当性を検討するべきかもしれません。
余談ですが、日本語版で独自に追加した isSpeaking() は、読み上げが終了したときにも、読み上げモードが「読み上げなし」「ビープ」の場合にも False を返します。 当然といえば当然なのですが、このAPIを利用するアプリケーション開発者は NVDA の読み上げモードがどうなっているか知る方法がないので、ユーザーが読み上げモードを変更したときには期待通りに動かないプログラムを書いてしまう可能性があります。
もしかすると「設定プロファイル」で特定のアプリケーションだけ音声ドライバーを「音声なし (silence) 」に設定した状態を作ればいいのではないか、と思いました。
しかし実際にやってみると(当たり前なのですが)そのアプリケーションは nvdaController_speakText() でしゃべらせても silence ドライバーを使ってしまうので、このチケットの目的にはそぐわない結果になります。
なお、silence ドライバーは isSpeaking() に対応していないので、「音声なし」ドライバーでは test_isSpeaking.py は無限ループします。
appModuleHandler.py を詳しく読むと、前述の「アプリケーションごとのスリープ」を制御する API を実装するのが、安全なアプローチと思われるので、まずこれをやってみたいと思います。
下記のとおり sleepapi ブランチを作りました。
To ssh://git@bitbucket.org/nvdajp/nvdajp.git * [new branch] sleepapi -> sleepapi
SetNotReadMe 関数の仕様案をいただいているのですが、NVDAHelper と globalCommands の既存の実装に合わせて、また API の呼び出し元のウィンドウハンドルを自動で取得するのが困難と思われるので、下記の仕様案で進めます。
@WINFUNCTYPE(c_long, c_ulonglong, c_long) nvdaController_setAppSleepMode(windowHandle, sleepMode) ウィンドウハンドル windowHandle を持つアプリケーション(プロセス)に 固有のスリープモードを sleepMode にセットする。 sleepMode: True=1 False=0 で指定 返り値は 0: 成功、そのほかの値はエラーコード。 返り値と sleepMode は c_long とする。 windowHandle は c_ulonglong とする。
補足:windowHandle は HWND (void *) なので x86 では32ビット符号なし整数と互換、x86_64 では64ビット符号なし整数と互換であることから、実装をそろえるために大きいほうを使用。
もしかすると NVDA 自身が 32ビットアプリケーションなので、64ビットの HWND の処理は Python ではなく C++ の RPC 側で処理する必要があるかも知れません。
ウィンドウハンドルを受け取る仕様がよいかどうか、もう少し考えてみます。
なお、キーボードでアプリケーションのスリープを On/Off すると、gainFocus と loseFocus のイベントを発生させるように globalCommands が実装されています。 今回作成する API はユーザーの直接の操作ではないことから、これらのイベントを発生させない方向で進めます。
テストプログラムは wxPython と C# XAML 版の両方を作ってみるつもりです。
NVDA 日本語テスト版 jpbeta140118
開発者向けライブラリ nvdajp-client-140118
動的にスリープモードの制御をする API が追加されています。それ以外の一般ユーザー向けの仕様変更はありません。
リポジトリの情報:
To ssh://git@bitbucket.org/nvdajp/nvdajp.git 1de8697..93a2fbf sleepapi -> sleepapi
追加したAPIの説明
@WINFUNCTYPE(c_long, c_ulonglong, c_int) nvdaController_setAppSleepMode(windowHandle, sleepMode)
ウィンドウハンドル windowHandle を持つアプリケーション(プロセス)に固有のスリープモードを sleepMode にセットする。
現在の実装の制約
wxPython で作ったテストプログラム:
# coding: utf-8 from __future__ import unicode_literals import time from ctypes import * import wx DLLPATH = r'..\client\nvdaControllerClient32.dll' clientLib = windll.LoadLibrary(DLLPATH) if clientLib: clientLib.nvdaController_setAppSleepMode.argtypes = [c_ulonglong, c_int] def nvdaRunning(): if clientLib: res = clientLib.nvdaController_testIfRunning() if res == 0: return True return False class MyFrame(wx.Frame): def __init__(self): wx.Frame.__init__(self, None, title="TestApp", size=(300,200)) self.tc = wx.TextCtrl(self, -1, style=wx.TE_MULTILINE) self.tc.Value = "hello\nline2\nline3\n" self.menubar = wx.MenuBar() self.fileMenu = wx.Menu() self.speakItem = self.fileMenu.Append(-1, 'Speak') self.Bind(wx.EVT_MENU, self.OnSpeak, self.speakItem) self.sleepItem = self.fileMenu.Append(-1, 'Sleep') self.Bind(wx.EVT_MENU, self.OnSleep, self.sleepItem) self.wakeupItem = self.fileMenu.Append(-1, 'Wakeup') self.Bind(wx.EVT_MENU, self.OnWakeup, self.wakeupItem) self.quitItem = self.fileMenu.Append(-1, 'Quit', 'Quit application') self.Bind(wx.EVT_MENU, self.OnQuit, self.quitItem) self.menubar.Append(self.fileMenu, '&File') self.SetMenuBar(self.menubar) self.Centre() self.Show(True) self.windowHandle = self.GetHandle() def OnSpeak(self, event): if nvdaRunning(): res = clientLib.nvdaController_speakText(self.tc.Value) def OnSleep(self, event): if nvdaRunning(): res = clientLib.nvdaController_setAppSleepMode(self.windowHandle, 1) print "setAppSleepMode(%x,1):%x" % (self.windowHandle, res) def OnWakeup(self, event): if nvdaRunning(): res = clientLib.nvdaController_setAppSleepMode(self.windowHandle, 0) print "setAppSleepMode(%x,0):%x" % (self.windowHandle, res) def OnQuit(self, event): self.Close() app = wx.App(False) frame = MyFrame() frame.Show() app.MainLoop()
64ビットアプリケーションを上記仕様のAPIで制御できるかどうかが課題ですが、具体的には以下を調査する必要があります:
なお ProcessID は32ビットでも64ビットでも DWORD 固定のようです。
いったん HWND を渡す仕様として API を作ったのですが、ProcessID を最初から API の引数にすれば、もっとシンプルで確実なものになりそうです。
詳しくは 64 ビットアプリを Visual Studio で作りながら調査します。
仕様の再検討も含めて引き続き検討させてください。
NVDA 日本語テスト版 jpbeta140119
開発者向けライブラリ nvdajp-client-140119
動的にスリープモードの制御をする API の仕様を変更しました。
@WINFUNCTYPE(c_long, c_ulong, c_int) nvdaController_setAppSleepMode(procId, sleepMode)
プロセスID procId を持つアプリケーションに固有のスリープモードを sleepMode にセットする。
現在の実装の制約とコメント
リポジトリの情報:
To ssh://git@bitbucket.org/nvdajp/nvdajp.git 93a2fbf..cdda48a sleepapi -> sleepapi
テストプログラムの修正版:
# coding: utf-8 from __future__ import unicode_literals import time from ctypes import * import wx DLLPATH = r'..\client\nvdaControllerClient32.dll' clientLib = windll.LoadLibrary(DLLPATH) if clientLib: clientLib.nvdaController_setAppSleepMode.argtypes = [c_uint, c_int] procId = windll.kernel32.GetProcessId(windll.kernel32.GetCurrentProcess()) def nvdaRunning(): if clientLib: res = clientLib.nvdaController_testIfRunning() if res == 0: return True return False class MyFrame(wx.Frame): def __init__(self): wx.Frame.__init__(self, None, title="TestApp", size=(300,200)) self.tc = wx.TextCtrl(self, -1, style=wx.TE_MULTILINE) self.tc.Value = "hello\nline2\nline3\n" self.menubar = wx.MenuBar() self.fileMenu = wx.Menu() self.speakItem = self.fileMenu.Append(-1, '&Speak') self.Bind(wx.EVT_MENU, self.OnSpeak, self.speakItem) self.sleepItem = self.fileMenu.Append(-1, 'Sleep O&n') self.Bind(wx.EVT_MENU, self.OnSleep, self.sleepItem) self.wakeupItem = self.fileMenu.Append(-1, 'Sleep O&ff') self.Bind(wx.EVT_MENU, self.OnWakeup, self.wakeupItem) self.quitItem = self.fileMenu.Append(-1, '&Quit') self.Bind(wx.EVT_MENU, self.OnQuit, self.quitItem) self.menubar.Append(self.fileMenu, '&File') self.SetMenuBar(self.menubar) self.Centre() self.Show(True) def OnSpeak(self, event): if nvdaRunning(): res = clientLib.nvdaController_speakText(self.tc.Value) def OnSleep(self, event): if nvdaRunning(): res = clientLib.nvdaController_setAppSleepMode(procId, 1) print "setAppSleepMode(%d,1):%d" % (procId, res) def OnWakeup(self, event): if nvdaRunning(): res = clientLib.nvdaController_setAppSleepMode(procId, 0) print "setAppSleepMode(%d,0):%d" % (procId, res) def OnQuit(self, event): self.Close() app = wx.App(False) frame = MyFrame() frame.Show() app.MainLoop()
APIの引数にプロセスIDを渡す仕様はスマートとは言えないのでもう少し調査してみたのですが、例えば後述のような変更をしてもうまくプロセスIDを取得できない(NVDAHelper.py に 0 しか渡されない)ことを確認しています。
具体的には、RPC のクライアント側のコードはすべて IDL から MIDL で生成されたスタブコードで、現状の実装では簡単に手を入れられません。
(scons すると build/x86/client/nvdaController_C.c などのソースが生成される)
nvdaHelper/local/nvdaController.c のコードはクライアント側から見るとリモートプロセスなので nvdaController.c から GetCurrentProcessId() してもクライアント側のプロセスの情報が得られない、という状況です。
(さきほどのテストプログラムで windll.kernel32.GetProcessId(windll.kernel32.GetCurrentProcess()) と書いた部分は GetCurrentProcessId() と等価)
diff --git a/nvdaHelper/local/nvdaController.c b/nvdaHelper/local/nvdaController.c index 5a07ed3..0705651 100644 --- a/nvdaHelper/local/nvdaController.c +++ b/nvdaHelper/local/nvdaController.c @@ -65,5 +65,5 @@ error_status_t __stdcall nvdaController_testIfRunning() { error_status_t(__stdcall *_nvdaController_setAppSleepMode)(const unsigned __int32 procId, const int mode); error_status_t __stdcall nvdaController_setAppSleepMode(const unsigned __int32 procId, const int mode) { - return _nvdaController_setAppSleepMode(procId, mode); + return _nvdaController_setAppSleepMode(GetCurrentProcessId(), mode); } diff --git a/source/NVDAHelper.py b/source/NVDAHelper.py index 7bab0c4..69b50e6 100755 --- a/source/NVDAHelper.py +++ b/source/NVDAHelper.py @@ -99,6 +99,7 @@ def nvdaController_setRate(nRate): @WINFUNCTYPE(c_long, c_ulong, c_int) def nvdaController_setAppSleepMode(procId, mode): + log.info("%d,%d" % (procId,mode)) import appModuleHandler curApp = appModuleHandler.getAppModuleFromProcessID(procId) curApp.sleepMode = True if mode == 1 else False
NVDA 日本語テスト版 jpbeta140119a
開発者向けライブラリ nvdajp-client-140119a
スリープモード制御 API の仕様を再変更して、プロセスIDの受け渡しを不要にしました。
@WINFUNCTYPE(c_long, c_int) nvdaController_setAppSleepMode(sleepMode)
このAPIを呼び出したアプリケーションに固有のスリープモードを sleepMode にセットする。
現在の実装の制約とコメント
リポジトリの情報:
To ssh://git@bitbucket.org/nvdajp/nvdajp.git 8074f14..87399cc sleepapi -> sleepapi
テストプログラムの修正版:
# coding: utf-8 from __future__ import unicode_literals import time from ctypes import * import wx DLLPATH = r'..\client\nvdaControllerClient32.dll' clientLib = windll.LoadLibrary(DLLPATH) def nvdaRunning(): if clientLib: res = clientLib.nvdaController_testIfRunning() if res == 0: return True return False class MyFrame(wx.Frame): def __init__(self): wx.Frame.__init__(self, None, title="TestApp", size=(300,200)) self.tc = wx.TextCtrl(self, -1, style=wx.TE_MULTILINE) self.tc.Value = "hello\nline2\nline3\n" self.menubar = wx.MenuBar() self.fileMenu = wx.Menu() self.speakItem = self.fileMenu.Append(-1, '&Speak') self.Bind(wx.EVT_MENU, self.OnSpeak, self.speakItem) self.sleepItem = self.fileMenu.Append(-1, 'Sleep O&n') self.Bind(wx.EVT_MENU, self.OnSleep, self.sleepItem) self.wakeupItem = self.fileMenu.Append(-1, 'Sleep O&ff') self.Bind(wx.EVT_MENU, self.OnWakeup, self.wakeupItem) self.quitItem = self.fileMenu.Append(-1, '&Quit') self.Bind(wx.EVT_MENU, self.OnQuit, self.quitItem) self.menubar.Append(self.fileMenu, '&File') self.SetMenuBar(self.menubar) self.Centre() self.Show(True) def OnSpeak(self, event): if nvdaRunning(): res = clientLib.nvdaController_speakText(self.tc.Value) def OnSleep(self, event): if nvdaRunning(): res = clientLib.nvdaController_setAppSleepMode(1) print "setAppSleepMode(1):%d" % res def OnWakeup(self, event): if nvdaRunning(): res = clientLib.nvdaController_setAppSleepMode(0) print "setAppSleepMode(0):%d" % res def OnQuit(self, event): self.Close() app = wx.App(False) frame = MyFrame() frame.Show() app.MainLoop()
C# 版のテストプログラム
https://bitbucket.org/nishimotz/nvdademoapp
1da99b4 で setAppSleepMode を使う機能を追加して、64ビットアプリケーションで動作確認できました。
sleepapi ブランチは jpbranch にマージします。
既存 API の不具合や、この他の API については、別のチケットで扱います。
このAPIを読み上げの抑制に使用される開発者の方が結構いらっしゃるようですが、この機能でスリープにされると、点字ディスプレイのスクロールが効かなくなってしまい、非常に困っています。音声読み上げのみ抑制し、点字ディスプレイ等、その他のNVDAの機能に影響を与えないようにすることはできないのでしょうか?
読み上げではなくNVDAのスリープモードを制御するAPIであることを明確にするために概要を更新しました。
NVDA に対応したアプリ開発 についての開発者に向けたメッセージは以前から変わっていません。 https://www.nvda.jp/nvda2017.1jp/ja/readmejp.html#toc64
読み上げだけを無効化し、点字ディスプレイ対応を無効化しないような API を提供できるかどうか、 改めて検討してみたいと思いますが、 API が実行されるタイミングと NVDA の入出力のタイミングによって誤動作が起こり得るので、 そもそも限界があるアプローチです。
一般論としてはアプリ側で accessible name のような情報をカスタマイズしていただくか、 NVDA のアプリモジュールでウィンドウクラスごとに対応するほうが安全と思います。
アドオンの開発、アプリモジュールの NVDA 日本語版への組み込みのお手伝いは (無償でとは限りませんが)可能と思います。 アプリケーションの開発者の方とは個別に相談させてください。
nishimoto への返信 アプリケーション作者からの要望は 動的に読み上げをON/OFFするAPI ではないのでしょうか。 NVDAをスリープモードにする目的はなんでしょうか?
このチケットの 2014-01-17 ごろのコメントで検討の経緯を書いていますが、 まず、将来のメンテナンスを困難にしないために、できるだけ NVDA にすでに備わっている機能を ベースに実装をしたいと考えました。
当時の要望が NVDA からの自発的な読み上げは止めたいが、 コントロールクライアント API の speakText は無効にしたくない、という話だったので、 この方針であれば確実と判断しました。
音声や点字ディスプレイにいちばん正しく対応できるアプリ開発方法は、 コントロールクライアントに依存しないことである、 ということは、そのころにもご説明したと記憶しています。
nishimoto への返信
NVDAはスクリーンリーダーであって音声読み上げエンジンではないのですから、コントロールクライアントに依存しない読み上げを実現することが望ましいことは理解しているつもりです。 ただそれをアプリ開発者の方に分かっていただくのが難しいです。
ソフトウェア開発者のかたから NVDA 日本語版への対応を検討したいので下記のようなAPIを追加してほしいというご要望をいただきました。
関連チケット
#25653 サーチエイド、Voice Popper への対応 https://sourceforge.jp/ticket/browse.php?group_id=4221&tid=25653
#30245 95Reader/PC-Talker互換APIの実装 https://sourceforge.jp/ticket/browse.php?group_id=4221&tid=30245
#29342 コントローラークライアントAPIの拡張 https://sourceforge.jp/ticket/browse.php?group_id=4221&tid=29342
追記(2014年7月10日)
チケットの概要だけお読みになるかたのために、リリース版の仕様を下記に再掲します:
開発者向けライブラリ nvdajp-client-140119a.zip はこのチケットの添付ファイルをご利用ください。