Ticket #32444

スリープモードAPI

Open Date: 2013-11-13 23:15 Last Update: 2017-04-04 17:01

Reporter: nishimoto Owner: (None)
Type: Feature Requests Status: Closed
Component: クライアントAPI MileStone: (None)
Priority: 5 - Medium Severity: 5 - Medium
Resolution: Fixed

Details

ソフトウェア開発者のかたから NVDA 日本語版への対応を検討したいので下記のようなAPIを追加してほしいというご要望をいただきました。

動的に読み上げをON/OFFするAPI(案)

アプリケーションごとのスリープモードと同等の制御を、アプリケーションから動的・即時に行う。

void SetNotReadMe(BOOL enable);

enable = TRUE で呼び出すと、この関数を呼んだアプリケーションをNVDAが読み上げない。
enable = FALSE で呼び出すと、この関数を呼んだアプリケーションをNVDAが読み上げる。

関連チケット

#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日)

チケットの概要だけお読みになるかたのために、リリース版の仕様を下記に再掲します:

@WINFUNCTYPE(c_long, c_int)
nvdaController_setAppSleepMode(sleepMode)

このAPIを呼び出したアプリケーションに固有のスリープモードを sleepMode にセットする。

返り値は 0: 成功、そのほかの値は MS-RPC のエラーコード。返り値の型は c_long (32ビット符号あり整数)
sleepMode: True=1 / Falseは1以外の値 / 型は c_int (32ビット符号あり整数) 

開発者向けライブラリ nvdajp-client-140119a.zip はこのチケットの添付ファイルをご利用ください。

Attachment File List

Ticket History (3/21 Histories)

2013-11-13 23:15 Updated by: nishimoto
  • New Ticket "動的に読み上げをON/OFFするAPI" created
2013-12-22 22:15 Updated by: nishimoto
Comment

新しい 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 のソースコードは問題なくビルドできます。

2014-01-17 19:02 Updated by: nishimoto
Comment

チケット #29342 で提供している ControllerClient のパッケージをビルドするバッチファイルをリポジトリに追加しました。

To ssh://git@bitbucket.org/nvdajp/nvdajp.git
   83d4198..658c544  jpbranch -> jpbranch

> cd jptools
> buildControllerClient.cmd

作成される場所 jptools\nvdajpClient\(VERSION).zip

7z コマンドにパスを通しておく必要があります。

2014-01-17 20:39 Updated by: nishimoto
Comment

現状の 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)を「コマンドプロンプト」に対して行ったところ、下記のような動作になっています。

  • コマンドプロンプトをスリープさせると、コマンドプロンプトを読み上げなくなり、さらに NVDA 制御キーを使う操作も一切無効になる。
  • clientLib.nvdaController_speakText() による音声出力は有効である。

本チケットの「動的に読み上げのオン・オフを制御するAPI」の仕様について、以下の検討が必要と思われます。

  • NVDA の読み上げだけを無効にして、操作は無効にしない、という仕様にするのか?
  • 読み上げも操作もすべて無効にする(アプリケーションごとのスリープと同じ機能)でよいのか?

もっと具体的なデモプログラムを書きながら、仕様の妥当性を検討するべきかもしれません。

余談ですが、日本語版で独自に追加した isSpeaking() は、読み上げが終了したときにも、読み上げモードが「読み上げなし」「ビープ」の場合にも False を返します。 当然といえば当然なのですが、このAPIを利用するアプリケーション開発者は NVDA の読み上げモードがどうなっているか知る方法がないので、ユーザーが読み上げモードを変更したときには期待通りに動かないプログラムを書いてしまう可能性があります。

2014-01-17 23:35 Updated by: nishimoto
Comment

もしかすると「設定プロファイル」で特定のアプリケーションだけ音声ドライバーを「音声なし (silence) 」に設定した状態を作ればいいのではないか、と思いました。

しかし実際にやってみると(当たり前なのですが)そのアプリケーションは nvdaController_speakText() でしゃべらせても silence ドライバーを使ってしまうので、このチケットの目的にはそぐわない結果になります。

