Revved to 1.0.0; at-spi 1.0 is now API frozen.
[platform/core/uifw/at-spi2-atk.git] / test / screen-review-test.c
1 /*
2  * AT-SPI - Assistive Technology Service Provider Interface
3  * (Gnome Accessibility Project; http://developer.gnome.org/projects/gap)
4  *
5  * Copyright 2001 Sun Microsystems Inc.
6  *
7  * This library is free software; you can redistribute it and/or
8  * modify it under the terms of the GNU Library General Public
9  * License as published by the Free Software Foundation; either
10  * version 2 of the License, or (at your option) any later version.
11  *
12  * This library is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15  * Library General Public License for more details.
16  *
17  * You should have received a copy of the GNU Library General Public
18  * License along with this library; if not, write to the
19  * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
20  * Boston, MA 02111-1307, USA.
21  */
22
23 #include <stdlib.h>
24 #include "../cspi/spi-private.h"
25
26 #undef CLIP_DEBUG
27
28 /*
29  * Screen Review Algorithm Demonstration, Benchmark, and Test-bed.
30  *
31  * Issues:
32  *
33  * * there are bugs in the compositing code.
34  *
35  * * brute-force algorithm uses no client-side cache; performance mediocre.
36  *
37  * * we don't have good ordering information for the toplevel windows,
38  *          and our current heuristic is not guaranteed if
39  *          the active window is not always on top (i.e. autoraise is
40  *          not enabled).
41  *
42  * * can't know about "inaccessible" objects that may be obscuring the
43  *          accessible windows (inherent to API-based approach).
44  *
45  * * text column alignment is crude since it relies on a hard-coded
46  *          (and arbitrary) ratio of horizontal pixels to character columns.
47  *          For small proportional fonts, overruns are likely to result in
48  *          occasional review lines that exceed the standard length.
49  *
50  * * (others).
51  */
52
53 #undef CHUNK_LIST_DEBUG /* define to get list of text chunks before clip */
54
55 #define BOUNDS_CONTAIN_X_BOUNDS(b, p)  ( ((p).x>=(b).x) &&\
56                                          (((p).x + (p).width) <= \
57                                          ((b).x + (b).width)) && \
58                                         ((b).width > 0) && ((b).height > 0))
59
60 #define BOUNDS_CONTAIN_Y(b, p)  (((p)>=(b)->y) && ((p)<=((b)->y + (b)->height))\
61             && ((b)->width > 0) && ((b)->height > 0))
62
63 #define CHUNK_BOUNDS_BEFORE_START(c, i) ((i) && ((c)->clip_bounds.x < \
64                                          (i)->clip_bounds.x)) 
65
66 #define CHUNK_BOUNDS_END_BEFORE_START(c, i) ((i) && \
67                                          (((c)->clip_bounds.x + \
68                                          (c)->clip_bounds.width) < \
69                                          (i)->clip_bounds.x))
70
71 #define CHUNK_BOUNDS_AFTER_END(c, i) ((i) && ((c)->clip_bounds.x >= \
72                                        ((i)->clip_bounds.x + \
73                                         (i)->clip_bounds.width)))
74
75 #define CHUNK_BOUNDS_SPANS_END(c, i) ((i) && (((c)->clip_bounds.x + \
76                                               (c)->clip_bounds.width) >= \
77                                        ((i)->clip_bounds.x + \
78                                         (i)->clip_bounds.width)))
79 /*
80  * #define CHUNK_BOUNDS_WITHIN(c, i)  ((i) && ((c)->clip_bounds.x >= \
81  *                               (i)->text_bounds.x) && \
82  *                               (((c)->clip_bounds.x + (c)->clip_bounds.width) \
83  *                             <= ((i)->text_bounds.x + (i)->text_bounds.width)))
84  */
85
86 #define IS_CLIPPING_CONTAINER(a) ((a) != SPI_ROLE_PAGE_TAB)
87
88 static void report_screen_review_line  (const AccessibleEvent *event, void *user_data);
89
90 static gint n_elements_traversed = 0;
91 static AccessibleEventListener *mouseclick_listener;
92
93 typedef struct _BoundaryRect {
94         long int x;
95         long int y;
96         long int width;
97         long int height;
98         AccessibleRole role; /* role of last clipping element */
99         gboolean isClipped;
100         gboolean isEmpty;
101 } BoundaryRect;
102
103 typedef struct _TextChunk {
104         char           *string;
105         Accessible     *source;
106         int             start_offset;
107         int             end_offset;
108         BoundaryRect    clip_bounds;
109         BoundaryRect    text_bounds;
110         BoundaryRect    start_char_bounds;
111         BoundaryRect    end_char_bounds;
112 } TextChunk;
113
114 typedef struct _ScreenReviewBuffer { 
115         GList *text_chunks;
116 } ScreenReviewBuffer;
117
118 static gboolean isJava = FALSE;
119
120 int
121 main (int argc, char **argv)
122 {
123   int i, j;
124   int n_desktops;
125   int n_apps;
126   char *s;
127   GTimer *timer;
128   gdouble elapsed_time;
129   Accessible *desktop;
130   Accessible *application;
131   const char *modules;
132
133   SPI_init ();
134
135   mouseclick_listener = SPI_createAccessibleEventListener (
136           report_screen_review_line, NULL); 
137
138   SPI_registerGlobalEventListener (mouseclick_listener,
139                                    "Gtk:GtkWidget:button-press-event");
140 #undef JAVA_TEST_HACK
141 #ifdef JAVA_TEST_HACK /* Only use this to test Java apps */
142   SPI_registerGlobalEventListener (mouseclick_listener,
143                                    "object:text-caret-moved");
144   isJava = TRUE;
145 #endif
146   SPI_event_main ();
147
148   putenv ("AT_BRIDGE_SHUTDOWN=1");
149
150   /*
151    * TODO: Add a key event listener that calls test_exit, to
152    * deregister and cleanup appropriately.
153    */
154
155   return SPI_exit ();
156 }
157
158 static inline gboolean
159 chunk_bounds_within (TextChunk *chunk, TextChunk *test_chunk)
160 {
161         int x1, x2, tx1, tx2;
162         gboolean gtx1, ltx2;
163
164         x1 = chunk->clip_bounds.x;
165         x2 = x1 + chunk->clip_bounds.width;
166         tx1 = test_chunk->clip_bounds.x;
167         tx2 = tx1 + test_chunk->clip_bounds.width;
168         gtx1 = (chunk->clip_bounds.x >= test_chunk->clip_bounds.x);
169         ltx2 = (chunk->clip_bounds.x + chunk->clip_bounds.width
170                 <= test_chunk->clip_bounds.x + test_chunk->clip_bounds.width);
171         return gtx1 && ltx2;
172 }
173
174 #define CHUNK_BOUNDS_WITHIN(a, b) chunk_bounds_within(a, b)
175
176 static BoundaryRect **
177 clip_bounds_clone (BoundaryRect *bounds[])
178 {
179         int i;
180         BoundaryRect **bounds_clone;
181         bounds_clone = (BoundaryRect **)
182                 g_new0 (gpointer, SPI_LAYER_LAST_DEFINED);
183         for (i = 1; i < SPI_LAYER_LAST_DEFINED; ++i) {
184                 bounds_clone[i] = g_new0 (BoundaryRect, 1);
185                 *bounds_clone[i] = *bounds[i];
186         }
187         return bounds_clone;
188 }
189
190 static void
191 boundary_clip (BoundaryRect *bounds, BoundaryRect *clipBounds)
192 {
193         int x2 = bounds->x + bounds->width;
194         int y2 = bounds->y + bounds->height;
195 #ifdef CLIP_DEBUG
196         fprintf (stderr, "bclip %d-%d, %d-%d; ",
197                  bounds->x, x2,
198                  clipBounds->x, clipBounds->x+clipBounds->width);
199 #endif           
200         bounds->x = MAX (bounds->x, clipBounds->x);
201         bounds->y = MAX (bounds->y, clipBounds->y);
202         x2 =  MIN (x2,  clipBounds->x + clipBounds->width);
203         y2 =  MIN (y2, clipBounds->y + clipBounds->height);
204         bounds->width = MAX (x2 - bounds->x, 0);
205         bounds->height = MAX (y2 - bounds->y, 0);
206         if (!bounds->width || !bounds->height)
207                 bounds->isEmpty = TRUE;
208         if (IS_CLIPPING_CONTAINER (bounds->role)) {
209                 *clipBounds = *bounds;
210         }
211 #ifdef CLIP_DEBUG
212         fprintf (stderr, "%d-%d\n",
213                  bounds->x, bounds->x+bounds->width);
214 #endif
215 }
216
217 static void
218 boundary_xclip_head (BoundaryRect *bounds, BoundaryRect *clipBounds)
219 {
220         int x2;
221         int cx2 = clipBounds->x + clipBounds->width;
222         if (cx2 < bounds->x) return;
223         x2 = bounds->x + bounds->width;
224         if (cx2 <= x2) bounds->x = cx2;
225         bounds->width = MAX (0, x2 - cx2);
226 }
227
228 static void
229 boundary_xclip_tail (BoundaryRect *bounds, BoundaryRect *clipBounds)
230 {
231         int x2 = bounds->x + bounds->width;
232         if (clipBounds->x > x2) return;
233         bounds->width = MAX (0, clipBounds->x - bounds->x);
234 }
235
236 static TextChunk*
237 text_chunk_copy (TextChunk *chunk)
238 {
239         TextChunk *copy = g_new0 (TextChunk, 1);
240         *copy = *chunk;
241         if (chunk->string) copy->string = g_strdup (chunk->string);
242         if (copy->source) Accessible_ref (copy->source);
243         return copy;
244 }
245
246 static void
247 text_chunk_tail_clip (TextChunk *bottom, TextChunk *top)
248 {
249 #ifdef CLIP_DEBUG
250         fprintf (stderr, "bottom %d-%d, top %d-%d;",
251                  bottom->clip_bounds.x,
252                  bottom->clip_bounds.x + bottom->clip_bounds.width,
253                  top->clip_bounds.x,
254                  top->clip_bounds.x + top->clip_bounds.width);
255 #endif
256         boundary_xclip_tail (&bottom->text_bounds, &top->clip_bounds);
257         boundary_xclip_tail (&bottom->clip_bounds, &top->clip_bounds);
258         bottom->text_bounds.isClipped = TRUE;
259         bottom->clip_bounds.isClipped = TRUE;
260 #ifdef CLIP_DEBUG
261         fprintf (stderr, "result %d-%d\n",
262                  bottom->clip_bounds.x,
263                  bottom->clip_bounds.x + bottom->clip_bounds.width);
264 #endif
265 }
266
267 static void
268 text_chunk_head_clip (TextChunk *bottom, TextChunk *top)
269 {
270 #ifdef CLIP_DEBUG
271         fprintf (stderr, "bottom %d-%d, top %d-%d;",
272                  bottom->clip_bounds.x,
273                  bottom->clip_bounds.x + bottom->clip_bounds.width,
274                  top->clip_bounds.x,
275                  top->clip_bounds.x + top->clip_bounds.width);
276 #endif  
277         boundary_xclip_head (&bottom->text_bounds, &top->clip_bounds);
278         boundary_xclip_head (&bottom->clip_bounds, &top->clip_bounds);
279         bottom->text_bounds.isClipped = TRUE;
280         bottom->clip_bounds.isClipped = TRUE;
281 #ifdef CLIP_DEBUG       
282         fprintf (stderr, "result %d-%d\n",
283                  bottom->clip_bounds.x,
284                  bottom->clip_bounds.x + bottom->clip_bounds.width);
285 #endif  
286 }
287
288 static GList *
289 text_chunk_split_insert (GList *chunk_list, GList *iter, TextChunk *chunk)
290 {
291         TextChunk *iter_chunk = iter->data;
292         TextChunk *iter_copy = text_chunk_copy (iter_chunk);
293         /* TODO: FIXME something is wrong here */
294 #ifdef CLIP_DEBUG
295         fprintf (stderr, "***clip insert of %s into %s\n", 
296                  chunk->string ? chunk->string : "<null>",
297                  iter_chunk->string ? iter_chunk->string : "<null>");
298 #endif  
299         chunk_list = g_list_insert_before (chunk_list, iter, iter_copy);
300         text_chunk_tail_clip (iter_copy, chunk);
301         chunk_list = g_list_insert_before (chunk_list, iter, chunk);
302         text_chunk_head_clip (iter_chunk, chunk);
303         return chunk_list;
304 }
305
306 /* #define PRINT_CHUNK_DEBUG(a, b, c, d) print_chunk_debug(a, b, c, d) */
307
308 #define PRINT_CHUNK_DEBUG(a, b, c, d)
309
310 #ifdef PRINT_CHUNK_DEBUG
311 static void
312 print_chunk_debug (TextChunk *chunk, char *opname, GList *prev, GList *next)
313 {
314         fprintf (stderr, "%sing chunk %s between %s and %s; %d-%d\n",
315                  opname,
316                  chunk->string,
317                  (prev ? ((TextChunk *) prev->data)->string : "<null>"),
318                  (next ? ((TextChunk *) next->data)->string : "<null>"),
319                  chunk->clip_bounds.x,
320                  chunk->text_bounds.x + chunk->text_bounds.width);
321 }
322 #endif
323
324 static GList *
325 text_chunk_list_head_clip (GList *text_chunk_list,
326                            TextChunk *chunk,
327                            GList *next)
328 {
329         GList *target, *iter = next, *prev;
330         prev = iter->prev;
331 //      if (chunk->string && strlen (chunk->string)) { 
332                 text_chunk_list =
333                         g_list_insert_before (text_chunk_list, next, chunk);
334 //      }
335         while (iter && CHUNK_BOUNDS_SPANS_END (chunk, (TextChunk *)iter->data)) {
336 #ifdef CLIP_DEBUG                       
337                         fprintf (stderr, "deleting %s\n",
338                                  ((TextChunk *)iter->data)->string);
339 #endif                  
340                         target = iter;
341                         iter = iter->next;
342                         text_chunk_list =
343                                 g_list_delete_link (text_chunk_list, target);
344         }
345         if (iter && !CHUNK_BOUNDS_END_BEFORE_START (chunk,
346                                                     (TextChunk *)iter->data)) {
347                 text_chunk_head_clip ((TextChunk *)iter->data,
348                                       chunk);
349         }
350         if (prev &&
351             !CHUNK_BOUNDS_AFTER_END (
352                     chunk,
353                     (TextChunk *)prev->data)) {
354                 text_chunk_tail_clip (
355                         (TextChunk *)prev->data,
356                         chunk);
357         }
358         
359         return text_chunk_list;
360 }
361
362 static GList *
363 text_chunk_list_clip_and_insert (GList *text_chunk_list,
364                                  TextChunk *chunk,
365                                  GList *prev,
366                                  GList *next)
367 {
368 #ifdef CLIP_DEBUG
369 /*      if (chunk->string) */
370                 fprintf (stderr, "clip-and-insert for %s, between %s (%d) and %s (%d)\n",
371                          chunk->string,
372                          (prev && ((TextChunk *)prev->data)->string ? ((TextChunk *)prev->data)->string : "<null>"),
373                          (prev ? ((TextChunk *)prev->data)->text_bounds.x : -1),
374                          (next && ((TextChunk *)next->data)->string ? ((TextChunk *)next->data)->string : "<null>"),
375                          (next ? ((TextChunk *)next->data)->text_bounds.x : -1));
376 #endif
377         /* cases: */
378         if (!prev && !next) { /* first element in, no clip needed */
379                 text_chunk_list =
380                         g_list_append (text_chunk_list, chunk);
381                 PRINT_CHUNK_DEBUG (chunk, "append",
382                                    prev,
383                                    NULL);
384         } else { /* check for clip with prev */
385                 /* if we split the prev */
386                 if (prev &&
387                     CHUNK_BOUNDS_WITHIN (chunk, (TextChunk *) prev->data)) {
388                         text_chunk_list =
389                                 text_chunk_split_insert (
390                                         text_chunk_list,
391                                         prev, chunk);
392                 } else if (next) { 
393                     /* we split the 'next' element */
394                     if (CHUNK_BOUNDS_WITHIN (chunk, (TextChunk *)next->data)) {
395                             text_chunk_list =
396                                     text_chunk_split_insert (text_chunk_list,
397                                                              next, chunk);
398                     } else {
399                             /* do an insert +  head clip */
400                             text_chunk_list =
401                                     text_chunk_list_head_clip (
402                                             text_chunk_list,
403                                             chunk,
404                                             next);
405                     }
406                 } else {
407                         if (!CHUNK_BOUNDS_AFTER_END (chunk,
408                                                      (TextChunk *) prev->data)) {
409                                 text_chunk_tail_clip (prev->data, chunk);
410                         }
411                         text_chunk_list =
412                                 g_list_append (text_chunk_list, chunk);
413                 }
414         }
415         return text_chunk_list;
416 }
417
418 static GList *
419 text_chunk_list_insert_chunk (GList *text_chunk_list, TextChunk *chunk)
420 {
421         GList *iter = g_list_first (text_chunk_list);
422         TextChunk *iter_chunk = NULL;
423         do {
424                 if (iter) iter_chunk = (TextChunk *) iter->data;
425                 /* if we're ahead of the current element */
426                 if (!iter) {
427                         text_chunk_list =
428                                 text_chunk_list_clip_and_insert (text_chunk_list,
429                                                                  chunk,
430                                                                  iter,
431                                                                  NULL);
432                         break;
433                 } else if (CHUNK_BOUNDS_BEFORE_START (chunk, iter_chunk)) {
434                         text_chunk_list =
435                                 text_chunk_list_clip_and_insert (text_chunk_list,
436                                                                  chunk,
437                                                                  iter->prev,
438                                                                  iter);
439                         break;
440                 } else if (!iter->next ) {
441                         text_chunk_list =
442                                 text_chunk_list_clip_and_insert (text_chunk_list,
443                                                                  chunk,
444                                                                  iter,
445                                                                  NULL);
446                         break;
447                 }
448                 if (iter) iter = iter->next;
449         } while (iter);
450         return text_chunk_list;
451 }
452
453 static TextChunk *
454 review_buffer_get_text_chunk (ScreenReviewBuffer *reviewBuffer,
455                               Accessible *accessible, BoundaryRect *bounds,
456                               int screen_x, int screen_y)
457 {
458         AccessibleText *text = NULL;
459         AccessibleRole role;
460         TextChunk *text_chunk;
461         BoundaryRect start_char_bounds, end_char_bounds;
462         char *s = NULL;
463         int offset;
464         long start, end;
465         long x2, y2;
466
467         role = Accessible_getRole (accessible);
468         text_chunk = g_new0 (TextChunk, 1);
469         text_chunk->clip_bounds = *bounds;
470         text_chunk->source = accessible;
471         Accessible_ref (accessible);
472         if (Accessible_isText (accessible)) {
473                 text = Accessible_getText (accessible);
474                 offset = AccessibleText_getOffsetAtPoint (text,
475                                                           screen_x,
476                                                           screen_y,
477                                                           SPI_COORD_TYPE_SCREEN);
478                 s = AccessibleText_getTextAtOffset (text, offset,
479                                                     SPI_TEXT_BOUNDARY_LINE_START,
480                                                     &start, &end);
481                 if (end > start) {
482                         AccessibleText_getCharacterExtents (
483                                 text, start,
484                                 &text_chunk->start_char_bounds.x,
485                                 &text_chunk->start_char_bounds.y,
486                                 &text_chunk->start_char_bounds.width,
487                                 &text_chunk->start_char_bounds.height,
488                                 SPI_COORD_TYPE_SCREEN);
489 #ifdef CLIP_DEBUG
490                         fprintf (stderr, "%s: start char (%d) x, width %d %d; ",
491                                  s,
492                                  start,
493                                  text_chunk->start_char_bounds.x,
494                                  text_chunk->start_char_bounds.width);
495 #endif
496                         if (s && strlen (s) && s[strlen (s) - 1] == '\n')
497                                 end--;
498                         AccessibleText_getCharacterExtents (
499                                 text, end - 1,
500                                 &text_chunk->end_char_bounds.x,
501                                 &text_chunk->end_char_bounds.y,
502                                 &text_chunk->end_char_bounds.width,
503                                 &text_chunk->end_char_bounds.height,
504                                 SPI_COORD_TYPE_SCREEN);
505 #ifdef CLIP_DEBUG                       
506                         fprintf (stderr, "end char (%d) x, width %d %d\n",
507                                  end - 1,
508                                  text_chunk->end_char_bounds.x,
509                                  text_chunk->end_char_bounds.width);
510 #endif
511                 }
512                 text_chunk->text_bounds.x = MIN (text_chunk->start_char_bounds.x,
513                                                  text_chunk->end_char_bounds.x);
514                 text_chunk->text_bounds.y = MIN (text_chunk->start_char_bounds.y,
515                                                  text_chunk->end_char_bounds.y);
516                 x2 = MAX (text_chunk->start_char_bounds.x +
517                           text_chunk->start_char_bounds.width,
518                           text_chunk->end_char_bounds.x +
519                           text_chunk->end_char_bounds.width);
520                 text_chunk->text_bounds.width = x2 - text_chunk->text_bounds.x;
521                 y2 = MAX (text_chunk->start_char_bounds.y +
522                           text_chunk->start_char_bounds.height,
523                           text_chunk->end_char_bounds.y + 
524                           text_chunk->end_char_bounds.height);
525                 text_chunk->text_bounds.height = y2 - text_chunk->text_bounds.y;
526                 text_chunk->start_offset = start;
527                 text_chunk->end_offset = end;
528                 AccessibleText_unref (text);
529         } else {
530                 if (role == SPI_ROLE_PUSH_BUTTON ||
531                     role == SPI_ROLE_CHECK_BOX ||
532                     role == SPI_ROLE_LABEL ||
533                     role == SPI_ROLE_MENU ||
534                     role == SPI_ROLE_MENU_ITEM) { /* don't like this
535                                                      special casing :-( */
536                         s = Accessible_getName (accessible);
537                         /* use name instead */
538                         text_chunk->text_bounds = text_chunk->clip_bounds;
539                         text_chunk->start_offset = 0;
540                         text_chunk->end_offset = strlen (s);
541                 }
542         }
543         if (text_chunk->text_bounds.x < text_chunk->clip_bounds.x) {
544                 text_chunk->text_bounds.x = text_chunk->clip_bounds.x;
545                 text_chunk->text_bounds.isClipped = TRUE;
546         } 
547         if ((text_chunk->text_bounds.x +
548              text_chunk->text_bounds.width)
549             > (text_chunk->clip_bounds.x +
550                text_chunk->clip_bounds.width)) {
551                 text_chunk->text_bounds.width =
552                         MAX (0, (text_chunk->clip_bounds.x +
553                                  text_chunk->clip_bounds.width) -
554                              text_chunk->text_bounds.x);
555                 text_chunk->text_bounds.isClipped = TRUE;
556         }
557         if (!BOUNDS_CONTAIN_Y (&text_chunk->text_bounds,
558                                screen_y)) {
559 #ifdef CLIP_DEBUG                       
560                 fprintf (stderr, "%s out of bounds (%d-%d)\n", s,
561                          text_chunk->text_bounds.y,
562                          text_chunk->text_bounds.y +
563                          text_chunk->text_bounds.height);
564 #endif                  
565                 s = NULL;
566                 text_chunk->start_offset = offset;
567                 text_chunk->end_offset = offset;
568         }
569         if (s && strlen (s)) {
570                 if (s[strlen(s)-1] == '\n') s[strlen(s)-1] = ' ';
571                 /* XXX: if last char is newline, aren't its bounds wrong now? */
572                 text_chunk->string = s;
573 #ifdef CLIP_DEBUG
574                 fprintf (stderr, "%s, bounds %d-%d; clip %d-%d\n",
575                          s,
576                          text_chunk->text_bounds.x,
577                          text_chunk->text_bounds.x+text_chunk->text_bounds.width,
578                          text_chunk->clip_bounds.x,
579                          text_chunk->clip_bounds.x+text_chunk->clip_bounds.width);
580 #endif          
581         } else {
582                 text_chunk->string = NULL;
583         }
584         return text_chunk;
585 }
586
587 static void
588 debug_chunk_list (GList *iter)
589 {
590         TextChunk *chunk;
591         while (iter) {
592                 chunk = (TextChunk *)iter->data;
593                 fprintf (stderr, "Chunk %s, clip %d-%d, text %d-%d\n",
594                          chunk->string,
595                          chunk->clip_bounds.x,
596                          chunk->clip_bounds.x + chunk->clip_bounds.width,
597                          chunk->text_bounds.x,
598                          chunk->text_bounds.x + chunk->text_bounds.width);
599                 iter = iter->next;
600         }
601 }
602
603 static void
604 clip_into_buffers (Accessible *accessible,  BoundaryRect* parentClipBounds[],
605                    ScreenReviewBuffer *reviewBuffers[],
606                    int screen_x, int screen_y)
607 {
608         int n_children, child_n;
609         Accessible *child;
610         BoundaryRect bounds;
611         BoundaryRect** clip_bounds;
612         TextChunk *text_chunk;
613         AccessibleComponent *component;
614         AccessibleRole role;
615         int layer;
616
617         clip_bounds = clip_bounds_clone (parentClipBounds);
618         if (Accessible_isComponent (accessible)) {
619                 role = Accessible_getRole (accessible);
620                 component = Accessible_getComponent (accessible);
621                 layer = AccessibleComponent_getLayer (component);
622                 bounds = *clip_bounds[layer];
623                 if (!bounds.isEmpty) {
624                         AccessibleComponent_getExtents (component,
625                                                         &bounds.x,
626                                                         &bounds.y,
627                                                         &bounds.width,
628                                                         &bounds.height,
629                                                         SPI_COORD_TYPE_SCREEN);
630                         bounds.role = role;
631                         if (clip_bounds[layer])
632                                 boundary_clip (&bounds, clip_bounds[layer]);
633                         if (BOUNDS_CONTAIN_Y (&bounds, screen_y)) {
634                                 text_chunk = review_buffer_get_text_chunk (
635                                         reviewBuffers[layer], accessible, &bounds,
636                                         screen_x, screen_y);
637                                 reviewBuffers[layer]->text_chunks = 
638                                         text_chunk_list_insert_chunk (
639                                                 reviewBuffers[layer]->text_chunks,
640                                                 text_chunk);
641                         } else {
642                                 bounds.isEmpty =
643                                         IS_CLIPPING_CONTAINER (bounds.role);
644                         }
645                 } 
646                 Accessible_unref (component);
647         }
648         /*
649          * we always descend into children in case they are in a higher layer
650          * this can of course be optimized for the topmost layer...
651          * but nobody uses that one! (SPI_LAYER_OVERLAY)
652          */
653         n_children = Accessible_getChildCount (accessible);
654         for (child_n = 0; child_n < n_children; ++child_n) {
655                 child = Accessible_getChildAtIndex (accessible, child_n);
656                 clip_into_buffers (child, clip_bounds, reviewBuffers, screen_x, screen_y);
657                 Accessible_unref (child);
658         }
659         /* TODO: free the parent clip bounds */
660 }
661
662 #undef CHARACTER_CLIP_DEBUG
663
664 static char*
665 text_chunk_get_clipped_substring_by_char (TextChunk *chunk, int start, int end)
666 {
667         BoundaryRect char_bounds;
668         int i;
669         char *s;
670         GString *string = g_string_new ("");
671         gunichar c;
672         AccessibleText *text = Accessible_getText (chunk->source);
673         for (i = start; i < end; ++i) {
674                 AccessibleText_getCharacterExtents (text,
675                                                     i,
676                                                     &char_bounds.x,
677                                                     &char_bounds.y,
678                                                     &char_bounds.width,
679                                                     &char_bounds.height,
680                                                     SPI_COORD_TYPE_SCREEN);
681 #ifdef CHARACTER_CLIP_DEBUG
682                 fprintf (stderr, "testing %d-%d against %d-%d\n",
683                          char_bounds.x, char_bounds.x+char_bounds.width,
684                          chunk->text_bounds.x,
685                          chunk->text_bounds.x + chunk->text_bounds.width);
686 #endif
687                 if (BOUNDS_CONTAIN_X_BOUNDS (chunk->text_bounds,
688                                              char_bounds)) {
689                         c = AccessibleText_getCharacterAtOffset (
690                                 text, i);
691 #ifdef CLIP_DEBUG                               
692                         fprintf (stderr, "[%c]", c);
693 #endif                          
694                         g_string_append_unichar (string, c);
695                 }
696         }
697         AccessibleText_unref (text);
698         s = string->str;
699         g_string_free (string, FALSE);
700         return s;
701 }
702
703
704 /*
705  * Note: this routine shouldn't have to do as much as it currently does,
706  *       but at the moment it works around another bug (probably one in this
707  *       code).
708  */
709 static char *
710 string_strip_newlines (char *s, long offset, long *start_offset, long *end_offset)
711 {
712         int i;
713         char *word_start = s;
714         /* FIXME: potential memory leak here */
715         for (i=0; s && s[i]; ++i)
716         {
717                 if (s [i] == '\n' && i > (offset - *start_offset) ) {
718                         s [i] = '\0';
719                         *end_offset = *start_offset + i;
720                         return word_start;
721                 } else if (s [i] == '\n') {
722                         word_start = &s[i + 1];
723                 }
724         }
725         return word_start;
726 }
727
728 static char *
729 string_guess_clip (TextChunk *chunk)
730 {
731         BoundaryRect b;
732         char *s = NULL, *sp = chunk->string, *ep;
733         long start_offset, end_offset, len;
734         if (sp) {
735                 AccessibleComponent *component =
736                         Accessible_getComponent (chunk->source);
737                 ep = sp + (strlen (sp));
738                 len = g_utf8_strlen (chunk->string, -1);
739                 if (component) {
740                         AccessibleComponent_getExtents (component,
741                                                         &b.x, &b.y,
742                                                         &b.width, &b.height,
743                                                         SPI_COORD_TYPE_SCREEN);
744                         start_offset = len * (chunk->text_bounds.x - b.x) / b.width;
745                         end_offset = len * (chunk->text_bounds.x +
746                                             chunk->text_bounds.width - b.x) / b.width;
747                         fprintf (stderr, "String len %d, clipped to %d-%d\n",
748                                  len, start_offset, end_offset);
749                         len = end_offset - start_offset;
750                         sp = g_utf8_offset_to_pointer (chunk->string, start_offset);
751                         ep = g_utf8_offset_to_pointer (chunk->string, end_offset);
752                 }
753                 s = g_new0 (char, ep - sp + 1);
754                 s = g_utf8_strncpy (s, sp, len);
755                 s [sp - ep] = '\0';
756                 g_assert (g_utf8_validate (s, -1, NULL));
757         }
758         return s;
759 }
760
761 static char*
762 text_chunk_get_clipped_string (TextChunk *chunk)
763 {
764         char *s, *string = "";
765         int i;
766         long start = chunk->start_offset, end = chunk->end_offset;
767         long word_start, word_end, range_end;
768         BoundaryRect start_bounds, end_bounds;
769         gboolean start_inside, end_inside;
770         if (!chunk->text_bounds.isClipped || !chunk->string)
771                 string = chunk->string;
772         else if (chunk->source && Accessible_isText (chunk->source)) {
773                 /* while words at offset lie within the bounds, add them */
774                 AccessibleText *text = Accessible_getText (chunk->source);
775 #ifdef CLIP_DEBUG               
776                 fprintf (stderr, "clipping %s\n", chunk->string);
777 #endif
778                 do {
779                     s = AccessibleText_getTextAtOffset (text,
780                                                         start,
781                                                 SPI_TEXT_BOUNDARY_WORD_END,
782                                                         &word_start,
783                                                         &word_end);
784                     range_end = word_end;
785                     s = string_strip_newlines (s, start, &word_start, &word_end);
786                     AccessibleText_getCharacterExtents (text,
787                                                         word_start,
788                                                         &start_bounds.x,
789                                                         &start_bounds.y,
790                                                         &start_bounds.width,
791                                                         &start_bounds.height,
792                                                         SPI_COORD_TYPE_SCREEN);
793                     AccessibleText_getCharacterExtents (text,
794                                                         word_end - 1,
795                                                         &end_bounds.x,
796                                                         &end_bounds.y,
797                                                         &end_bounds.width,
798                                                         &end_bounds.height,
799                                                         SPI_COORD_TYPE_SCREEN);
800                     start_inside = BOUNDS_CONTAIN_X_BOUNDS (chunk->text_bounds,
801                                                             start_bounds);
802                     end_inside = BOUNDS_CONTAIN_X_BOUNDS (chunk->text_bounds,
803                                                           end_bounds);
804                     if (start_inside && end_inside) {
805                             /* word is contained in bounds */
806                             string = g_strconcat (string, s, NULL);
807                     } else if (start_inside || end_inside) {
808                             /* one end of word is in */
809                             if (word_end > end) word_end = end;
810                             s = text_chunk_get_clipped_substring_by_char (
811                                     chunk,
812                                     MAX (word_start, chunk->start_offset),
813                                     MIN (word_end, chunk->end_offset));
814                             string = g_strconcat (string, s, NULL);
815                     } else {
816                     }
817                     start = range_end;
818                 } while (start < chunk->end_offset);
819         } else { /* we're clipped, but don't implement AccessibleText :-( */
820                 /* guess for now, maybe we can do better someday */
821                 string = string_guess_clip (chunk);
822         }
823         return string;
824 }
825
826
827 static char*
828 text_chunk_pad_string (TextChunk *chunk, char *string, glong offset, const char *pad_chars)
829 {
830         char *s = "";
831         char *cp;
832         char startbuf[6], padbuf[6], endbuf[6];
833         int pixels_per_column = 6;
834         /* this is an arbitrary pixel-to-textcolumn mapping at present */
835         glong end_padding;
836         gint howmany;
837         howmany = g_unichar_to_utf8 (g_utf8_get_char (pad_chars), startbuf);
838         startbuf[howmany] = '\0';
839         g_assert (howmany < 7 && howmany > 0);
840         cp = g_utf8_find_next_char (pad_chars, NULL);
841         howmany = g_unichar_to_utf8 (g_utf8_get_char (cp), padbuf);
842         padbuf[howmany] = '\0';
843         g_assert (howmany < 7 && howmany > 0);
844         cp = g_utf8_find_next_char (cp, NULL);
845         howmany = g_unichar_to_utf8 (g_utf8_get_char (cp), endbuf);
846         endbuf[howmany] = '\0';
847         g_assert (howmany < 7 && howmany > 0);
848         end_padding = chunk->clip_bounds.x / pixels_per_column; 
849         while (offset < end_padding - 1) {
850                 s = g_strconcat (s, padbuf, NULL); /* could be more efficient */
851                 ++offset;
852         }
853         s = g_strconcat (s, startbuf, string, NULL);
854         offset += g_utf8_strlen (string, -1) + 1;
855         end_padding = chunk->text_bounds.x / pixels_per_column; 
856         while (offset < end_padding) {
857                 s = g_strconcat (s, padbuf, NULL); /* could be more efficient */
858                 ++offset;
859         }
860         end_padding = (chunk->clip_bounds.x + chunk->clip_bounds.width) /
861                 pixels_per_column;
862         while (offset < end_padding - 1) {
863                 s = g_strconcat (s, padbuf, NULL); /* could be more efficient */
864                 ++offset;
865         }
866         s = g_strconcat (s, endbuf, NULL);
867         return s;
868 }
869
870 static char*
871 text_chunk_to_string (TextChunk *chunk, glong offset)
872 {
873         char *s = NULL;
874         if (chunk->string) {
875                 s = text_chunk_get_clipped_string (chunk);
876                 if (chunk->clip_bounds.role == SPI_ROLE_PUSH_BUTTON) {
877                         s = text_chunk_pad_string (chunk, s, offset, "[ ]");
878                 } else if (chunk->clip_bounds.role == SPI_ROLE_FRAME) {
879                         s = text_chunk_pad_string (chunk, s, offset, "| |");
880                 } else if (chunk->clip_bounds.role == SPI_ROLE_TEXT) {
881                         s = text_chunk_pad_string (chunk, s, offset, "\" \"");
882                 } else {
883                         s = text_chunk_pad_string (chunk, s, offset, "   ");
884                 }
885         }
886         return s;
887 }
888
889 static char*
890 text_chunk_list_to_string (GList *iter)
891 {
892         char *s = "";
893         char *string;
894         TextChunk *chunk = NULL;
895         int offset = 0;
896         while (iter) {
897                 chunk = (TextChunk *)iter->data;
898                 if (chunk) {
899                         string = text_chunk_to_string (chunk, g_utf8_strlen (s, -1));
900                         if (string)
901                                 s = g_strconcat (s, string, NULL);
902                 }
903                 iter = iter->next;
904         }
905         s = g_strconcat (s, "|", NULL);
906         return s;
907 }
908
909 #define COMPOSITE_DEBUG
910
911 static void
912 toplevel_composite (ScreenReviewBuffer *buffers[])
913 {
914         int i;
915         GList *chunk_list, *iter;
916         TextChunk *chunk;
917
918         chunk_list = buffers[SPI_LAYER_CANVAS]->text_chunks;
919         for (i = SPI_LAYER_MDI; i < SPI_LAYER_OVERLAY; ++i) {
920                 iter = buffers[i]->text_chunks;
921 #ifdef COMPOSITE_DEBUG
922                 fprintf (stderr, "layer %d has %d chunks\n",
923                          i, g_list_length (iter));
924 #endif          
925                 while (iter) {
926                         chunk = (TextChunk *) iter->data;
927                         if (chunk) {
928 #ifdef COMPOSITE_DEBUG
929                                 fprintf (stderr, "inserting chunk <%s>\n",
930                                          chunk->string ? chunk->string : "<null>");
931 #endif
932                                 chunk_list =
933                                         text_chunk_list_insert_chunk (chunk_list,
934                                                                       chunk);
935                         }
936                         iter = iter->next;
937                 }
938         }
939 }
940
941 static char*
942 review_buffer_composite (ScreenReviewBuffer *buffers[])
943 {
944         /* TODO: FIXME: something is wrong here, compositing fails */
945         int i;
946         GList *chunk_list, *iter;
947         TextChunk *chunk;
948         chunk_list = buffers[SPI_LAYER_BACKGROUND]->text_chunks;
949         for (i = 2; i < SPI_LAYER_LAST_DEFINED; ++i) {
950                 if (i == SPI_LAYER_WIDGET) i = SPI_LAYER_OVERLAY;
951                 /*
952                  * Q: why skip these layers ?
953                  * A: since layers WIDGET, MDI, and POPUP have already been
954                  *  composited into layer CANVAS for each toplevel before this
955                  *  routine is called.
956                  */
957                 iter = buffers[i]->text_chunks;
958 #ifdef CLIP_DEBUG
959                 fprintf (stderr, "layer %d has %d chunks\n",
960                          i, g_list_length (iter));
961 #endif          
962                 while (iter) {
963                         chunk = (TextChunk *) iter->data;
964                         if (chunk) {
965 #ifdef CLIP_DEBUG
966                                 fprintf (stderr, "inserting chunk <%s>\n",
967                                          chunk->string ? chunk->string : "<null>");
968 #endif
969                                 chunk_list =
970                                         text_chunk_list_insert_chunk (chunk_list,
971                                                                       chunk);
972                         }
973                         iter = iter->next;
974                 }
975         }
976         
977         chunk_list = buffers[SPI_LAYER_WIDGET]->text_chunks;
978         return text_chunk_list_to_string (chunk_list);
979 }
980
981 static char *
982 get_screen_review_line_at (int x, int y)
983 {
984   char *string;
985   Accessible *desktop, *app, *toplevel, *child;
986   AccessibleComponent *component;
987   AccessibleStateSet *states;
988   GList *toplevels = NULL, *actives = NULL, *iter;
989   ScreenReviewBuffer* reviewBuffers[SPI_LAYER_LAST_DEFINED];
990   BoundaryRect* clip_bounds[SPI_LAYER_LAST_DEFINED];
991   BoundaryRect toplevel_bounds;
992   int n_apps, n_toplevels, n_children, app_n, toplevel_n, child_n;
993   GTimer *timer = g_timer_new ();
994   int i;
995
996   for (i = 1; i < SPI_LAYER_LAST_DEFINED; ++i) {
997           reviewBuffers[i] = g_new0 (ScreenReviewBuffer, 1);
998           clip_bounds[i] = g_new0 (BoundaryRect, 1);
999           clip_bounds[i]->isClipped = FALSE;
1000           clip_bounds[i]->isEmpty = FALSE;
1001   }
1002   
1003   /* how do we decide which desktop ? */
1004   desktop = SPI_getDesktop (0);
1005   
1006   /* for each app */
1007   n_apps = Accessible_getChildCount (desktop);
1008   for (app_n = 0; app_n < n_apps; ++app_n) {
1009           /* for each toplevel in app */
1010           app =  Accessible_getChildAtIndex (desktop, app_n);
1011           n_toplevels = Accessible_getChildCount (app);
1012           for (toplevel_n = 0; toplevel_n < n_toplevels; ++toplevel_n) {
1013                   Accessible *toplevel = Accessible_getChildAtIndex (app, toplevel_n);
1014                   if (Accessible_isComponent (toplevel))
1015                           toplevels = g_list_prepend (toplevels, toplevel);
1016                   else {
1017                           Accessible_unref (toplevel);
1018                           fprintf (stderr, "warning, app toplevel not a component.\n");
1019                   }
1020           }
1021   }
1022   
1023   /* sort: at the moment we don't have a good way to sort except to put actives on top */
1024   for (iter = g_list_first (toplevels); iter; iter = iter->next) {
1025           Accessible *toplevel =
1026                   (Accessible *) iter->data;
1027           if (AccessibleStateSet_contains (Accessible_getStateSet (toplevel),
1028                                            SPI_STATE_ACTIVE)) {
1029                   actives = g_list_prepend (actives, toplevel);
1030           }
1031   }
1032
1033   for (iter = g_list_first (actives); iter; iter = actives->next) {
1034           toplevels = g_list_remove (toplevels, iter->data); /* place at end */
1035           toplevels = g_list_append (toplevels, iter->data);
1036   }
1037   g_list_free (actives);
1038
1039   /* for each toplevel, ending with the active one(s),
1040    * clip against children, putting results into appropriate charBuffer.
1041    */
1042   for (iter = g_list_first (toplevels); iter; iter = iter->next) {
1043           toplevel = (Accessible *) iter->data;
1044           if (Accessible_isComponent (toplevel)) {
1045               /* make sure toplevel is visible and not iconified or shaded */
1046               states = Accessible_getStateSet (toplevel);
1047               if (AccessibleStateSet_contains (states, SPI_STATE_VISIBLE)
1048                   && !AccessibleStateSet_contains (states, SPI_STATE_ICONIFIED)
1049                   || isJava) { /* isJava hack! */
1050                       component = Accessible_getComponent (toplevel);
1051                       AccessibleComponent_getExtents (component,
1052                                                       &toplevel_bounds.x,
1053                                                       &toplevel_bounds.y,
1054                                                       &toplevel_bounds.width,
1055                                                       &toplevel_bounds.height,
1056                                                       SPI_COORD_TYPE_SCREEN);
1057                       toplevel_bounds.isEmpty = FALSE;
1058                       for (i = 1; i < SPI_LAYER_LAST_DEFINED; ++i) {
1059                               *clip_bounds[i] = toplevel_bounds;
1060                       }
1061                       clip_into_buffers (toplevel, clip_bounds,
1062                                      reviewBuffers, x, y);
1063
1064                       toplevel_composite (reviewBuffers);
1065 #ifdef CHUNK_LIST_DEBUG
1066                       fprintf (stderr, "toplevel clip done\n");
1067                       debug_chunk_list (reviewBuffers[SPI_LAYER_WIDGET]->text_chunks);
1068 #endif                
1069               }
1070           }
1071           Accessible_unref (toplevel);
1072   }
1073
1074   string = review_buffer_composite (reviewBuffers);
1075
1076   /* SIMPLE SINGLE-PASS ALGORITHM:*/
1077   /* traverse the tree:
1078    *   keep a pointer to outermost instance of each layer
1079    *   clip against outermost in same layer
1080    *   when this clip occurs, store outermost clipped string in 2d string buffer.
1081    *   string buffer may have attributes to mark component bounds, line art,
1082    *      or attributes of text being reviewed.
1083    *   composite the layers, ignoring NULL chars in the string buffers.
1084    *
1085    * Limitations:
1086    *   sibling clip not correct, text may overwrite if siblings intersect onscreen
1087    *   length of resulting text buffer may vary!
1088    *
1089    * Technical issues:
1090    *   no API for ordering toplevels yet, other than knowing which is ACTIVE.
1091    *   not much implementation for the LAYER API yet, other than menus.
1092    */
1093   g_timer_stop (timer);
1094   fprintf (stderr, "elapsed time = %f s\n", g_timer_elapsed (timer, NULL));
1095   
1096   return string;
1097 }
1098
1099 void
1100 report_screen_review_line (const AccessibleEvent *event, void *user_data)
1101 {
1102   static Display *display = NULL;
1103   int x, y, win_x, win_y;
1104   Window root_return, child_return;
1105   unsigned int mask_return;
1106   
1107   if (!display) display = XOpenDisplay (getenv ("DISPLAY"));
1108   /*
1109    *  we would prefer to get the x,y info in the above event.
1110    *  At the moment we don't get detail params for "toolkit" events,
1111    *  so for testing purposes we use XQueryPointer.  Actual apps
1112    *  probably shouldn't do this.
1113    */
1114   XQueryPointer (display,
1115                  DefaultRootWindow (display),
1116                  &root_return, &child_return,
1117                  &x, &y,
1118                  &win_x, &win_y,
1119                  &mask_return);
1120
1121   fprintf (stderr, "screen review event %s at %d, %d\n", event->type,
1122            x, y);
1123   fprintf (stderr, "[%s]\n", 
1124            get_screen_review_line_at (x, y));
1125 }
1126
1127 void
1128 test_exit ()
1129 {
1130   SPI_deregisterGlobalEventListenerAll (mouseclick_listener);
1131   AccessibleEventListener_unref (mouseclick_listener);
1132 }