1namespace Caribou {
2    // We can't use the name "Keyboard" here since caribou-gtk-module
3    // might register the name first.
4    [DBus (name = "org.gnome.Caribou.Keyboard")]
5    interface _Keyboard : Object {
6        public abstract void set_cursor_location (int x, int y, int w, int h)
7            throws IOError;
8        public abstract void set_entry_location (int x, int y, int w, int h)
9            throws IOError;
10        public abstract void show (uint32 timestamp) throws IOError;
11        public abstract void hide (uint32 timestamp) throws IOError;
12    }
13
14    [DBus (name = "org.gnome.Caribou.Daemon")]
15    class Daemon : Object {
16        _Keyboard keyboard;
17        Atspi.Accessible current_acc;
18        unowned Gdk.Display display;
19        uint name_id;
20
21        public Daemon () {
22            display = Gdk.Display.get_default ();
23            name_id = Bus.own_name (BusType.SESSION,
24                                    "org.gnome.Caribou.Daemon",
25                                    BusNameOwnerFlags.ALLOW_REPLACEMENT
26                                    | BusNameOwnerFlags.REPLACE,
27                                    on_bus_acquired, null, quit);
28        }
29
30        ~Daemon () {
31            Bus.unown_name (name_id);
32        }
33
34        void on_bus_acquired (DBusConnection conn) {
35            try {
36                conn.register_object ("/org/gnome/Caribou/Daemon", this);
37            } catch (IOError e) {
38                error ("Could not register D-Bus service: %s", e.message);
39            }
40        }
41
42        void on_get_proxy_ready (GLib.Object? obj, GLib.AsyncResult res) {
43            try {
44                keyboard = Bus.get_proxy.end (res);
45            } catch (Error e) {
46                error ("%s\n".printf (e.message));
47            }
48
49            try {
50                register_event_listeners ();
51            } catch (Error e) {
52                warning ("can't register event listeners: %s", e.message);
53            }
54        }
55
56        uint32 get_timestamp () {
57            if (display is Gdk.X11Display)
58                return Gdk.X11Display.get_user_time (display);
59            else
60                return 0;
61        }
62
63        void set_entry_location (Atspi.Accessible acc) throws Error {
64            var text = acc.get_text ();
65            var rect = text.get_character_extents (text.get_caret_offset (),
66                                                   Atspi.CoordType.SCREEN);
67            var component = acc.get_component ();
68            var entry_rect = component.get_extents (Atspi.CoordType.SCREEN);
69            if (rect.x == 0 && rect.y == 0 &&
70                rect.width == 0 && rect.height == 0) {
71                rect = entry_rect;
72            }
73
74            keyboard.set_cursor_location (rect.x, rect.y,
75                                          rect.width, rect.height);
76
77            keyboard.set_entry_location (entry_rect.x, entry_rect.y,
78                                         entry_rect.width, entry_rect.height);
79
80            keyboard.show (get_timestamp ());
81        }
82
83        void on_focus (owned Atspi.Event event) throws Error {
84            var acc = event.source;
85            var source_role = acc.get_role ();
86            if (acc.get_state_set ().contains (Atspi.StateType.EDITABLE) ||
87                source_role == Atspi.Role.TERMINAL) {
88                switch (source_role) {
89                case Atspi.Role.TEXT:
90                case Atspi.Role.PARAGRAPH:
91                case Atspi.Role.PASSWORD_TEXT:
92                case Atspi.Role.TERMINAL:
93                case Atspi.Role.ENTRY:
94                    if (event.type == "object:state-changed:focused") {
95                        if (event.detail1 == 1) {
96                            set_entry_location (acc);
97                            current_acc = event.source;
98                            debug ("enter text widget in %s",
99                                   event.source.get_application ().name);
100                        } else if (acc == current_acc) {
101                            keyboard.hide (get_timestamp ());
102                            current_acc = null;
103                            debug ("leave text widget in %s",
104                                   event.source.get_application ().name);
105                        }
106                    } else {
107                        warning ("unknown focus event: %s", event.type);
108                    }
109                    break;
110                default:
111                    break;
112                }
113            }
114        }
115
116        void on_focus_ignore_error (owned Atspi.Event event) {
117            try {
118                on_focus (event);
119            } catch (Error e) {
120                warning ("error in focus handler: %s", e.message);
121            }
122        }
123
124        void on_text_caret_moved (owned Atspi.Event event) throws Error {
125            if (current_acc == event.source) {
126                var text = current_acc.get_text ();
127                var rect = text.get_character_extents (text.get_caret_offset (),
128                                                       Atspi.CoordType.SCREEN);
129                if (rect.x == 0 && rect.y == 0 &&
130                    rect.width == 0 && rect.height == 0) {
131                    var component = current_acc.get_component ();
132                    rect = component.get_extents (Atspi.CoordType.SCREEN);
133                }
134
135                keyboard.set_cursor_location (rect.x, rect.y,
136                                              rect.width, rect.height);
137                debug ("object:text-caret-moved in %s: %d %s",
138                       event.source.get_application ().name,
139                       event.detail1, event.source.description);
140            }
141        }
142
143        void on_text_caret_moved_ignore_error (owned Atspi.Event event) {
144            try {
145                on_text_caret_moved (event);
146            } catch (Error e) {
147                warning ("error in text caret movement handler: %s", e.message);
148            }
149        }
150
151        void register_event_listeners () throws Error {
152            Atspi.EventListener.register_from_callback (
153                on_focus_ignore_error, "object:state-changed:focused");
154            Atspi.EventListener.register_from_callback (
155                on_text_caret_moved_ignore_error, "object:text-caret-moved");
156        }
157
158        void deregister_event_listeners () throws Error {
159            Atspi.EventListener.deregister_from_callback (
160                on_focus_ignore_error, "object:state-changed:focused");
161            Atspi.EventListener.deregister_from_callback (
162                on_text_caret_moved_ignore_error, "object:text-caret-moved");
163        }
164
165        [DBus (name = "Run")]
166        public void ping () {
167            // This method is called over D-Bus, upon activation.
168        }
169
170        [DBus (visible = false)]
171        public void run () {
172            Bus.get_proxy.begin<_Keyboard> (BusType.SESSION,
173                                            "org.gnome.Caribou.Keyboard",
174                                            "/org/gnome/Caribou/Keyboard",
175                                            0,
176                                            null,
177                                            on_get_proxy_ready);
178            // Use Atspi.Event.{main,quit}, instead of GLib.MainLoop
179            // to enable property caching in libatspi.
180            Atspi.Event.main ();
181        }
182
183        public void quit () {
184            if (keyboard != null) {
185                try {
186                    keyboard.hide (get_timestamp ());
187                } catch (IOError e) {
188                    warning ("can't hide keyboard: %s", e.message);
189                }
190
191                try {
192                    deregister_event_listeners ();
193                } catch (Error e) {
194                    warning ("can't deregister event listeners: %s", e.message);
195                }
196                keyboard = null;
197            }
198
199            Atspi.Event.quit ();
200        }
201    }
202}
203
204static const OptionEntry[] options = {
205    { null }
206};
207
208static int main (string[] args) {
209    Gdk.init (ref args);
210
211    Intl.setlocale (LocaleCategory.ALL, "");
212    Intl.bindtextdomain (Config.GETTEXT_PACKAGE, Config.LOCALEDIR);
213    Intl.bind_textdomain_codeset (Config.GETTEXT_PACKAGE, "UTF-8");
214    Intl.textdomain (Config.GETTEXT_PACKAGE);
215
216    var option_context = new OptionContext (_(
217        "- accessibility event monitoring daemon for screen keyboard"));
218    option_context.add_main_entries (options, "caribou");
219    try {
220        option_context.parse (ref args);
221    } catch (OptionError e) {
222        stderr.printf ("%s\n", e.message);
223        return 1;
224    }
225
226    var retval = Atspi.init ();
227    if (retval != 0) {
228        printerr ("can't initialize atspi\n");
229        return 1;
230    }
231
232    var daemon = new Caribou.Daemon ();
233    Unix.signal_add (Posix.SIGINT, () => {
234            daemon.quit ();
235            return false;
236        });
237    daemon.run ();
238
239    return 0;
240}
241