なお、silence ドライバーは isSpeaking() に対応していないので、「音声なし」ドライバーでは test_isSpeaking.py は無限ループします。

2014-01-18 10:13 Updated by: nishimoto
Comment

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 版の両方を作ってみるつもりです。

2014-01-18 17:22 Updated by: nishimoto
  • Milestone Update from (None) to 2014.1jp (closed)
  • Resolution Update from None to Fixed
Comment

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 にセットする。

  • 返り値は 0: 成功、そのほかの値は MS-RPC のエラーコード。返り値の型は c_long (32ビット符号あり整数)
  • sleepMode: True=1 False=0 型は c_int (32ビット符号あり整数)
  • windowHandle: c_ulonglong (64ビット符号なし整数)

現在の実装の制約

  • sleepMode == 1 以外の値はすべて False として扱っている
  • x86_64 プロセスの64ビットのウィンドウハンドルは正しく扱えないので、クライアント側は32ビットアプリケーションであることを仮定

wxPython で作ったテストプログラム:

  • メニューの Speak はスリープモードにかかわらずエディットボックスを読み上げる。
  • メニューの Sleep と Wakeup はアプリケーションスリープモードの On/Off を切り替える。
# 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()
2014-01-19 09:56 Updated by: nishimoto
Comment

64ビットアプリケーションを上記仕様のAPIで制御できるかどうかが課題ですが、具体的には以下を調査する必要があります:

  • GetWindowThreadProcessId が 32ビットアプリケーションで使用されたときに 64ビットの HWND を受け取って 64ビットプロセスの ProcessID を返すことができるか?

なお ProcessID は32ビットでも64ビットでも DWORD 固定のようです。

いったん HWND を渡す仕様として API を作ったのですが、ProcessID を最初から API の引数にすれば、もっとシンプルで確実なものになりそうです。

詳しくは 64 ビットアプリを Visual Studio で作りながら調査します。

仕様の再検討も含めて引き続き検討させてください。

2014-01-19 16:18 Updated by: nishimoto
Comment

NVDA 日本語テスト版 jpbeta140119

開発者向けライブラリ nvdajp-client-140119

動的にスリープモードの制御をする API の仕様を変更しました。

@WINFUNCTYPE(c_long, c_ulong, c_int)
nvdaController_setAppSleepMode(procId, sleepMode)

プロセスID procId を持つアプリケーションに固有のスリープモードを sleepMode にセットする。

  • 返り値は 0: 成功、そのほかの値は MS-RPC のエラーコード。返り値の型は c_long (32ビット符号あり整数)
  • procId : c_ulong (32ビット符号なし整数)
  • sleepMode: True=1 False=0 型は c_int (32ビット符号あり整数)

現在の実装の制約とコメント

  • sleepMode == 1 以外の値はすべて False として扱っている
  • 処理系によっては64ビット整数を引数とする DLL 呼び出しができない場合があるため、32ビット引数しか使わないAPI仕様が望ましい。
  • プロセスIDは64ビットシステムであっても DWORD であるため、この仕様であれば32ビットと64ビットの両方のクライアントに対して共通化できる。

リポジトリの情報:

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()
2014-01-19 17:34 Updated by: nishimoto
Comment

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
2014-01-19 18:12 Updated by: nishimoto
Comment

NVDA 日本語テスト版 jpbeta140119a

開発者向けライブラリ nvdajp-client-140119a

スリープモード制御 API の仕様を再変更して、プロセスIDの受け渡しを不要にしました。

@WINFUNCTYPE(c_long, c_int)
nvdaController_setAppSleepMode(sleepMode)

このAPIを呼び出したアプリケーションに固有のスリープモードを sleepMode にセットする。

  • 返り値は 0: 成功、そのほかの値は MS-RPC のエラーコード。返り値の型は c_long (32ビット符号あり整数)
  • sleepMode: True=1 False=0 型は c_int (32ビット符号あり整数)

