diff --git a/ahk/templates/_daemon.ahk b/ahk/templates/_daemon.ahk index 3fa6e86f..c08731ab 100644 --- a/ahk/templates/_daemon.ahk +++ b/ahk/templates/_daemon.ahk @@ -504,6 +504,20 @@ AHKWinGetPos(ByRef command) { return s } +AHKWinWait(ByRef command) { + title := command[2] + text := command[3] + timeout := command[4] + extitle := command[5] + extext := command[6] + WinWait,%title%,%text%,%timeout%,%extitle%,%extext% + if !ErrorLevel + { + WinGet, output, ID + return output + } +} + CountNewlines(ByRef s) { newline := "`n" StringReplace, s, s, %newline%, %newline%, UseErrorLevel diff --git a/ahk/templates/daemon.ahk b/ahk/templates/daemon.ahk index 72208ebc..0b1c0b44 100644 --- a/ahk/templates/daemon.ahk +++ b/ahk/templates/daemon.ahk @@ -512,6 +512,20 @@ AHKWinGetPos(ByRef command) { return s } +AHKWinWait(ByRef command) { + title := command[2] + text := command[3] + timeout := command[4] + extitle := command[5] + extext := command[6] + WinWait,%title%,%text%,%timeout%,%extitle%,%extext% + if !ErrorLevel + { + WinGet, output, ID + return output + } +} + CountNewlines(ByRef s) { newline := "`n" StringReplace, s, s, %newline%, %newline%, UseErrorLevel diff --git a/ahk/templates/daemon/win_wait.ahk b/ahk/templates/daemon/win_wait.ahk new file mode 100644 index 00000000..1d2f141d --- /dev/null +++ b/ahk/templates/daemon/win_wait.ahk @@ -0,0 +1 @@ +AHKWinWait,{{title}},{{text}},{{timeout}},{{exclude_title}},{{exclude_text}} diff --git a/ahk/templates/window/win_wait.ahk b/ahk/templates/window/win_wait.ahk new file mode 100644 index 00000000..97112e73 --- /dev/null +++ b/ahk/templates/window/win_wait.ahk @@ -0,0 +1,9 @@ +{% extends "base.ahk" %} +{% block body %} +WinWait,{{title}},{{text}},{{timeout}},{{exclude_title}},{{exclude_text}} +if !ErrorLevel +{ + WinGet, output, ID + FileAppend,%output%,* +} +{% endblock body %} diff --git a/ahk/window.py b/ahk/window.py index 804a165b..26cb86ec 100644 --- a/ahk/window.py +++ b/ahk/window.py @@ -513,14 +513,6 @@ def show(self): """ return self._base_method('WinShow') or None - def wait(self, seconds_to_wait=''): - """ - - :param seconds_to_wait: - :return: - """ - return self._base_method('WinWait', seconds_to_wait=seconds_to_wait, blocking=True) or None - def wait_active(self, seconds_to_wait=''): """ @@ -652,6 +644,30 @@ def win_get(self, title='', text='', exclude_title='', exclude_text='', encoding script = self._win_get(title=title, text=text, exclude_title=exclude_title, exclude_text=exclude_text) encoding = encoding or self.window_encoding ahk_id = self.run_script(script) + if not ahk_id: + return None + return Window(engine=self, ahk_id=ahk_id, encoding=encoding) + + def win_wait(self, *, title='', text='', exclude_title='', timeout=0.5, exclude_text='', encoding=None): + """ + WinWait + Wait for a window. If found within the timeout (in seconds), returns a Window object. + If not found, raises a TimeoutError + + ref: https://www.autohotkey.com/docs/commands/WinWait.htm + """ + script = self.render_template( + 'window/win_wait.ahk', + title=title, + text=text, + exclude_title=exclude_title, + timeout=timeout, + exclude_text=exclude_text, + ) + encoding = encoding or self.window_encoding + ahk_id = self.run_script(script) + if not ahk_id: + raise TimeoutError(f'No window found after timeout ({timeout})') return Window(engine=self, ahk_id=ahk_id, encoding=encoding) def _win_set(self, subcommand, *args, blocking=True): @@ -948,6 +964,8 @@ async def win_get(self, *args, **kwargs): encoding = kwargs.pop('encoding', self.window_encoding) script = self._win_get(*args, **kwargs) ahk_id = await self.a_run_script(script) + if not ahk_id: + return None return AsyncWindow(engine=self, ahk_id=ahk_id, encoding=encoding) async def _all_window_ids(self): @@ -1004,7 +1022,7 @@ async def find_window(self, func=None, **kwargs): """ async for window in self.find_windows(func=func, **kwargs): return window # return the first result - raise WindowNotFoundError('yikes') + return None async def find_windows_by_title(self, title, exact=False): """ @@ -1067,3 +1085,18 @@ async def find_window_by_class(self, *args, **kwargs): """ async for window in self.find_windows_by_class(*args, **kwargs): return window + + async def win_wait(self, *, title='', text='', exclude_title='', timeout=0.5, exclude_text='', encoding=None): + script = self.render_template( + 'window/win_wait.ahk', + title=title, + text=text, + exclude_title=exclude_title, + timeout=timeout, + exclude_text=exclude_text, + ) + encoding = encoding or self.window_encoding + ahk_id = await self.a_run_script(script) + if not ahk_id: + raise TimeoutError(f'No window found after timeout ({timeout})') + return AsyncWindow(engine=self, ahk_id=ahk_id, encoding=encoding) diff --git a/docs/README.md b/docs/README.md index 27c75e9e..4fd6d8d2 100644 --- a/docs/README.md +++ b/docs/README.md @@ -94,6 +94,13 @@ win = list(ahk.windows()) # list of all windows win = Window(ahk, ahk_id='0xabc123') # by ahk_id win = Window.from_mouse_position(ahk) # the window under the mouse cursor win = Window.from_pid(ahk, pid='20366') # by process ID + +# Wait for a window +try: + # wait up to 5 seconds for notepad + win = ahk.win_wait(title='Untitled - Notepad', timeout=5) +except TimeoutError: + print('Notepad was not found!') ``` ### Working with windows diff --git a/tests/unittests/test_daemon_async.py b/tests/unittests/test_daemon_async.py index d178b460..a1f926af 100644 --- a/tests/unittests/test_daemon_async.py +++ b/tests/unittests/test_daemon_async.py @@ -177,13 +177,8 @@ async def test_get_calculator(self): async def test_win_close(self): await self.win.close() - try: - win = await self.ahk.win_get(title='Untitled - Notepad') - await win.position - except WindowNotFoundError as e: - pass - else: - raise AssertionError('Expected WindowNotFoundError') + win = await self.ahk.win_get(title='Untitled - Notepad') + assert win is None async def test_find_window_func(self): async def func(win): diff --git a/tests/unittests/test_win_get.py b/tests/unittests/test_win_get.py index f48d2b83..5771ebed 100644 --- a/tests/unittests/test_win_get.py +++ b/tests/unittests/test_win_get.py @@ -34,8 +34,7 @@ def test_win_close(): assert win assert win.position win.close() - with pytest.raises(WindowNotFoundError): - ahk.win_get(title='Untitled - Notepad').position + assert ahk.win_get(title='Untitled - Notepad') is None finally: if p is not None: p.terminate() diff --git a/tests/unittests/test_win_get_async.py b/tests/unittests/test_win_get_async.py index aa6215a7..4e72c060 100644 --- a/tests/unittests/test_win_get_async.py +++ b/tests/unittests/test_win_get_async.py @@ -29,11 +29,12 @@ async def test_get_calculator(self): async def a_win_get(self): win = await self.ahk.win_get(title='Untitled - Notepad') - await win.position + return win def test_win_close(self): asyncio.run(self.win.close()) - self.assertRaises(WindowNotFoundError, asyncio.run, self.a_win_get()) + win = asyncio.run(self.a_win_get()) + assert win is None async def test_find_window_func(self): async def func(win): diff --git a/tests/unittests/test_window.py b/tests/unittests/test_window.py index 604d7eb3..afc58832 100644 --- a/tests/unittests/test_window.py +++ b/tests/unittests/test_window.py @@ -3,11 +3,14 @@ from unittest import TestCase import os, sys +import pytest + project_root = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '../..')) sys.path.insert(0, project_root) from ahk import AHK from ahk.daemon import AHKDaemon +from ahk.window import WindowNotFoundError class TestWindow(TestCase): @@ -99,6 +102,26 @@ def test_title_change(self): def tearDown(self): self.p.terminate() + def test_find_window(self): + win = self.ahk.find_window(title=b'Untitled - Notepad') + assert win.id == self.win.id + + def test_find_window_nonexistent_is_none(self): + win = self.ahk.find_window(title=b'This should not exist') + assert win is None + + def test_winget_nonexistent_window_is_none(self): + win = self.ahk.win_get(title='This should not exist') + assert win is None + + def test_winwait_nonexistent_raises_timeout_error(self): + with pytest.raises(TimeoutError): + win = self.ahk.win_wait(title='This should not exist') + + def test_winwait_existing_window(self): + win = self.ahk.win_wait(title='Untitled - Notepad') + assert win.id == self.win.id + class TestWindowDaemon(TestWindow): def setUp(self): diff --git a/tests/unittests/test_window_async.py b/tests/unittests/test_window_async.py index c0de45e6..1a905921 100644 --- a/tests/unittests/test_window_async.py +++ b/tests/unittests/test_window_async.py @@ -5,6 +5,8 @@ import os import sys +import pytest + project_root = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '../..')) sys.path.insert(0, project_root) from ahk import AHK, AsyncAHK @@ -104,6 +106,26 @@ async def test_title_setter(self): await self.win.set_title('new title') assert await self.win.get_title() != starting_title + async def test_find_window(self): + win = await self.ahk.find_window(title=b'Untitled - Notepad') + assert win.id == self.win.id + + async def test_find_window_nonexistent_is_none(self): + win = await self.ahk.find_window(title=b'This should not exist') + assert win is None + + async def test_winget_nonexistent_window_is_none(self): + win = await self.ahk.win_get(title='This should not exist') + assert win is None + + async def test_winwait_nonexistent_raises_timeout_error(self): + with pytest.raises(TimeoutError): + win = await self.ahk.win_wait(title='This should not exist') + + async def test_winwait_existing_window(self): + win = await self.ahk.win_wait(title='Untitled - Notepad') + assert win.id == self.win.id + def tearDown(self): self.p.terminate() asyncio.run(asyncio.sleep(0.5))