0f1d8bb5a41cab782780ad0f61bcfef127eb9223
[profile/ivi/ecore.git] / src / lib / ecore_x / xlib / ecore_x_dnd.c
1 #ifdef HAVE_CONFIG_H
2 # include <config.h>
3 #endif /* ifdef HAVE_CONFIG_H */
4
5 #include <stdlib.h>
6 #include <string.h>
7
8 #include "Ecore.h"
9 #include "ecore_x_private.h"
10 #include "Ecore_X.h"
11 #include "Ecore_X_Atoms.h"
12
13 EAPI int ECORE_X_EVENT_XDND_ENTER = 0;
14 EAPI int ECORE_X_EVENT_XDND_POSITION = 0;
15 EAPI int ECORE_X_EVENT_XDND_STATUS = 0;
16 EAPI int ECORE_X_EVENT_XDND_LEAVE = 0;
17 EAPI int ECORE_X_EVENT_XDND_DROP = 0;
18 EAPI int ECORE_X_EVENT_XDND_FINISHED = 0;
19
20 static Ecore_X_DND_Source *_source = NULL;
21 static Ecore_X_DND_Target *_target = NULL;
22 static int _ecore_x_dnd_init_count = 0;
23
24 typedef struct _Version_Cache_Item
25 {
26    Ecore_X_Window win;
27    int            ver;
28 } Version_Cache_Item;
29 static Version_Cache_Item *_version_cache = NULL;
30 static int _version_cache_num = 0, _version_cache_alloc = 0;
31 static void (*_posupdatecb)(void *, Ecore_X_Xdnd_Position *);
32 static void *_posupdatedata;
33
34 void
35 _ecore_x_dnd_init(void)
36 {
37    if (!_ecore_x_dnd_init_count)
38      {
39         _source = calloc(1, sizeof(Ecore_X_DND_Source));
40         if (!_source) return;
41         _source->version = ECORE_X_DND_VERSION;
42         _source->win = None;
43         _source->dest = None;
44         _source->state = ECORE_X_DND_SOURCE_IDLE;
45         _source->prev.window = 0;
46
47         _target = calloc(1, sizeof(Ecore_X_DND_Target));
48         if (!_target)
49           {
50              free(_source);
51              _source = NULL;
52              return;
53           }
54         _target->win = None;
55         _target->source = None;
56         _target->state = ECORE_X_DND_TARGET_IDLE;
57
58         ECORE_X_EVENT_XDND_ENTER = ecore_event_type_new();
59         ECORE_X_EVENT_XDND_POSITION = ecore_event_type_new();
60         ECORE_X_EVENT_XDND_STATUS = ecore_event_type_new();
61         ECORE_X_EVENT_XDND_LEAVE = ecore_event_type_new();
62         ECORE_X_EVENT_XDND_DROP = ecore_event_type_new();
63         ECORE_X_EVENT_XDND_FINISHED = ecore_event_type_new();
64      }
65
66    _ecore_x_dnd_init_count++;
67 } /* _ecore_x_dnd_init */
68
69 void
70 _ecore_x_dnd_shutdown(void)
71 {
72    _ecore_x_dnd_init_count--;
73    if (_ecore_x_dnd_init_count > 0)
74       return;
75
76    if (_source)
77       free(_source);
78
79    _source = NULL;
80
81    if (_target)
82       free(_target);
83
84    _target = NULL;
85
86    _ecore_x_dnd_init_count = 0;
87 } /* _ecore_x_dnd_shutdown */
88
89 static Eina_Bool
90 _ecore_x_dnd_converter_copy(char *target  __UNUSED__,
91                             void         *data,
92                             int           size,
93                             void        **data_ret,
94                             int          *size_ret,
95                             Ecore_X_Atom *tprop __UNUSED__,
96                             int          *count __UNUSED__)
97 {
98    XTextProperty text_prop;
99    char *mystr;
100    XICCEncodingStyle style = XTextStyle;
101
102    if (!data || !size)
103       return EINA_FALSE;
104
105    mystr = calloc(1, size + 1);
106    if (!mystr)
107       return EINA_FALSE;
108
109    memcpy(mystr, data, size);
110
111    if (XmbTextListToTextProperty(_ecore_x_disp, &mystr, 1, style,
112                                  &text_prop) == Success)
113      {
114         int bufsize = strlen((char *)text_prop.value) + 1;
115         *data_ret = malloc(bufsize);
116         if (!*data_ret)
117           {
118              free(mystr);
119              return EINA_FALSE;
120           }
121         memcpy(*data_ret, text_prop.value, bufsize);
122         *size_ret = bufsize;
123         XFree(text_prop.value);
124         free(mystr);
125         return EINA_TRUE;
126      }
127    else
128      {
129         free(mystr);
130         return EINA_FALSE;
131      }
132 } /* _ecore_x_dnd_converter_copy */
133
134 EAPI void
135 ecore_x_dnd_aware_set(Ecore_X_Window win, Eina_Bool on)
136 {
137    Ecore_X_Atom prop_data = ECORE_X_DND_VERSION;
138
139    LOGFN(__FILE__, __LINE__, __FUNCTION__);
140    if (on)
141       ecore_x_window_prop_property_set(win, ECORE_X_ATOM_XDND_AWARE,
142                                        XA_ATOM, 32, &prop_data, 1);
143    else
144       ecore_x_window_prop_property_del(win, ECORE_X_ATOM_XDND_AWARE);
145 } /* ecore_x_dnd_aware_set */
146
147 EAPI int
148 ecore_x_dnd_version_get(Ecore_X_Window win)
149 {
150    unsigned char *prop_data;
151    int num;
152    Version_Cache_Item *t;
153
154    LOGFN(__FILE__, __LINE__, __FUNCTION__);
155    // this looks hacky - and it is, but we need a way of caching info about
156    // a window while dragging, because we literally query this every mouse
157    // move and going to and from x multiple times per move is EXPENSIVE
158    // and slows things down, puts lots of load on x etc.
159    if (_source->state == ECORE_X_DND_SOURCE_DRAGGING)
160       if (_version_cache)
161         {
162            int i;
163
164            for (i = 0; i < _version_cache_num; i++)
165              {
166                 if (_version_cache[i].win == win)
167                    return _version_cache[i].ver;
168              }
169         }
170
171    if (ecore_x_window_prop_property_get(win, ECORE_X_ATOM_XDND_AWARE,
172                                         XA_ATOM, 32, &prop_data, &num))
173      {
174         int version = (int)*prop_data;
175         free(prop_data);
176         if (_source->state == ECORE_X_DND_SOURCE_DRAGGING)
177           {
178              _version_cache_num++;
179              if (_version_cache_num > _version_cache_alloc)
180                 _version_cache_alloc += 16;
181
182              t = realloc(_version_cache,
183                          _version_cache_alloc *
184                          sizeof(Version_Cache_Item));
185              if (!t) return 0;
186              _version_cache = t;
187              _version_cache[_version_cache_num - 1].win = win;
188              _version_cache[_version_cache_num - 1].ver = version;
189           }
190
191         return version;
192      }
193
194    if (_source->state == ECORE_X_DND_SOURCE_DRAGGING)
195      {
196         _version_cache_num++;
197         if (_version_cache_num > _version_cache_alloc)
198            _version_cache_alloc += 16;
199
200         t = realloc(_version_cache, _version_cache_alloc *
201                     sizeof(Version_Cache_Item));
202         if (!t) return 0;
203         _version_cache = t;
204         _version_cache[_version_cache_num - 1].win = win;
205         _version_cache[_version_cache_num - 1].ver = 0;
206      }
207
208    return 0;
209 } /* ecore_x_dnd_version_get */
210
211 EAPI Eina_Bool
212 ecore_x_dnd_type_isset(Ecore_X_Window win, const char *type)
213 {
214    int num, i, ret = EINA_FALSE;
215    unsigned char *data;
216    Ecore_X_Atom *atoms, atom;
217
218    LOGFN(__FILE__, __LINE__, __FUNCTION__);
219    if (!ecore_x_window_prop_property_get(win, ECORE_X_ATOM_XDND_TYPE_LIST,
220                                          XA_ATOM, 32, &data, &num))
221       return ret;
222
223    atom = ecore_x_atom_get(type);
224    atoms = (Ecore_X_Atom *)data;
225
226    for (i = 0; i < num; ++i)
227      {
228         if (atom == atoms[i])
229           {
230              ret = EINA_TRUE;
231              break;
232           }
233      }
234
235    XFree(data);
236    return ret;
237 } /* ecore_x_dnd_type_isset */
238
239 EAPI void
240 ecore_x_dnd_type_set(Ecore_X_Window win, const char *type, Eina_Bool on)
241 {
242    Ecore_X_Atom atom;
243    Ecore_X_Atom *oldset = NULL, *newset = NULL;
244    int i, j = 0, num = 0;
245    unsigned char *data = NULL;
246    unsigned char *old_data = NULL;
247
248    LOGFN(__FILE__, __LINE__, __FUNCTION__);
249    atom = ecore_x_atom_get(type);
250    ecore_x_window_prop_property_get(win, ECORE_X_ATOM_XDND_TYPE_LIST,
251                                     XA_ATOM, 32, &old_data, &num);
252    oldset = (Ecore_X_Atom *)old_data;
253
254    LOGFN(__FILE__, __LINE__, __FUNCTION__);
255    if (on)
256      {
257         if (ecore_x_dnd_type_isset(win, type))
258           {
259              XFree(old_data);
260              return;
261           }
262
263         newset = calloc(num + 1, sizeof(Ecore_X_Atom));
264         if (!newset)
265            return;
266
267         data = (unsigned char *)newset;
268
269         for (i = 0; i < num; i++)
270            newset[i + 1] = oldset[i];
271         /* prepend the new type */
272         newset[0] = atom;
273
274         ecore_x_window_prop_property_set(win, ECORE_X_ATOM_XDND_TYPE_LIST,
275                                          XA_ATOM, 32, data, num + 1);
276      }
277    else
278      {
279         if (!ecore_x_dnd_type_isset(win, type))
280           {
281              XFree(old_data);
282              return;
283           }
284
285         newset = calloc(num - 1, sizeof(Ecore_X_Atom));
286         if (!newset)
287           {
288              XFree(old_data);
289              return;
290           }
291
292         data = (unsigned char *)newset;
293         for (i = 0; i < num; i++)
294            if (oldset[i] != atom)
295               newset[j++] = oldset[i];
296
297         ecore_x_window_prop_property_set(win, ECORE_X_ATOM_XDND_TYPE_LIST,
298                                          XA_ATOM, 32, data, num - 1);
299      }
300
301    XFree(oldset);
302    free(newset);
303 } /* ecore_x_dnd_type_set */
304
305 EAPI void
306 ecore_x_dnd_types_set(Ecore_X_Window win,
307                       const char   **types,
308                       unsigned int   num_types)
309 {
310    Ecore_X_Atom *newset = NULL;
311    unsigned int i;
312    unsigned char *data = NULL;
313
314    LOGFN(__FILE__, __LINE__, __FUNCTION__);
315    if (!num_types)
316       ecore_x_window_prop_property_del(win, ECORE_X_ATOM_XDND_TYPE_LIST);
317    else
318      {
319         newset = calloc(num_types, sizeof(Ecore_X_Atom));
320         if (!newset)
321            return;
322
323         data = (unsigned char *)newset;
324         for (i = 0; i < num_types; i++)
325           {
326              newset[i] = ecore_x_atom_get(types[i]);
327              ecore_x_selection_converter_atom_add(newset[i],
328                                                   _ecore_x_dnd_converter_copy);
329           }
330         ecore_x_window_prop_property_set(win, ECORE_X_ATOM_XDND_TYPE_LIST,
331                                          XA_ATOM, 32, data, num_types);
332         free(newset);
333      }
334 } /* ecore_x_dnd_types_set */
335
336 EAPI void
337 ecore_x_dnd_actions_set(Ecore_X_Window win,
338                         Ecore_X_Atom  *actions,
339                         unsigned int   num_actions)
340 {
341    unsigned int i;
342    unsigned char *data = NULL;
343
344    LOGFN(__FILE__, __LINE__, __FUNCTION__);
345    if (!num_actions)
346       ecore_x_window_prop_property_del(win, ECORE_X_ATOM_XDND_ACTION_LIST);
347    else
348      {
349         data = (unsigned char *)actions;
350         for (i = 0; i < num_actions; i++)
351           {
352              ecore_x_selection_converter_atom_add(actions[i],
353                                                   _ecore_x_dnd_converter_copy);
354           }
355         ecore_x_window_prop_property_set(win, ECORE_X_ATOM_XDND_ACTION_LIST,
356                                          XA_ATOM, 32, data, num_actions);
357      }
358 } /* ecore_x_dnd_actions_set */
359
360 /**
361  * The DND position update cb is called Ecore_X sends a DND position to a
362  * client.
363  *
364  * It essentially mirrors some of the data sent in the position message.
365  * Generally this cb should be set just before position update is called.
366  * Please note well you need to look after your own data pointer if someone
367  * trashes you position update cb set.
368  *
369  * It is considered good form to clear this when the dnd event finishes.
370  *
371  * @param cb Callback to updated each time ecore_x sends a position update.
372  * @param data User data.
373  */
374 EAPI void
375 ecore_x_dnd_callback_pos_update_set(
376                            void (*cb)(void *, Ecore_X_Xdnd_Position *data),
377                            const void *data)
378 {
379    _posupdatecb = cb;
380    _posupdatedata = (void *)data; /* Discard the const early */
381 }
382
383 Ecore_X_DND_Source *
384 _ecore_x_dnd_source_get(void)
385 {
386    return _source;
387 } /* _ecore_x_dnd_source_get */
388
389 Ecore_X_DND_Target *
390 _ecore_x_dnd_target_get(void)
391 {
392    return _target;
393 } /* _ecore_x_dnd_target_get */
394
395 EAPI Eina_Bool
396 ecore_x_dnd_begin(Ecore_X_Window source, unsigned char *data, int size)
397 {
398    LOGFN(__FILE__, __LINE__, __FUNCTION__);
399    if (!ecore_x_dnd_version_get(source))
400       return EINA_FALSE;
401
402    /* Take ownership of XdndSelection */
403    if (!ecore_x_selection_xdnd_set(source, data, size))
404       return EINA_FALSE;
405
406    if (_version_cache)
407      {
408         free(_version_cache);
409         _version_cache = NULL;
410         _version_cache_num = 0;
411         _version_cache_alloc = 0;
412      }
413
414    ecore_x_window_shadow_tree_flush();
415
416    _source->win = source;
417    ecore_x_window_ignore_set(_source->win, 1);
418    _source->state = ECORE_X_DND_SOURCE_DRAGGING;
419    _source->time = _ecore_x_event_last_time;
420    _source->prev.window = 0;
421
422    /* Default Accepted Action: move */
423    _source->action = ECORE_X_ATOM_XDND_ACTION_MOVE;
424    _source->accepted_action = None;
425    _source->dest = None;
426
427    return EINA_TRUE;
428 } /* ecore_x_dnd_begin */
429
430 EAPI Eina_Bool
431 ecore_x_dnd_drop(void)
432 {
433    XEvent xev;
434    int status = EINA_FALSE;
435
436    LOGFN(__FILE__, __LINE__, __FUNCTION__);
437    if (_source->dest)
438      {
439         xev.xany.type = ClientMessage;
440         xev.xany.display = _ecore_x_disp;
441         xev.xclient.format = 32;
442         xev.xclient.window = _source->dest;
443
444         if (_source->will_accept)
445           {
446              xev.xclient.message_type = ECORE_X_ATOM_XDND_DROP;
447              xev.xclient.data.l[0] = _source->win;
448              xev.xclient.data.l[1] = 0;
449              xev.xclient.data.l[2] = _source->time;
450              XSendEvent(_ecore_x_disp, _source->dest, False, 0, &xev);
451              _source->state = ECORE_X_DND_SOURCE_DROPPED;
452              status = EINA_TRUE;
453           }
454         else
455           {
456              xev.xclient.message_type = ECORE_X_ATOM_XDND_LEAVE;
457              xev.xclient.data.l[0] = _source->win;
458              xev.xclient.data.l[1] = 0;
459              XSendEvent(_ecore_x_disp, _source->dest, False, 0, &xev);
460              _source->state = ECORE_X_DND_SOURCE_IDLE;
461           }
462      }
463    else
464      {
465         /* Dropping on nothing */
466         ecore_x_selection_xdnd_clear();
467         _source->state = ECORE_X_DND_SOURCE_IDLE;
468      }
469
470    ecore_x_window_ignore_set(_source->win, 0);
471
472    _source->prev.window = 0;
473
474    return status;
475 } /* ecore_x_dnd_drop */
476
477 EAPI void
478 ecore_x_dnd_send_status(Eina_Bool         will_accept,
479                         Eina_Bool         suppress,
480                         Ecore_X_Rectangle rectangle,
481                         Ecore_X_Atom      action)
482 {
483    XEvent xev;
484
485    if (_target->state == ECORE_X_DND_TARGET_IDLE)
486       return;
487
488    LOGFN(__FILE__, __LINE__, __FUNCTION__);
489    memset(&xev, 0, sizeof(XEvent));
490
491    _target->will_accept = will_accept;
492
493    xev.xclient.type = ClientMessage;
494    xev.xclient.display = _ecore_x_disp;
495    xev.xclient.message_type = ECORE_X_ATOM_XDND_STATUS;
496    xev.xclient.format = 32;
497    xev.xclient.window = _target->source;
498
499    xev.xclient.data.l[0] = _target->win;
500    xev.xclient.data.l[1] = 0;
501    if (will_accept)
502       xev.xclient.data.l[1] |= 0x1UL;
503
504    if (!suppress)
505       xev.xclient.data.l[1] |= 0x2UL;
506
507    /* Set rectangle information */
508    xev.xclient.data.l[2] = rectangle.x;
509    xev.xclient.data.l[2] <<= 16;
510    xev.xclient.data.l[2] |= rectangle.y;
511    xev.xclient.data.l[3] = rectangle.width;
512    xev.xclient.data.l[3] <<= 16;
513    xev.xclient.data.l[3] |= rectangle.height;
514
515    if (will_accept)
516      {
517         xev.xclient.data.l[4] = action;
518         _target->accepted_action = action;
519      }
520    else
521      {
522         xev.xclient.data.l[4] = None;
523         _target->accepted_action = action;
524      }
525
526    XSendEvent(_ecore_x_disp, _target->source, False, 0, &xev);
527 } /* ecore_x_dnd_send_status */
528
529 EAPI void
530 ecore_x_dnd_send_finished(void)
531 {
532    XEvent xev;
533
534    if (_target->state == ECORE_X_DND_TARGET_IDLE)
535       return;
536
537    LOGFN(__FILE__, __LINE__, __FUNCTION__);
538    xev.xany.type = ClientMessage;
539    xev.xany.display = _ecore_x_disp;
540    xev.xclient.message_type = ECORE_X_ATOM_XDND_FINISHED;
541    xev.xclient.format = 32;
542    xev.xclient.window = _target->source;
543
544    xev.xclient.data.l[0] = _target->win;
545    xev.xclient.data.l[1] = 0;
546    xev.xclient.data.l[2] = 0;
547    if (_target->will_accept)
548      {
549         xev.xclient.data.l[1] |= 0x1UL;
550         xev.xclient.data.l[2] = _target->accepted_action;
551      }
552
553    XSendEvent(_ecore_x_disp, _target->source, False, 0, &xev);
554
555    _target->state = ECORE_X_DND_TARGET_IDLE;
556 } /* ecore_x_dnd_send_finished */
557
558 void
559 ecore_x_dnd_source_action_set(Ecore_X_Atom action)
560 {
561    _source->action = action;
562    if (_source->prev.window)
563       _ecore_x_dnd_drag(_source->prev.window, _source->prev.x, _source->prev.y);
564 } /* ecore_x_dnd_source_action_set */
565
566 Ecore_X_Atom
567 ecore_x_dnd_source_action_get(void)
568 {
569    return _source->action;
570 } /* ecore_x_dnd_source_action_get */
571
572 void
573 _ecore_x_dnd_drag(Ecore_X_Window root, int x, int y)
574 {
575    XEvent xev;
576    Ecore_X_Window win;
577    Ecore_X_Window *skip;
578    Ecore_X_Xdnd_Position pos;
579    int num;
580
581    if (_source->state != ECORE_X_DND_SOURCE_DRAGGING)
582       return;
583
584    /* Preinitialize XEvent struct */
585    memset(&xev, 0, sizeof(XEvent));
586    xev.xany.type = ClientMessage;
587    xev.xany.display = _ecore_x_disp;
588    xev.xclient.format = 32;
589
590    /* Attempt to find a DND-capable window under the cursor */
591    skip = ecore_x_window_ignore_list(&num);
592 // WARNING - this function is HEAVY. it goes to and from x a LOT walking the
593 // window tree - use the SHADOW version - makes a 1-off tree copy, then uses
594 // that instead.
595 //   win = ecore_x_window_at_xy_with_skip_get(x, y, skip, num);
596    win = ecore_x_window_shadow_tree_at_xy_with_skip_get(root, x, y, skip, num);
597
598 // NOTE: This now uses the shadow version to find parent windows
599 //   while ((win) && !(ecore_x_dnd_version_get(win)))
600 //     win = ecore_x_window_parent_get(win);
601    while ((win) && !(ecore_x_dnd_version_get(win)))
602       win = ecore_x_window_shadow_parent_get(root, win);
603
604    /* Send XdndLeave to current destination window if we have left it */
605    if ((_source->dest) && (win != _source->dest))
606      {
607         xev.xclient.window = _source->dest;
608         xev.xclient.message_type = ECORE_X_ATOM_XDND_LEAVE;
609         xev.xclient.data.l[0] = _source->win;
610         xev.xclient.data.l[1] = 0;
611
612         XSendEvent(_ecore_x_disp, _source->dest, False, 0, &xev);
613         _source->suppress = 0;
614      }
615
616    if (win)
617      {
618         int x1, x2, y1, y2;
619
620         _source->version = MIN(ECORE_X_DND_VERSION,
621                                ecore_x_dnd_version_get(win));
622         if (win != _source->dest)
623           {
624              int i;
625              unsigned char *data;
626              Ecore_X_Atom *types;
627
628              ecore_x_window_prop_property_get(_source->win,
629                                               ECORE_X_ATOM_XDND_TYPE_LIST,
630                                               XA_ATOM,
631                                               32,
632                                               &data,
633                                               &num);
634              types = (Ecore_X_Atom *)data;
635
636              /* Entered new window, send XdndEnter */
637              xev.xclient.window = win;
638              xev.xclient.message_type = ECORE_X_ATOM_XDND_ENTER;
639              xev.xclient.data.l[0] = _source->win;
640              xev.xclient.data.l[1] = 0;
641              if (num > 3)
642                 xev.xclient.data.l[1] |= 0x1UL;
643              else
644                 xev.xclient.data.l[1] &= 0xfffffffeUL;
645
646              xev.xclient.data.l[1] |= ((unsigned long)_source->version) << 24;
647
648              for (i = 2; i < 5; i++)
649                 xev.xclient.data.l[i] = 0;
650              for (i = 0; i < MIN(num, 3); ++i)
651                 xev.xclient.data.l[i + 2] = types[i];
652              XFree(data);
653              XSendEvent(_ecore_x_disp, win, False, 0, &xev);
654              _source->await_status = 0;
655              _source->will_accept = 0;
656           }
657
658         /* Determine if we're still in the rectangle from the last status */
659         x1 = _source->rectangle.x;
660         x2 = _source->rectangle.x + _source->rectangle.width;
661         y1 = _source->rectangle.y;
662         y2 = _source->rectangle.y + _source->rectangle.height;
663
664         if ((!_source->await_status) ||
665             (!_source->suppress) ||
666             ((x < x1) || (x > x2) || (y < y1) || (y > y2)))
667           {
668              xev.xclient.window = win;
669              xev.xclient.message_type = ECORE_X_ATOM_XDND_POSITION;
670              xev.xclient.data.l[0] = _source->win;
671              xev.xclient.data.l[1] = 0; /* Reserved */
672              xev.xclient.data.l[2] = ((x << 16) & 0xffff0000) | (y & 0xffff);
673              xev.xclient.data.l[3] = _source->time; /* Version 1 */
674              xev.xclient.data.l[4] = _source->action; /* Version 2, Needs to be pre-set */
675              XSendEvent(_ecore_x_disp, win, False, 0, &xev);
676
677              _source->await_status = 1;
678           }
679      }
680
681    if (_posupdatecb)
682      {
683         pos.position.x = x;
684         pos.position.y = y;
685         pos.win = win;
686         pos.prev = _source->dest;
687         _posupdatecb(_posupdatedata, &pos);
688      }
689
690    _source->prev.x = x;
691    _source->prev.y = y;
692    _source->prev.window = root;
693    _source->dest = win;
694
695
696 } /* _ecore_x_dnd_drag */
697
698
699
700 /* vim:set ts=8 sw=3 sts=3 expandtab cino=>5n-2f0^-2{2(0W1st0 :*/