#!/usr/bin/python3 # coding=utf-8 license=Apache-2.0 """ Tool to follow caret position reported by applications. Listens to caret move events and prints and displays the reported positions to ease debugging apps and toolkits. """ import pyatspi import cairo import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk, GLib class BaseListener: def __init__(self, types): self.__types = types def __enter__(self): for t in self.__types: pyatspi.Registry.registerEventListener(self._on_event, t) pyatspi.Registry.start() def __exit__(self, *args, **kwargs): pyatspi.Registry.stop() for t in self.__types: pyatspi.Registry.deregisterEventListener(self._on_event, t) def _on_event(self, event): return False def main(listener): try: with listener as dummy: pass except KeyboardInterrupt: pass class Highlight(Gtk.Window): """ A overlay to highlight an area on screen. TODO: make the window input-less """ def __init__(self, x, y, w, h): super().__init__(type=Gtk.WindowType.POPUP, app_paintable=True) self.set_default_size(w, h) self.move(x, y) self.do_screen_changed(None) def do_screen_changed(self, previous_screen): screen = self.get_screen() visual = screen.get_rgba_visual() if visual: self.set_visual(visual) def do_draw(self, cr): cr.set_source_rgba(1, 0, 0, 0.2) cr.set_operator(cairo.OPERATOR_SOURCE) cr.paint() cr.set_source_rgba(1, 0, 0, 0.8) alloc = self.get_allocation() cr.rectangle(alloc.x, alloc.y, alloc.width, alloc.height) cr.stroke() return True class Listener(BaseListener): def __init__(self, apps=None): super().__init__(types=[ "object:text-caret-moved", ]) self.apps = [app.casefold() for app in apps] if apps else [] self.area = None self.area_clear_id = 0 def _update_area(self, extents): def clear(): if self.area: self.area.destroy() self.area = None self.area_clear_id = 0 return False if self.area: self.area.destroy() GLib.source_remove(self.area_clear_id) self.area = Highlight(*extents) self.area.show() self.area_clear_id = GLib.timeout_add(2000, clear) def _on_event(self, event): if super()._on_event(event): return True if event.type != "object:text-caret-moved": return False if self.apps and event.host_application.name.casefold() not in self.apps: return False text = event.source.queryText() extents = text.getCharacterExtents(event.detail1, pyatspi.DESKTOP_COORDS) print(event) print("\tx=%s y=%s w=%s h=%s" % extents) self._update_area(extents) return True if __name__ == '__main__': from sys import argv main(Listener(apps=(argv[1:] if len(argv) > 1 else None)))