Tweaks and fixes to screen-review-test.c; text clipping is substantially
[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  * * bounds now fail to include window border decoration
34  *         (which we can't really know about).
35  *
36  * * brute-force algorithm uses no client-side cache; performance mediocre.
37  *
38  * * we don't have good ordering information for the toplevel windows,
39  *          and our current heuristic is not guaranteed if
40  *          the active window is not always on top.
41  *
42  * * text-bearing objects that don't implement AccessibleText (such as buttons)
43  *          don't get their text clipped since we don't know the character
44  *          bounding boxes.
45  * * can't know about "inaccessible" objects that may be obscuring the
46  *          accessible windows (inherent to API-based approach).
47  *
48  * * this implementation doesn't worry about text column-alignment, it generates
49  *          review lines of more-or-less arbitrary length.  The x-coordinate
50  *          info is preserved in the reviewBuffers list structures, but the
51  *          "buffer-to-string" algorithm ignores it.
52  *
53  * * (others).
54  */
55
56
57 #define BOUNDS_CONTAIN_X_BOUNDS(b, p)  ( ((p).x>=(b).x) &&\
58                                          (((p).x + (p).width) <= \
59                                          ((b).x + (b).width)) && \
60                                         ((b).width > 0) && ((b).height > 0))
61
62 #define BOUNDS_CONTAIN_Y(b, p)  (((p)>=(b)->y) && ((p)<=((b)->y + (b)->height))\
63             && ((b)->width > 0) && ((b)->height > 0))
64
65 #define CHUNK_BOUNDS_BEFORE_START(c, i) ((i) && ((c)->clip_bounds.x < \
66                                          (i)->clip_bounds.x)) 
67
68 #define CHUNK_BOUNDS_END_BEFORE_START(c, i) ((i) && \
69                                          (((c)->clip_bounds.x + \
70                                          (c)->clip_bounds.width) < \
71                                          (i)->clip_bounds.x))
72
73 #define CHUNK_BOUNDS_AFTER_END(c, i) ((i) && ((c)->clip_bounds.x >= \
74                                        ((i)->clip_bounds.x + \
75                                         (i)->clip_bounds.width)))
76
77 #define CHUNK_BOUNDS_SPANS_END(c, i) ((i) && (((c)->clip_bounds.x + \
78                                               (c)->clip_bounds.width) >= \
79                                        ((i)->clip_bounds.x + \
80                                         (i)->clip_bounds.width)))
81 /*
82  * #define CHUNK_BOUNDS_WITHIN(c, i)  ((i) && ((c)->clip_bounds.x >= \
83  *                               (i)->text_bounds.x) && \
84  *                               (((c)->clip_bounds.x + (c)->clip_bounds.width) \
85  *                             <= ((i)->text_bounds.x + (i)->text_bounds.width)))
86  */
87
88 #define IS_CLIPPING_CONTAINER(a) ((a) != SPI_ROLE_PAGE_TAB)
89
90 static void report_screen_review_line  (const AccessibleEvent *event, void *user_data);
91
92 static gint n_elements_traversed = 0;
93 static AccessibleEventListener *mouseclick_listener;
94
95 typedef struct _BoundaryRect {
96         long int x;
97         long int y;
98         long int width;
99         long int height;
100         AccessibleRole role; /* role of last clipping element */
101         gboolean isClipped;
102         gboolean isEmpty;
103 } BoundaryRect;
104
105 typedef struct _TextChunk {
106         char           *string;
107         AccessibleText *source;
108         int             start_offset;
109         int             end_offset;
110         BoundaryRect    clip_bounds;
111         BoundaryRect    text_bounds;
112         BoundaryRect    start_char_bounds;
113         BoundaryRect    end_char_bounds;
114 } TextChunk;
115
116 typedef struct _ScreenReviewBuffer { /* TODO: implement */
117         GList *text_chunks;
118 } ScreenReviewBuffer;
119
120 static gboolean isJava = FALSE;
121
122 int
123 main (int argc, char **argv)
124 {
125   int i, j;
126   int n_desktops;
127   int n_apps;
128   char *s;
129   GTimer *timer;
130   gdouble elapsed_time;
131   Accessible *desktop;
132   Accessible *application;
133   const char *modules;
134
135   SPI_init ();
136
137   mouseclick_listener = SPI_createAccessibleEventListener (
138           report_screen_review_line, NULL); 
139
140   SPI_registerGlobalEventListener (mouseclick_listener,
141                                    "Gtk:GtkWidget:button-press-event");
142 #undef JAVA_TEST_HACK
143 #ifdef JAVA_TEST_HACK /* Only use this to test Java apps */
144   SPI_registerGlobalEventListener (mouseclick_listener,
145                                    "object:text-caret-moved");
146   isJava = TRUE;
147 #endif
148   SPI_event_main ();
149
150   putenv ("AT_BRIDGE_SHUTDOWN=1");
151
152   /*
153    * TODO: Add a key event listener that calls test_exit, to
154    * deregister and cleanup appropriately.
155    */
156
157   return SPI_exit ();
158 }
159
160 static inline gboolean
161 chunk_bounds_within (TextChunk *chunk, TextChunk *test_chunk)
162 {
163         int x1, x2, tx1, tx2;
164         gboolean gtx1, ltx2;
165
166         x1 = chunk->clip_bounds.x;
167         x2 = x1 + chunk->clip_bounds.width;
168         tx1 = test_chunk->clip_bounds.x;
169         tx2 = tx1 + test_chunk->clip_bounds.width;
170         gtx1 = (chunk->clip_bounds.x >= test_chunk->clip_bounds.x);
171         ltx2 = (chunk->clip_bounds.x + chunk->clip_bounds.width
172                 <= test_chunk->clip_bounds.x + test_chunk->clip_bounds.width);
173         return gtx1 && ltx2;
174 }
175
176 #define CHUNK_BOUNDS_WITHIN(a, b) chunk_bounds_within(a, b)
177
178 static BoundaryRect **
179 clip_bounds_clone (BoundaryRect *bounds[])
180 {
181         int i;
182         BoundaryRect **bounds_clone;
183         bounds_clone = (BoundaryRect **)
184                 g_new0 (gpointer, SPI_LAYER_LAST_DEFINED);
185         for (i = 0; i < SPI_LAYER_LAST_DEFINED; ++i) {
186                 bounds_clone[i] = g_new0 (BoundaryRect, 1);
187                 *bounds_clone[i] = *bounds[i];
188         }
189         return bounds_clone;
190 }
191
192 static void
193 boundary_clip (BoundaryRect *bounds, BoundaryRect *clipBounds)
194 {
195         int x2 = bounds->x + bounds->width;
196         int y2 = bounds->y + bounds->height;
197 #ifdef CLIP_DEBUG
198         fprintf (stderr, "bclip %d-%d, %d-%d; ",
199                  bounds->x, x2,
200                  clipBounds->x, clipBounds->x+clipBounds->width);
201 #endif           
202         bounds->x = MAX (bounds->x, clipBounds->x);
203         bounds->y = MAX (bounds->y, clipBounds->y);
204         x2 =  MIN (x2,  clipBounds->x + clipBounds->width);
205         y2 =  MIN (y2, clipBounds->y + clipBounds->height);
206         bounds->width = MAX (x2 - bounds->x, 0);
207         bounds->height = MAX (y2 - bounds->y, 0);
208         if (!bounds->width || !bounds->height)
209                 bounds->isEmpty = TRUE;
210         if (IS_CLIPPING_CONTAINER (bounds->role)) {
211                 *clipBounds = *bounds;
212         }
213 #ifdef CLIP_DEBUG
214         fprintf (stderr, "%d-%d\n",
215                  bounds->x, bounds->x+bounds->width);
216 #endif
217 }
218
219 static void
220 boundary_xclip_head (BoundaryRect *bounds, BoundaryRect *clipBounds)
221 {
222         int x2;
223         int cx2 = clipBounds->x + clipBounds->width;
224         if (cx2 < bounds->x) return;
225         x2 = bounds->x + bounds->width;
226         if (cx2 < x2) bounds->x = cx2;
227         bounds->width = MAX (0, x2 - cx2);
228 }
229
230 static void
231 boundary_xclip_tail (BoundaryRect *bounds, BoundaryRect *clipBounds)
232 {
233         int x2 = bounds->x + bounds->width;
234         if (clipBounds->x > x2) return;
235         bounds->width = MAX (0, clipBounds->x - bounds->x);
236 }
237
238 static TextChunk*
239 text_chunk_copy (TextChunk *chunk)
240 {
241         TextChunk *copy = g_new0 (TextChunk, 1);
242         *copy = *chunk;
243         if (chunk->string) copy->string = g_strdup (chunk->string);
244         if (copy->source) AccessibleText_ref (copy->source);
245         return copy;
246 }
247
248 static void
249 text_chunk_tail_clip (TextChunk *bottom, TextChunk *top)
250 {
251 #ifdef CLIP_DEBUG
252         fprintf (stderr, "bottom %d-%d, top %d-%d;",
253                  bottom->clip_bounds.x,
254                  bottom->clip_bounds.x + bottom->clip_bounds.width,
255                  top->clip_bounds.x,
256                  top->clip_bounds.x + top->clip_bounds.width);
257 #endif
258         boundary_xclip_tail (&bottom->text_bounds, &top->clip_bounds);
259         boundary_xclip_tail (&bottom->clip_bounds, &top->clip_bounds);
260         bottom->text_bounds.isClipped = TRUE;
261         bottom->clip_bounds.isClipped = TRUE;
262 #ifdef CLIP_DEBUG
263         fprintf (stderr, "result %d-%d\n",
264                  bottom->clip_bounds.x,
265                  bottom->clip_bounds.x + bottom->clip_bounds.width);
266 #endif
267 }
268
269 static void
270 text_chunk_head_clip (TextChunk *bottom, TextChunk *top)
271 {
272 #ifdef CLIP_DEBUG
273         fprintf (stderr, "bottom %d-%d, top %d-%d;",
274                  bottom->clip_bounds.x,
275                  bottom->clip_bounds.x + bottom->clip_bounds.width,
276                  top->clip_bounds.x,
277                  top->clip_bounds.x + top->clip_bounds.width);
278 #endif  
279         boundary_xclip_head (&bottom->text_bounds, &top->clip_bounds);
280         boundary_xclip_head (&bottom->clip_bounds, &top->clip_bounds);
281         bottom->text_bounds.isClipped = TRUE;
282         bottom->clip_bounds.isClipped = TRUE;
283 #ifdef CLIP_DEBUG       
284         fprintf (stderr, "result %d-%d\n",
285                  bottom->clip_bounds.x,
286                  bottom->clip_bounds.x + bottom->clip_bounds.width);
287 #endif  
288 }
289
290 static GList *
291 text_chunk_split_insert (GList *chunk_list, GList *iter, TextChunk *chunk)
292 {
293         TextChunk *iter_chunk = iter->data;
294         TextChunk *iter_copy = text_chunk_copy (iter_chunk);
295         /* TODO: FIXME something is wrong here */
296 #ifdef CLIP_DEBUG
297         fprintf (stderr, "***clip insert of %s into %s\n", 
298                  chunk->string ? chunk->string : "<null>",
299                  iter_chunk->string ? iter_chunk->string : "<null>");
300 #endif  
301         chunk_list = g_list_insert_before (chunk_list, iter, iter_copy);
302         text_chunk_tail_clip (iter_copy, chunk);
303         chunk_list = g_list_insert_before (chunk_list, iter, chunk);
304         text_chunk_head_clip (iter_chunk, chunk);
305         return chunk_list;
306 }
307
308 /* #define PRINT_CHUNK_DEBUG(a, b, c, d) print_chunk_debug(a, b, c, d) */
309
310 #define PRINT_CHUNK_DEBUG(a, b, c, d) 
311
312 #ifdef PRINT_CHUNK_DEBUG
313 static void
314 print_chunk_debug (TextChunk *chunk, char *opname, GList *prev, GList *next)
315 {
316         fprintf (stderr, "%sing chunk %s between %s and %s; %d-%d\n",
317                  opname,
318                  chunk->string,
319                  (prev ? ((TextChunk *) prev->data)->string : "<null>"),
320                  (next ? ((TextChunk *) next->data)->string : "<null>"),
321                  chunk->clip_bounds.x,
322                  chunk->text_bounds.x + chunk->text_bounds.width);
323 }
324 #endif
325
326 static GList *
327 text_chunk_list_head_clip (GList *text_chunk_list,
328                            TextChunk *chunk,
329                            GList *next)
330 {
331         GList *target, *iter = next, *prev;
332         prev = iter->prev;
333 //      if (chunk->string && strlen (chunk->string)) { 
334                 text_chunk_list =
335                         g_list_insert_before (text_chunk_list, next, chunk);
336 //      }
337         while (iter) {
338                 if (CHUNK_BOUNDS_SPANS_END (chunk, (TextChunk *)iter->data)) {
339 #ifdef CLIP_DEBUG                       
340                         fprintf (stderr, "deleting %s\n",
341                                  ((TextChunk *)iter->data)->string);
342 #endif                  
343                         target = iter;
344                         iter = iter->next;
345                         text_chunk_list =
346                                 g_list_delete_link (text_chunk_list, target);
347                 } else {
348                         if (!CHUNK_BOUNDS_END_BEFORE_START (chunk,
349                                                       (TextChunk *)iter->data)) {
350                                 text_chunk_head_clip ((TextChunk *)iter->data,
351                                                       chunk);
352                         }
353                         if (prev &&
354                             !CHUNK_BOUNDS_AFTER_END (
355                                     chunk,
356                                     (TextChunk *)prev->data)) {
357                                 text_chunk_tail_clip (
358                                         (TextChunk *)prev->data,
359                                         chunk);
360                         }
361                         break;
362                 }
363         }
364         
365         return text_chunk_list;
366 }
367
368 static GList *
369 text_chunk_list_clip_and_insert (GList *text_chunk_list,
370                                  TextChunk *chunk,
371                                  GList *prev,
372                                  GList *next)
373 {
374 #ifdef CLIP_DEBUG
375         if (chunk->string)
376                 fprintf (stderr, "clip-and-insert for %s, between %s and %s\n",
377                          chunk->string,
378                          (prev && ((TextChunk *)prev->data)->string ? ((TextChunk *)prev->data)->string : "<null>"),
379                          (next && ((TextChunk *)next->data)->string ? ((TextChunk *)next->data)->string : "<null>"));
380 #endif
381         /* cases: */
382         if (!prev && !next) { /* first element in, no clip needed */
383 //              if (chunk->string && strlen (chunk->string)) {
384                         text_chunk_list =
385                                 g_list_append (text_chunk_list, chunk);
386                         PRINT_CHUNK_DEBUG (chunk, "append",
387                                            prev,
388                                            NULL);
389 //              }
390         } else { /* check for clip with prev */
391                 /* if we split the prev */
392                 if (prev &&
393                     CHUNK_BOUNDS_WITHIN (chunk, (TextChunk *) prev->data)) {
394                         text_chunk_list =
395                                 text_chunk_split_insert (
396                                         text_chunk_list,
397                                         prev, chunk);
398                 } else if (next) { 
399                     /* we split the 'next' element */
400                     if (CHUNK_BOUNDS_WITHIN (chunk, (TextChunk *)next->data)) {
401                             text_chunk_list =
402                                     text_chunk_split_insert (text_chunk_list,
403                                                              next, chunk);
404                     } else {
405                             /* do an insert +  head clip */
406                             text_chunk_list =
407                                     text_chunk_list_head_clip (
408                                             text_chunk_list,
409                                             chunk,
410                                             next);
411                     }
412                 } else {
413                         if (!CHUNK_BOUNDS_AFTER_END (chunk,
414                                                      (TextChunk *) prev->data)) {
415                                 text_chunk_tail_clip (prev->data, chunk);
416                         }
417 //                      if (chunk->string && strlen (chunk->string)) {
418                                 text_chunk_list =
419                                         g_list_append (text_chunk_list, chunk);
420 //                      }
421                 }
422         }
423         return text_chunk_list;
424 }
425
426 static GList *
427 text_chunk_list_insert_chunk (GList *text_chunk_list, TextChunk *chunk)
428 {
429         GList *iter = g_list_first (text_chunk_list);
430         TextChunk *iter_chunk = NULL;
431         do {
432                 if (iter) iter_chunk = (TextChunk *) iter->data;
433                 /* if we're ahead of the current element */
434                 if (!iter) {
435                         text_chunk_list =
436                                 text_chunk_list_clip_and_insert (text_chunk_list,
437                                                                  chunk,
438                                                                  iter,
439                                                                  NULL);
440                         break;
441                 } else if (CHUNK_BOUNDS_BEFORE_START (chunk, iter_chunk)) {
442                         text_chunk_list =
443                                 text_chunk_list_clip_and_insert (text_chunk_list,
444                                                                  chunk,
445                                                                  iter->prev,
446                                                                  iter);
447                         break;
448                 } else if (!iter->next ) {
449                         text_chunk_list =
450                                 text_chunk_list_clip_and_insert (text_chunk_list,
451                                                                  chunk,
452                                                                  iter,
453                                                                  NULL);
454                         break;
455                 }
456                 if (iter) iter = iter->next;
457         } while (iter);
458         return text_chunk_list;
459 }
460
461 static TextChunk *
462 review_buffer_get_text_chunk (ScreenReviewBuffer *reviewBuffer,
463                               Accessible *accessible, BoundaryRect *bounds,
464                               int screen_x, int screen_y)
465 {
466         AccessibleText *text = NULL;
467         AccessibleRole role;
468         TextChunk *text_chunk;
469         BoundaryRect start_char_bounds, end_char_bounds;
470         char *s = NULL;
471         int offset;
472         long start, end;
473         long x2, y2;
474
475         role = Accessible_getRole (accessible);
476         text_chunk = g_new0 (TextChunk, 1);
477         text_chunk->clip_bounds = *bounds;
478         if (Accessible_isText (accessible)) {
479                 text = Accessible_getText (accessible);
480                 offset = AccessibleText_getOffsetAtPoint (text,
481                                                           screen_x,
482                                                           screen_y,
483                                                           SPI_COORD_TYPE_SCREEN);
484                 s = AccessibleText_getTextAtOffset (text, offset,
485                                                     SPI_TEXT_BOUNDARY_LINE_START,
486                                                     &start, &end);
487                 if (end > start) {
488                         AccessibleText_getCharacterExtents (
489                                 text, start,
490                                 &text_chunk->start_char_bounds.x,
491                                 &text_chunk->start_char_bounds.y,
492                                 &text_chunk->start_char_bounds.width,
493                                 &text_chunk->start_char_bounds.height,
494                                 SPI_COORD_TYPE_SCREEN);
495 #ifdef CLIP_DEBUG
496                         fprintf (stderr, "%s: start char (%d) x, width %d %d; ",
497                                  s,
498                                  start,
499                                  text_chunk->start_char_bounds.x,
500                                  text_chunk->start_char_bounds.width);
501 #endif
502                         if (s && strlen (s) && s[strlen (s) - 1] == '\n')
503                                 end--;
504                         AccessibleText_getCharacterExtents (
505                                 text, end - 1,
506                                 &text_chunk->end_char_bounds.x,
507                                 &text_chunk->end_char_bounds.y,
508                                 &text_chunk->end_char_bounds.width,
509                                 &text_chunk->end_char_bounds.height,
510                                 SPI_COORD_TYPE_SCREEN);
511 #ifdef CLIP_DEBUG                       
512                         fprintf (stderr, "end char (%d) x, width %d %d\n",
513                                  end - 1,
514                                  text_chunk->end_char_bounds.x,
515                                  text_chunk->end_char_bounds.width);
516 #endif
517                 }
518                 text_chunk->text_bounds.x = MIN (text_chunk->start_char_bounds.x,
519                                                  text_chunk->end_char_bounds.x);
520                 text_chunk->text_bounds.y = MIN (text_chunk->start_char_bounds.y,
521                                                  text_chunk->end_char_bounds.y);
522                 x2 = MAX (text_chunk->start_char_bounds.x +
523                           text_chunk->start_char_bounds.width,
524                           text_chunk->end_char_bounds.x +
525                           text_chunk->end_char_bounds.width);
526                 text_chunk->text_bounds.width = x2 - text_chunk->text_bounds.x;
527                 y2 = MAX (text_chunk->start_char_bounds.y +
528                           text_chunk->start_char_bounds.height,
529                           text_chunk->end_char_bounds.y + 
530                           text_chunk->end_char_bounds.height);
531                 text_chunk->text_bounds.height = y2 - text_chunk->text_bounds.y;
532                 text_chunk->start_offset = start;
533                 text_chunk->end_offset = end;
534                 if (text_chunk->text_bounds.x < text_chunk->clip_bounds.x) {
535                         text_chunk->text_bounds.x = text_chunk->clip_bounds.x;
536                         text_chunk->text_bounds.isClipped = TRUE;
537                 } 
538                 if ((text_chunk->text_bounds.x +
539                      text_chunk->text_bounds.width)
540                     > (text_chunk->clip_bounds.x +
541                        text_chunk->clip_bounds.width)) {
542                         text_chunk->text_bounds.width =
543                                 MAX (0, (text_chunk->clip_bounds.x +
544                                          text_chunk->clip_bounds.width) -
545                                      text_chunk->text_bounds.x);
546                         text_chunk->text_bounds.isClipped = TRUE;
547                 }
548                 if (!BOUNDS_CONTAIN_Y (&text_chunk->text_bounds,
549                                        screen_y)) {
550 #ifdef CLIP_DEBUG                       
551                         fprintf (stderr, "%s out of bounds (%d-%d)\n", s,
552                                  text_chunk->text_bounds.y,
553                                  text_chunk->text_bounds.y +
554                                  text_chunk->text_bounds.height);
555 #endif                  
556                         s = NULL;
557                 }
558         } else {
559                 if (role == SPI_ROLE_PUSH_BUTTON ||
560                     role == SPI_ROLE_CHECK_BOX ||
561                     role == SPI_ROLE_MENU ||
562                     role == SPI_ROLE_MENU_ITEM) { /* don't like this
563                                                      special casing :-( */
564                         s = Accessible_getName (accessible);
565                         /* use name instead */
566                         text_chunk->text_bounds = text_chunk->clip_bounds;
567                         text_chunk->start_offset = 0;
568                         text_chunk->end_offset = strlen (s);
569                 }
570         }
571         if (s && strlen (s)) {
572                 if (s[strlen(s)-1] == '\n') s[strlen(s)-1] = ' ';
573                 /* XXX: if last char is newline, aren't its bounds wrong now? */
574                 text_chunk->string = s;
575                 text_chunk->source = text;
576                 if (text) AccessibleText_ref (text);
577 #ifdef CLIP_DEBUG
578                 fprintf (stderr, "%s, bounds %d-%d; clip %d-%d\n",
579                          s,
580                          text_chunk->text_bounds.x,
581                          text_chunk->text_bounds.x+text_chunk->text_bounds.width,
582                          text_chunk->clip_bounds.x,
583                          text_chunk->clip_bounds.x+text_chunk->clip_bounds.width);
584 #endif          
585         } else {
586                 text_chunk->string = NULL;
587                 text_chunk->source = NULL;
588         }
589         if (text) AccessibleText_unref (text);
590         return text_chunk;
591 }
592
593 static void
594 debug_chunk_list (GList *iter)
595 {
596         TextChunk *chunk;
597         while (iter) {
598                 chunk = (TextChunk *)iter->data;
599                 fprintf (stderr, "Chunk %s, clip %d-%d, text %d-%d\n",
600                          chunk->string,
601                          chunk->clip_bounds.x,
602                          chunk->clip_bounds.x + chunk->clip_bounds.width,
603                          chunk->text_bounds.x,
604                          chunk->text_bounds.x + chunk->text_bounds.width);
605                 iter = iter->next;
606         }
607 }
608
609 static void
610 clip_into_buffers (Accessible *accessible,  BoundaryRect* parentClipBounds[],
611                    ScreenReviewBuffer *reviewBuffers[],
612                    int screen_x, int screen_y)
613 {
614         int n_children, child_n;
615         Accessible *child;
616         BoundaryRect bounds;
617         BoundaryRect** clip_bounds;
618         TextChunk *text_chunk;
619         AccessibleComponent *component;
620         AccessibleRole role;
621         int layer;
622
623         clip_bounds = clip_bounds_clone (parentClipBounds);
624         if (Accessible_isComponent (accessible)) {
625                 role = Accessible_getRole (accessible);
626                 component = Accessible_getComponent (accessible);
627                 layer = AccessibleComponent_getLayer (component);
628                 bounds = *clip_bounds[layer];
629                 if (!bounds.isEmpty) {
630                         AccessibleComponent_getExtents (component,
631                                                         &bounds.x,
632                                                         &bounds.y,
633                                                         &bounds.width,
634                                                         &bounds.height,
635                                                         SPI_COORD_TYPE_SCREEN);
636                         bounds.role = role;
637                         if (clip_bounds[layer])
638                                 boundary_clip (&bounds, clip_bounds[layer]);
639                         if (BOUNDS_CONTAIN_Y (&bounds, screen_y)) {
640                                 text_chunk = review_buffer_get_text_chunk (
641                                         reviewBuffers[layer], accessible, &bounds,
642                                         screen_x, screen_y);
643                                 reviewBuffers[layer]->text_chunks = 
644                                         text_chunk_list_insert_chunk (
645                                                 reviewBuffers[layer]->text_chunks,
646                                                 text_chunk);
647                         } else {
648                                 bounds.isEmpty =
649                                         IS_CLIPPING_CONTAINER (bounds.role);
650                         }
651                 } 
652                 Accessible_unref (component);
653         }
654         /*
655          * we always descend into children in case they are in a higher layer
656          * this can of course be optimized for the topmost layer...
657          * but nobody uses that one! (SPI_LAYER_OVERLAY)
658          */
659         n_children = Accessible_getChildCount (accessible);
660         for (child_n = 0; child_n < n_children; ++child_n) {
661                 child = Accessible_getChildAtIndex (accessible, child_n);
662                 clip_into_buffers (child, clip_bounds, reviewBuffers, screen_x, screen_y);
663                 Accessible_unref (child);
664         }
665         /* TODO: free the parent clip bounds */
666 }
667
668 #undef CHARACTER_CLIP_DEBUG
669
670 static char*
671 text_chunk_get_clipped_substring_by_char (TextChunk *chunk, int start, int end)
672 {
673         BoundaryRect char_bounds;
674         int i;
675         GString *string = g_string_new ("");
676         gunichar c;
677         for (i = start; i < end; ++i) {
678                 AccessibleText_getCharacterExtents (chunk->source,
679                                                     i,
680                                                     &char_bounds.x,
681                                                     &char_bounds.y,
682                                                     &char_bounds.width,
683                                                     &char_bounds.height,
684                                                     SPI_COORD_TYPE_SCREEN);
685 #ifdef CHARACTER_CLIP_DEBUG
686                 fprintf (stderr, "testing %d-%d against %d-%d\n",
687                          char_bounds.x, char_bounds.x+char_bounds.width,
688                          chunk->text_bounds.x,
689                          chunk->text_bounds.x + chunk->text_bounds.width);
690 #endif
691                 if (BOUNDS_CONTAIN_X_BOUNDS (chunk->text_bounds,
692                                              char_bounds)) {
693                         c = AccessibleText_getCharacterAtOffset (
694                                 chunk->source, i);
695 #ifdef CLIP_DEBUG                               
696                         fprintf (stderr, "[%c]", c);
697 #endif                          
698                         g_string_append_unichar (string, c);
699                 }
700         }
701         return string->str;
702                 g_string_free (string, FALSE);
703
704 }
705
706
707 /*
708  * Note: this routine shouldn't have to do as much as it currently does,
709  *       but at the moment it works around a pango?/gail? bug which
710  *       causes WORD boundary type queries to return incorrect strings.
711  */
712 static char *
713 string_strip_newlines (char *s, long offset, long *start_offset, long *end_offset)
714 {
715         int i;
716         char *word_start = s;
717         /* FIXME: potential memory leak here */
718         for (i=0; s && s[i]; ++i)
719         {
720                 if (s [i] == '\n' && i > (offset - *start_offset) ) {
721                         s [i] = '\0';
722                         *end_offset = *start_offset + i;
723                         return word_start;
724                 } else if (s [i] == '\n') {
725                         word_start = &s[i + 1];
726                 }
727         }
728         return word_start;
729 }
730
731 static char*
732 text_chunk_get_clipped_string (TextChunk *chunk)
733 {
734         char *s, *string = "";
735         int i;
736         long start = chunk->start_offset, end = chunk->end_offset;
737         long word_start, word_end, range_end;
738         BoundaryRect start_bounds, end_bounds;
739         gboolean start_inside, end_inside;
740         if (!chunk->text_bounds.isClipped)
741                 string = chunk->string;
742         else if (chunk->source) {
743 #ifdef CLIP_DEBUG               
744                 fprintf (stderr, "clipping %s\n", chunk->string);
745 #endif
746                 /* while words at offset lie within the bounds, add them */
747                 do {
748                     s = AccessibleText_getTextAtOffset (chunk->source,
749                                                         start,
750                                                 SPI_TEXT_BOUNDARY_WORD_START,
751                                                         &word_start,
752                                                         &word_end);
753                     range_end = word_end;
754                     s = string_strip_newlines (s, start, &word_start, &word_end);
755                     AccessibleText_getCharacterExtents (chunk->source,
756                                                         word_start,
757                                                         &start_bounds.x,
758                                                         &start_bounds.y,
759                                                         &start_bounds.width,
760                                                         &start_bounds.height,
761                                                         SPI_COORD_TYPE_SCREEN);
762                     AccessibleText_getCharacterExtents (chunk->source,
763                                                         word_end - 1,
764                                                         &end_bounds.x,
765                                                         &end_bounds.y,
766                                                         &end_bounds.width,
767                                                         &end_bounds.height,
768                                                         SPI_COORD_TYPE_SCREEN);
769                     start_inside = BOUNDS_CONTAIN_X_BOUNDS (chunk->text_bounds,
770                                                             start_bounds);
771                     end_inside = BOUNDS_CONTAIN_X_BOUNDS (chunk->text_bounds,
772                                                           end_bounds);
773                     if (start_inside && end_inside) {
774                             /* word is contained in bounds */
775                             string = g_strconcat (string, s, NULL);
776                     } else if (start_inside || end_inside) {
777                             /* one end of word is in */
778                             if (word_end > end) word_end = end;
779                             s = text_chunk_get_clipped_substring_by_char (
780                                     chunk,
781                                     MAX (word_start, chunk->start_offset),
782                                     MIN (word_end, chunk->end_offset));
783                             string = g_strconcat (string, s, NULL);
784                     } else {
785                     }
786                     start = range_end;
787                 } while (start < chunk->end_offset);
788         } else { /* we're clipped, but don't implement AccessibleText :-( */
789                 /* punt for now, maybe we can do betterc someday */
790                 string = chunk->string;
791         }
792         return string;
793 }
794
795 static char*
796 text_chunk_list_to_string (GList *iter)
797 {
798         char *s = "";
799         char *string;
800         TextChunk *chunk = NULL;
801         while (iter) {
802                 chunk = (TextChunk *)iter->data;
803                 if (chunk /* && chunk->string */) {
804                         string = text_chunk_get_clipped_string (chunk);
805                         if (string)
806                                 s = g_strconcat (s, "|", string, NULL);
807                         else /* XXX test */
808                                 s = g_strconcat (s, ":", NULL);
809                 }
810                 iter = iter->next;
811         }
812         s = g_strconcat (s, "|", NULL);
813         return s;
814 }
815
816 static char*
817 review_buffer_composite (ScreenReviewBuffer *buffers[])
818 {
819         int i;
820         GList *chunk_list, *iter;
821         TextChunk *chunk;
822 #ifdef NEED_TO_FIX_THIS 
823         chunk_list = buffers[0]->text_chunks;
824         for (i = 1; i < SPI_LAYER_LAST_DEFINED; ++i) {
825                 iter = buffers[i]->text_chunks;
826 #ifdef CLIP_DEBUG
827                 fprintf (stderr, "layer %d has %d chunks\n",
828                          i, g_list_length (iter));
829 #endif          
830                 while (iter) {
831                         chunk = (TextChunk *) iter->data;
832                         if (chunk) {
833 #ifdef CLIP_DEBUG
834                                 fprintf (stderr, "inserting chunk <%s>\n",
835                                          chunk->string ? chunk->string : "<null>");
836 #endif
837                                 chunk_list =
838                                         text_chunk_list_insert_chunk (chunk_list,
839                                                                       chunk);
840                         }
841                         iter = iter->next;
842                 }
843         }
844 #endif
845         chunk_list = buffers[SPI_LAYER_WIDGET]->text_chunks;
846         return text_chunk_list_to_string (chunk_list);
847 }
848
849 static char *
850 get_screen_review_line_at (int x, int y)
851 {
852   char *string;
853   Accessible *desktop, *app, *toplevel, *child;
854   AccessibleComponent *component;
855   AccessibleStateSet *states;
856   GList *toplevels = NULL, *actives = NULL, *iter;
857   ScreenReviewBuffer* reviewBuffers[SPI_LAYER_LAST_DEFINED];
858   BoundaryRect* clip_bounds[SPI_LAYER_LAST_DEFINED];
859   BoundaryRect toplevel_bounds;
860   int n_apps, n_toplevels, n_children, app_n, toplevel_n, child_n;
861   GTimer *timer = g_timer_new ();
862   int i;
863
864   for (i = 0; i < SPI_LAYER_LAST_DEFINED; ++i) {
865           reviewBuffers[i] = g_new0 (ScreenReviewBuffer, 1);
866           clip_bounds[i] = g_new0 (BoundaryRect, 1);
867           clip_bounds[i]->isClipped = FALSE;
868           clip_bounds[i]->isEmpty = FALSE;
869   }
870   
871   /* how do we decide which desktop ? */
872   desktop = SPI_getDesktop (0);
873   
874   /* for each app */
875   n_apps = Accessible_getChildCount (desktop);
876   for (app_n = 0; app_n < n_apps; ++app_n) {
877           /* for each toplevel in app */
878           app =  Accessible_getChildAtIndex (desktop, app_n);
879           n_toplevels = Accessible_getChildCount (app);
880           for (toplevel_n = 0; toplevel_n < n_toplevels; ++toplevel_n) {
881                   Accessible *toplevel = Accessible_getChildAtIndex (app, toplevel_n);
882                   if (Accessible_isComponent (toplevel))
883                           toplevels = g_list_prepend (toplevels, toplevel);
884                   else {
885                           Accessible_unref (toplevel);
886                           fprintf (stderr, "warning, app toplevel not a component.\n");
887                   }
888           }
889   }
890   
891   /* sort: at the moment we don't have a good way to sort except to put actives on top */
892   for (iter = g_list_first (toplevels); iter; iter = iter->next) {
893           Accessible *toplevel =
894                   (Accessible *) iter->data;
895           if (AccessibleStateSet_contains (Accessible_getStateSet (toplevel),
896                                            SPI_STATE_ACTIVE)) {
897                   actives = g_list_prepend (actives, toplevel);
898           }
899   }
900
901   for (iter = g_list_first (actives); iter; iter = actives->next) {
902           toplevels = g_list_remove (toplevels, iter->data); /* place at end */
903           toplevels = g_list_append (toplevels, iter->data);
904   }
905   g_list_free (actives);
906
907   /* for each toplevel, ending with the active one(s),
908    * clip against children, putting results into appropriate charBuffer.
909    */
910   for (iter = g_list_first (toplevels); iter; iter = iter->next) {
911           toplevel = (Accessible *) iter->data;
912           if (Accessible_isComponent (toplevel)) {
913               /* make sure toplevel is visible and not iconified or shaded */
914               states = Accessible_getStateSet (toplevel);
915               if (AccessibleStateSet_contains (states, SPI_STATE_VISIBLE)
916                   && !AccessibleStateSet_contains (states, SPI_STATE_ICONIFIED)
917                   || isJava) { /* isJava hack! */
918                       component = Accessible_getComponent (toplevel);
919                       AccessibleComponent_getExtents (component,
920                                                       &toplevel_bounds.x,
921                                                       &toplevel_bounds.y,
922                                                       &toplevel_bounds.width,
923                                                       &toplevel_bounds.height,
924                                                       SPI_COORD_TYPE_SCREEN);
925                       toplevel_bounds.isEmpty = FALSE;
926                       for (i = 0; i < SPI_LAYER_LAST_DEFINED; ++i) {
927                               *clip_bounds[i] = toplevel_bounds;
928                       }
929                       clip_into_buffers (toplevel, clip_bounds,
930                                      reviewBuffers, x, y);
931 #ifdef CLIP_DEBUG
932                       fprintf (stderr, "toplevel clip done\n");
933                       debug_chunk_list (reviewBuffers[SPI_LAYER_WIDGET]->text_chunks);
934 #endif                
935               }
936           }
937           Accessible_unref (toplevel);
938   }
939
940   string = review_buffer_composite (reviewBuffers);
941
942   /* SIMPLE SINGLE-PASS ALGORITHM:*/
943   /* traverse the tree:
944    *   keep a pointer to outermost instance of each layer
945    *   clip against outermost in same layer
946    *   when this clip occurs, store outermost clipped string in 2d string buffer.
947    *   string buffer may have attributes to mark component bounds, line art,
948    *      or attributes of text being reviewed.
949    *   composite the layers, ignoring NULL chars in the string buffers.
950    *
951    * Limitations:
952    *   sibling clip not correct, text may overwrite if siblings intersect onscreen
953    *   length of resulting text buffer may vary!
954    *
955    * Technical issues:
956    *   no API for ordering toplevels yet, other than knowing which is ACTIVE.
957    *   not much implementation for the LAYER API yet, other than menus.
958    */
959   g_timer_stop (timer);
960   fprintf (stderr, "elapsed time = %f s\n", g_timer_elapsed (timer, NULL));
961   
962   return string;
963 }
964
965 void
966 report_screen_review_line (const AccessibleEvent *event, void *user_data)
967 {
968   static Display *display = NULL;
969   int x, y, win_x, win_y;
970   Window root_return, child_return;
971   unsigned int mask_return;
972   
973   if (!display) display = XOpenDisplay (getenv ("DISPLAY"));
974   /*
975    *  we would prefer to get the x,y info in the above event.
976    *  At the moment we don't get detail params for "toolkit" events,
977    *  so for testing purposes we use XQueryPointer.  Actual apps
978    *  probably shouldn't do this.
979    */
980   XQueryPointer (display,
981                  DefaultRootWindow (display),
982                  &root_return, &child_return,
983                  &x, &y,
984                  &win_x, &win_y,
985                  &mask_return);
986
987   fprintf (stderr, "screen review event %s at %d, %d\n", event->type,
988            x, y);
989   fprintf (stderr, "[%s]\n", 
990            get_screen_review_line_at (x, y));
991 }
992
993 void
994 test_exit ()
995 {
996   SPI_deregisterGlobalEventListenerAll (mouseclick_listener);
997   AccessibleEventListener_unref (mouseclick_listener);
998 }