現在の実装の制約とコメント

  • sleepMode == 1 以外の値はすべて False として扱っている。
  • RPC 呼び出し元のプロセスIDを I_RpcBindingInqLocalClientPID() で取得する。失敗した場合はログにエラーを出力する(同様の処理が NVDAHelper.py の別の場所でも行われている)

リポジトリの情報:

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()
2014-01-21 12:55 Updated by: nishimoto
  • Ticket Close date is changed to 2014-01-21 12:55
  • Status Update from Open to Closed
  • Component Update from (None) to クライアントAPI
Comment

C# 版のテストプログラム

https://bitbucket.org/nishimotz/nvdademoapp

1da99b4 で setAppSleepMode を使う機能を追加して、64ビットアプリケーションで動作確認できました。

sleepapi ブランチは jpbranch にマージします。

既存 API の不具合や、この他の API については、別のチケットで扱います。

2014-07-10 17:43 Updated by: nishimoto
2014-07-10 17:49 Updated by: nishimoto
  • File nvdajp-client-140119a.zip (File ID: 5115) is attached
2014-07-10 17:52 Updated by: nishimoto
  • Details Updated
2017-04-04 05:17 Updated by: dream945
Comment

このAPIを読み上げの抑制に使用される開発者の方が結構いらっしゃるようですが、この機能でスリープにされると、点字ディスプレイのスクロールが効かなくなってしまい、非常に困っています。音声読み上げのみ抑制し、点字ディスプレイ等、その他のNVDAの機能に影響を与えないようにすることはできないのでしょうか?

2017-04-04 08:42 Updated by: nishimoto
  • Summary Updated
2017-04-04 08:56 Updated by: nishimoto
Comment

読み上げではなくNVDAのスリープモードを制御するAPIであることを明確にするために概要を更新しました。

NVDA に対応したアプリ開発 についての開発者に向けたメッセージは以前から変わっていません。 https://www.nvda.jp/nvda2017.1jp/ja/readmejp.html#toc64

読み上げだけを無効化し、点字ディスプレイ対応を無効化しないような API を提供できるかどうか、 改めて検討してみたいと思いますが、 API が実行されるタイミングと NVDA の入出力のタイミングによって誤動作が起こり得るので、 そもそも限界があるアプローチです。

一般論としてはアプリ側で accessible name のような情報をカスタマイズしていただくか、 NVDA のアプリモジュールでウィンドウクラスごとに対応するほうが安全と思います。

アドオンの開発、アプリモジュールの NVDA 日本語版への組み込みのお手伝いは (無償でとは限りませんが)可能と思います。 アプリケーションの開発者の方とは個別に相談させてください。

2017-04-04 15:16 Updated by: dream945
Comment

nishimoto への返信 アプリケーション作者からの要望は 動的に読み上げをON/OFFするAPI ではないのでしょうか。 NVDAをスリープモードにする目的はなんでしょうか?

2017-04-04 16:27 Updated by: nishimoto
Comment

このチケットの 2014-01-17 ごろのコメントで検討の経緯を書いていますが、 まず、将来のメンテナンスを困難にしないために、できるだけ NVDA にすでに備わっている機能を ベースに実装をしたいと考えました。

当時の要望が NVDA からの自発的な読み上げは止めたいが、 コントロールクライアント API の speakText は無効にしたくない、という話だったので、 この方針であれば確実と判断しました。

音声や点字ディスプレイにいちばん正しく対応できるアプリ開発方法は、 コントロールクライアントに依存しないことである、 ということは、そのころにもご説明したと記憶しています。

2017-04-04 17:01 Updated by: dream945
Comment

nishimoto への返信

NVDAはスクリーンリーダーであって音声読み上げエンジンではないのですから、コントロールクライアントに依存しない読み上げを実現することが望ましいことは理解しているつもりです。 ただそれをアプリ開発者の方に分かっていただくのが難しいです。

Edit

Please login to add comment to this ticket » Login