File: dump-window-events.py

package info (click to toggle)
orca 49.5-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 53,532 kB
  • sloc: python: 98,331; javascript: 281; sh: 64; xml: 27; makefile: 5
file content (189 lines) | stat: -rw-r--r-- 6,247 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
#!/usr/bin/python
# dump-window-events.py
#
# Command-line tool to dump window activation and deactivation events along with
# the window(s) which claim to be active.
#
# Copyright 2025 Igalia, S.L.
# Author: Joanmarie Diggs <jdiggs@igalia.com>
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
# Boston MA  02110-1301 USA.

# pylint: disable=invalid-name
# pylint: disable=wrong-import-position

"""Command-line tool to dump window activation and deactivation events."""

import gi
gi.require_version("Atspi", "2.0")
from gi.repository import Atspi, GLib

def clear_atspi_cache(obj):
    """Clear the AT-SPI cache for a specific object."""

    try:
        Atspi.Accessible.clear_cache_single(obj)
    except GLib.GError:
        pass
    else:
        return

    try:
        Atspi.Accessible.clear_cache(obj)
    except GLib.GError:
        pass
    else:
        return

def get_all_applications():
    """Get all running accessible applications."""

    apps = []
    desktop = Atspi.get_desktop(0)
    for i in range(Atspi.Accessible.get_child_count(desktop)):
        try:
            child = Atspi.Accessible.get_child_at_index(desktop, i)
            # Children of mutter-x11-frames seem to mirror the actual apps, but are not what we
            # want. And Orca ignores them because we need to handle the real app; not some clone.
            if Atspi.Accessible.get_name(child) != "mutter-x11-frames":
                apps.append(child)
        except GLib.GError:
            continue

    return apps

def find_all_active_windows(apps=None):
    """Find all windows which claim to be active.."""

    active_windows = []
    if apps is None:
        apps = get_all_applications()

    for app in apps:
        try:
            for i in range(Atspi.Accessible.get_child_count(app)):
                child = Atspi.Accessible.get_child_at_index(app, i)
                # To ensure we're not basing the decision on stale data.
                clear_atspi_cache(child)
                state_set = Atspi.Accessible.get_state_set(child)
                if state_set.contains(Atspi.StateType.ACTIVE):
                    active_windows.append(child)
        except GLib.GError as error:
            print(f"(error finding active windows: {error})")

    return active_windows

def get_name_and_role(obj):
    """Get the name and role of an accessible object."""

    try:
        name = Atspi.Accessible.get_name(obj) or "<unnamed>"
        role = Atspi.Accessible.get_role(obj).value_nick
        return f"'{name}' {role}"
    except GLib.GError as error:
        return f"(error getting name and role: {error})"

def get_top_level_object(obj):
    """Get the highest level accessible object for a given object."""

    # In well-behaved applications, this should work.
    try:
        application = Atspi.Accessible.get_application(obj)
    except GLib.GError as error:
        print(f"(error getting application: {error})")
    else:
        if application is not None:
            return application

    # Walk up the tree as far as we can. We should be able to ascend all the way to the app.
    # But if we wound up here, we already have breakage and knowing how far we got may help
    # with debugging.
    desktop = Atspi.get_desktop(0)
    result = obj
    while result is not None:
        try:
            parent = Atspi.Accessible.get_parent(result)
        except GLib.GError:
            break
        if parent in (None, desktop):
            break
        result = parent

    return result

def get_object_info(obj):
    """Returns basic info about an accessible object."""

    name_and_role = get_name_and_role(obj)
    top_level_name_and_role = get_name_and_role(get_top_level_object(obj))
    return f"{name_and_role} from: {top_level_name_and_role}"

def on_window_event(e):
    """Handle window activation and deactivation events."""

    event_type = e.type.split(":")[-1].upper()
    print(f"\n{event_type}: {get_object_info(e.source)}")

    apps = get_all_applications()
    active_windows = find_all_active_windows(apps)
    if not active_windows:
        print("No windows have the active state. The following applications are known to AT-SPI2:")
        for i, app in enumerate(apps):
            try:
                print(f"{i+1:2}. {Atspi.Accessible.get_name(app)}")
            except GLib.GError as error:
                print(f"{i+1:2}. {error}")
        return

    if len(active_windows) == 1:
        print(f"The active window is: {get_object_info(active_windows[0])}")
        return

    print(f"{len(active_windows)} windows claim they are active:")
    for i, win in enumerate(active_windows):
        try:
            print(f"{i+1:2}. {get_object_info(win)}")
        except GLib.GError as error:
            print(f"{i+1:2}. {error}")

def on_focus_event(e):
    """Handle focus events for exit condition."""

    if not e.detail1:
        return

    try:
        if Atspi.Accessible.get_role(e.source) == Atspi.Role.TERMINAL:
            print("Exiting.")
            Atspi.event_quit()
    except GLib.GError:
        pass

def main():
    """Starts the window event dumper and waits for events."""

    window_listener = Atspi.EventListener.new(on_window_event)
    window_listener.register("window:activate")
    window_listener.register("window:deactivate")

    focus_listener = Atspi.EventListener.new(on_focus_event)
    focus_listener.register("object:state-changed:focused")

    print("Listening for window activation/deactivation events. Focus a terminal to exit.")
    Atspi.event_main()

if __name__ == "__main__":
    main()