Hammerspoon/hammerspoon — pull request #162 by tmandry (2014-12-21 07:42:28+00:00)
Title: Add uielement extension with watcher capability for applications and windows

This allows watching for accessibility events on any UI element, in theory, though currently only applications and windows are exposed.

The uielement extension is currently bare other than the watcher, but can hold more general UI element functionality shared by both applications and windows in the future.

Closes #58

I see the watcher as being a fairly "low-level" API that can be built on later for more convenience in doing simple tasks. However, I firmly believe that such functionality should be exposed to Lua, so people can do more complex things efficiently and listen for custom events or events that we don't explicitly handle yet.

I will be using this API as I build my own WM extension (which will be fairly complex), and extract abstractions into builtin extensions as they make sense.

Here's sample code that keeps track of open, close, and resize events for all windows (also depends on #161). There's a fair amount of bookkeeping involved and if this is a common use case, we can make an extension for it.

```lua
local events = hs.uielement.watcher

watchers = {}

function init()
appsWatcher = hs.application.watcher.new(handleGlobalAppEvent)
appsWatcher:start()

-- Watch any apps that already exist
local apps = hs.application.runningApplications()
for i = 1, #apps do
if apps[i]:title() ~= "Hammerspoon" then
watchApp(apps[i])
end
end
end

function handleGlobalAppEvent(name, event, app)
if event == hs.application.watcher.launched then
watchApp(app)
elseif event == hs.application.watcher.terminated then
-- Clean up
local appWatcher = watchers[app:pid()]
if appWatcher then
appWatcher:stop()
for id, watcher in pairs(appWatcher.windows) do
watcher:stop()
end
watchers[app:pid()] = nil
end
end
end

function watchApp(app)
if watchers[app:pid()] then return end

local watcher = app:newWatcher(handleAppEvent)
watchers[app:pid()] = {watcher = watcher, windows = {}}

watcher:start({events.windowCreated, events.focusedWindowChanged})

-- Watch any windows that already exist
for i, window in pairs(app:allWindows()) do
watchWindow(window)
end
end

function handleAppEvent(element, event)
if event == events.windowCreated then
if element:isStandard() then
watchWindow(element)
hs.alert.show('window created: '..element:id()..' with title: '..element:title())
end
elseif event == events.focusedWindowChanged then
-- Handle window change
end
end

function watchWindow(win)
local appWindows = watchers[win:application():pid()].windows
if not appWindows[win:id()] then
local watcher = win:newWatcher(handleWindowEvent, {pid=win:pid(), id=win:id()})
appWindows[win:id()] = watcher

watcher:start({events.elementDestroyed, events.windowResized, events.windowMoved})
end
end

function handleWindowEvent(win, event, watcher, info)
if event == events.elementDestroyed then
watcher:stop()
watchers[info.pid].windows[info.id] = nil
else
-- Handle other events...
end
hs.alert.show('window event '..event..' on '..info.id)
end

init()
```

I've tested the code quite a bit and I believe it to be stable. That said, please look over this carefully, as I am new to Objective C!