]> Dogcows Code - chaz/openbox/blob - openbox/event.c
pass the Client for frame_context cuz the client might be NULL
[chaz/openbox] / openbox / event.c
1 #include "openbox.h"
2 #include "client.h"
3 #include "xerror.h"
4 #include "prop.h"
5 #include "config.h"
6 #include "screen.h"
7 #include "frame.h"
8 #include "framerender.h"
9 #include "focus.h"
10 #include "stacking.h"
11 #include "extensions.h"
12 #include "timer.h"
13 #include "dispatch.h"
14
15 #include <X11/Xlib.h>
16 #include <X11/keysym.h>
17 #include <X11/Xatom.h>
18 #ifdef HAVE_SYS_SELECT_H
19 # include <sys/select.h>
20 #endif
21
22 static void event_process(XEvent *e);
23 static void event_handle_root(XEvent *e);
24 static void event_handle_client(Client *c, XEvent *e);
25
26 Time event_lasttime = 0;
27
28 /*! The value of the mask for the NumLock modifier */
29 unsigned int NumLockMask;
30 /*! The value of the mask for the ScrollLock modifier */
31 unsigned int ScrollLockMask;
32 /*! The key codes for the modifier keys */
33 static XModifierKeymap *modmap;
34 /*! Table of the constant modifier masks */
35 static const int mask_table[] = {
36 ShiftMask, LockMask, ControlMask, Mod1Mask,
37 Mod2Mask, Mod3Mask, Mod4Mask, Mod5Mask
38 };
39 static int mask_table_size;
40
41 void event_startup()
42 {
43 mask_table_size = sizeof(mask_table) / sizeof(mask_table[0]);
44
45 /* get lock masks that are defined by the display (not constant) */
46 modmap = XGetModifierMapping(ob_display);
47 g_assert(modmap);
48 if (modmap && modmap->max_keypermod > 0) {
49 size_t cnt;
50 const size_t size = mask_table_size * modmap->max_keypermod;
51 /* get the values of the keyboard lock modifiers
52 Note: Caps lock is not retrieved the same way as Scroll and Num
53 lock since it doesn't need to be. */
54 const KeyCode num_lock = XKeysymToKeycode(ob_display, XK_Num_Lock);
55 const KeyCode scroll_lock = XKeysymToKeycode(ob_display,
56 XK_Scroll_Lock);
57
58 for (cnt = 0; cnt < size; ++cnt) {
59 if (! modmap->modifiermap[cnt]) continue;
60
61 if (num_lock == modmap->modifiermap[cnt])
62 NumLockMask = mask_table[cnt / modmap->max_keypermod];
63 if (scroll_lock == modmap->modifiermap[cnt])
64 ScrollLockMask = mask_table[cnt / modmap->max_keypermod];
65 }
66 }
67 }
68
69 void event_shutdown()
70 {
71 XFreeModifiermap(modmap);
72 }
73
74 void event_loop()
75 {
76 fd_set selset;
77 XEvent e;
78 int x_fd;
79 struct timeval *wait;
80
81 while (TRUE) {
82 /*
83 There are slightly different event retrieval semantics here for
84 local (or high bandwidth) versus remote (or low bandwidth)
85 connections to the display/Xserver.
86 */
87 if (ob_remote) {
88 if (!XPending(ob_display))
89 break;
90 } else {
91 /*
92 This XSync allows for far more compression of events, which
93 makes things like Motion events perform far far better. Since
94 it also means network traffic for every event instead of every
95 X events (where X is the number retrieved at a time), it
96 probably should not be used for setups where Openbox is
97 running on a remote/low bandwidth display/Xserver.
98 */
99 XSync(ob_display, FALSE);
100 if (!XEventsQueued(ob_display, QueuedAlready))
101 break;
102 }
103 XNextEvent(ob_display, &e);
104
105 event_process(&e);
106 }
107
108 timer_dispatch((GTimeVal**)&wait);
109 x_fd = ConnectionNumber(ob_display);
110 FD_ZERO(&selset);
111 FD_SET(x_fd, &selset);
112 select(x_fd + 1, &selset, NULL, NULL, wait);
113 }
114
115 void event_process(XEvent *e)
116 {
117 XEvent ce;
118 KeyCode *kp;
119 Window window;
120 int i, k;
121 Client *client;
122
123 /* pick a window */
124 switch (e->type) {
125 case MapRequest:
126 window = e->xmap.window;
127 break;
128 case UnmapNotify:
129 window = e->xunmap.window;
130 break;
131 case DestroyNotify:
132 window = e->xdestroywindow.window;
133 break;
134 case ConfigureRequest:
135 window = e->xconfigurerequest.window;
136 break;
137 default:
138 #ifdef XKB
139 if (extensions_xkb && e->type == extensions_xkb_event_basep) {
140 switch (((XkbAnyEvent*)&e)->xkb_type) {
141 case XkbBellNotify:
142 window = ((XkbBellNotifyEvent*)&e)->window;
143 default:
144 window = None;
145 }
146 } else
147 #endif
148 window = e->xany.window;
149 }
150
151 client = g_hash_table_lookup(client_map, &window);
152
153 /* grab the lasttime and hack up the state */
154 switch (e->type) {
155 case ButtonPress:
156 case ButtonRelease:
157 event_lasttime = e->xbutton.time;
158 e->xbutton.state &= ~(LockMask | NumLockMask | ScrollLockMask);
159 /* kill off the Button1Mask etc, only want the modifiers */
160 e->xbutton.state &= (ControlMask | ShiftMask | Mod1Mask |
161 Mod2Mask | Mod3Mask | Mod4Mask | Mod5Mask);
162 break;
163 case KeyPress:
164 event_lasttime = e->xkey.time;
165 e->xkey.state &= ~(LockMask | NumLockMask | ScrollLockMask);
166 /* kill off the Button1Mask etc, only want the modifiers */
167 e->xkey.state &= (ControlMask | ShiftMask | Mod1Mask |
168 Mod2Mask | Mod3Mask | Mod4Mask | Mod5Mask);
169 /* add to the state the mask of the modifier being pressed, if it is
170 a modifier key being pressed (this is a little ugly..) */
171 /* I'm commenting this out cuz i don't want "C-Control_L" being returned. */
172 /* kp = modmap->modifiermap;*/
173 /* for (i = 0; i < mask_table_size; ++i) {*/
174 /* for (k = 0; k < modmap->max_keypermod; ++k) {*/
175 /* if (*kp == e->xkey.keycode) {*/ /* found the keycode */
176 /* add the mask for it */
177 /* e->xkey.state |= mask_table[i];*/
178 /* cause the first loop to break; */
179 /* i = mask_table_size;*/
180 /* break;*/ /* get outta here! */
181 /* }*/
182 /* ++kp;*/
183 /* }*/
184 /* }*/
185
186 break;
187 case KeyRelease:
188 event_lasttime = e->xkey.time;
189 e->xkey.state &= ~(LockMask | NumLockMask | ScrollLockMask);
190 /* kill off the Button1Mask etc, only want the modifiers */
191 e->xkey.state &= (ControlMask | ShiftMask | Mod1Mask |
192 Mod2Mask | Mod3Mask | Mod4Mask | Mod5Mask);
193 /* remove from the state the mask of the modifier being released, if
194 it is a modifier key being released (this is a little ugly..) */
195 kp = modmap->modifiermap;
196 for (i = 0; i < mask_table_size; ++i) {
197 for (k = 0; k < modmap->max_keypermod; ++k) {
198 if (*kp == e->xkey.keycode) { /* found the keycode */
199 /* remove the mask for it */
200 e->xkey.state &= ~mask_table[i];
201 /* cause the first loop to break; */
202 i = mask_table_size;
203 break; /* get outta here! */
204 }
205 ++kp;
206 }
207 }
208 break;
209 case MotionNotify:
210 event_lasttime = e->xmotion.time;
211 e->xmotion.state &= ~(LockMask | NumLockMask | ScrollLockMask);
212 /* kill off the Button1Mask etc, only want the modifiers */
213 e->xmotion.state &= (ControlMask | ShiftMask | Mod1Mask |
214 Mod2Mask | Mod3Mask | Mod4Mask | Mod5Mask);
215 /* compress events */
216 while (XCheckTypedWindowEvent(ob_display, window, e->type, &ce)) {
217 e->xmotion.x_root = ce.xmotion.x_root;
218 e->xmotion.y_root = ce.xmotion.y_root;
219 }
220 break;
221 case PropertyNotify:
222 event_lasttime = e->xproperty.time;
223 break;
224 case FocusIn:
225 #ifdef DEBUG_FOCUS
226 g_message("FocusIn on %lx mode %d detail %d", window,
227 e->xfocus.mode, e->xfocus.detail);
228 #endif
229 /* NotifyAncestor is not ignored in FocusIn like it is in FocusOut
230 because of RevertToPointerRoot. If the focus ends up reverting to
231 pointer root on a workspace change, then the FocusIn event that we
232 want will be of type NotifyAncestor. This situation does not occur
233 for FocusOut, so it is safely ignored there.
234 */
235 if (e->xfocus.detail == NotifyInferior ||
236 e->xfocus.detail > NotifyNonlinearVirtual ||
237 client == NULL) {
238 /* says a client was not found for the event (or a valid FocusIn
239 event was not found.
240 */
241 e->xfocus.window = None;
242 return;
243 }
244
245 #ifdef DEBUG_FOCUS
246 g_message("FocusIn on %lx", window);
247 #endif
248 break;
249 case FocusOut:
250 #ifdef DEBUG_FOCUS
251 g_message("FocusOut on %lx mode %d detail %d", window,
252 e->xfocus.mode, e->xfocus.detail);
253 #endif
254 if (e->xfocus.mode == NotifyGrab ||
255 e->xfocus.detail == NotifyInferior ||
256 e->xfocus.detail == NotifyAncestor ||
257 e->xfocus.detail > NotifyNonlinearVirtual) return;
258
259 #ifdef DEBUG_FOCUS
260 g_message("FocusOut on %lx", window);
261 #endif
262 /* Try process a FocusIn first, and if a legit one isn't found, then
263 do the fallback shiznit. */
264 {
265 XEvent fi, fo;
266 gboolean isfo = FALSE;
267
268 if (XCheckTypedEvent(ob_display, FocusIn, &fi)) {
269 event_process(&fi);
270
271 /* when we have gotten a fi/fo pair, then see if there are any
272 more fo's coming. if there are, then don't fallback just yet
273 */
274 if ((isfo = XCheckTypedEvent(ob_display, FocusOut, &fo)))
275 XPutBackEvent(ob_display, &fo);
276
277 /* secret magic way of event_process telling us that no client
278 was found for the FocusIn event. ^_^ */
279 if (!isfo && fi.xfocus.window == None)
280 focus_fallback(Fallback_NoFocus);
281 if (fi.xfocus.window == e->xfocus.window)
282 return;
283 } else
284 focus_fallback(Fallback_NoFocus);
285 }
286 break;
287 case EnterNotify:
288 case LeaveNotify:
289 event_lasttime = e->xcrossing.time;
290 /* NotifyUngrab occurs when a mouse button is released and the event is
291 caused, like when lowering a window */
292 if (e->xcrossing.mode == NotifyGrab ||
293 e->xcrossing.detail == NotifyInferior)
294 return;
295 break;
296 default:
297 event_lasttime = CurrentTime;
298 break;
299 }
300
301 /* deal with it in the kernel */
302 if (client)
303 event_handle_client(client, e);
304 else if (window == ob_root)
305 event_handle_root(e);
306 else if (e->type == MapRequest)
307 client_manage(window);
308 else if (e->type == ConfigureRequest) {
309 /* unhandled configure requests must be used to configure the
310 window directly */
311 XWindowChanges xwc;
312
313 xwc.x = e->xconfigurerequest.x;
314 xwc.y = e->xconfigurerequest.y;
315 xwc.width = e->xconfigurerequest.width;
316 xwc.height = e->xconfigurerequest.height;
317 xwc.border_width = e->xconfigurerequest.border_width;
318 xwc.sibling = e->xconfigurerequest.above;
319 xwc.stack_mode = e->xconfigurerequest.detail;
320
321 /* we are not to be held responsible if someone sends us an
322 invalid request! */
323 xerror_set_ignore(TRUE);
324 XConfigureWindow(ob_display, window,
325 e->xconfigurerequest.value_mask, &xwc);
326 xerror_set_ignore(FALSE);
327 }
328
329 /* dispatch the event to registered handlers */
330 dispatch_x(e, client);
331 }
332
333 static void event_handle_root(XEvent *e)
334 {
335 Atom msgtype;
336
337 switch(e->type) {
338 case ClientMessage:
339 if (e->xclient.format != 32) break;
340
341 msgtype = e->xclient.message_type;
342 if (msgtype == prop_atoms.net_current_desktop) {
343 unsigned int d = e->xclient.data.l[0];
344 if (d < screen_num_desktops)
345 screen_set_desktop(d);
346 } else if (msgtype == prop_atoms.net_number_of_desktops) {
347 unsigned int d = e->xclient.data.l[0];
348 if (d > 0)
349 screen_set_num_desktops(d);
350 } else if (msgtype == prop_atoms.net_showing_desktop) {
351 screen_show_desktop(e->xclient.data.l[0] != 0);
352 }
353 break;
354 case PropertyNotify:
355 if (e->xproperty.atom == prop_atoms.net_desktop_names)
356 screen_update_desktop_names();
357 else if (e->xproperty.atom == prop_atoms.net_desktop_layout)
358 screen_update_layout();
359 break;
360 }
361 }
362
363 static void event_handle_client(Client *client, XEvent *e)
364 {
365 XEvent ce;
366 Atom msgtype;
367 int i=0;
368
369 switch (e->type) {
370 case ButtonPress:
371 case ButtonRelease:
372 switch (frame_context(client, e->xbutton.window)) {
373 case Context_Maximize:
374 client->frame->max_press = (e->type == ButtonPress);
375 framerender_frame(client->frame);
376 break;
377 case Context_Close:
378 client->frame->close_press = (e->type == ButtonPress);
379 framerender_frame(client->frame);
380 break;
381 case Context_Iconify:
382 client->frame->iconify_press = (e->type == ButtonPress);
383 framerender_frame(client->frame);
384 break;
385 case Context_AllDesktops:
386 client->frame->desk_press = (e->type == ButtonPress);
387 framerender_frame(client->frame);
388 break;
389 case Context_Shade:
390 client->frame->shade_press = (e->type == ButtonPress);
391 framerender_frame(client->frame);
392 break;
393 default:
394 /* nothing changes with clicks for any other contexts */
395 break;
396 }
397 break;
398 case FocusIn:
399 focus_set_client(client);
400 case FocusOut:
401 #ifdef DEBUG_FOCUS
402 g_message("Focus%s on client for %lx", (e->type==FocusIn?"In":"Out"),
403 client->window);
404 #endif
405 /* focus state can affect the stacking layer */
406 client_calc_layer(client);
407 frame_adjust_focus(client->frame);
408 break;
409 case EnterNotify:
410 if (client_normal(client)) {
411 if (ob_state == State_Starting) {
412 /* move it to the top of the focus order */
413 guint desktop = client->desktop;
414 if (desktop == DESKTOP_ALL) desktop = screen_desktop;
415 focus_order[desktop] = g_list_remove(focus_order[desktop],
416 client);
417 focus_order[desktop] = g_list_prepend(focus_order[desktop],
418 client);
419 } else if (config_focus_follow) {
420 #ifdef DEBUG_FOCUS
421 g_message("EnterNotify on %lx, focusing window",
422 client->window);
423 #endif
424 client_focus(client);
425 }
426 }
427 break;
428 case ConfigureRequest:
429 /* compress these */
430 while (XCheckTypedWindowEvent(ob_display, client->window,
431 ConfigureRequest, &ce)) {
432 ++i;
433 /* XXX if this causes bad things.. we can compress config req's
434 with the same mask. */
435 e->xconfigurerequest.value_mask |=
436 ce.xconfigurerequest.value_mask;
437 if (ce.xconfigurerequest.value_mask & CWX)
438 e->xconfigurerequest.x = ce.xconfigurerequest.x;
439 if (ce.xconfigurerequest.value_mask & CWY)
440 e->xconfigurerequest.y = ce.xconfigurerequest.y;
441 if (ce.xconfigurerequest.value_mask & CWWidth)
442 e->xconfigurerequest.width = ce.xconfigurerequest.width;
443 if (ce.xconfigurerequest.value_mask & CWHeight)
444 e->xconfigurerequest.height = ce.xconfigurerequest.height;
445 if (ce.xconfigurerequest.value_mask & CWBorderWidth)
446 e->xconfigurerequest.border_width =
447 ce.xconfigurerequest.border_width;
448 if (ce.xconfigurerequest.value_mask & CWStackMode)
449 e->xconfigurerequest.detail = ce.xconfigurerequest.detail;
450 }
451
452 /* if we are iconic (or shaded (fvwm does this)) ignore the event */
453 if (client->iconic || client->shaded) return;
454
455 if (e->xconfigurerequest.value_mask & CWBorderWidth)
456 client->border_width = e->xconfigurerequest.border_width;
457
458 /* resize, then move, as specified in the EWMH section 7.7 */
459 if (e->xconfigurerequest.value_mask & (CWWidth | CWHeight |
460 CWX | CWY)) {
461 int x, y, w, h;
462 Corner corner;
463
464 x = (e->xconfigurerequest.value_mask & CWX) ?
465 e->xconfigurerequest.x : client->area.x;
466 y = (e->xconfigurerequest.value_mask & CWY) ?
467 e->xconfigurerequest.y : client->area.y;
468 w = (e->xconfigurerequest.value_mask & CWWidth) ?
469 e->xconfigurerequest.width : client->area.width;
470 h = (e->xconfigurerequest.value_mask & CWHeight) ?
471 e->xconfigurerequest.height : client->area.height;
472
473 switch (client->gravity) {
474 case NorthEastGravity:
475 case EastGravity:
476 corner = Corner_TopRight;
477 break;
478 case SouthWestGravity:
479 case SouthGravity:
480 corner = Corner_BottomLeft;
481 break;
482 case SouthEastGravity:
483 corner = Corner_BottomRight;
484 break;
485 default: /* NorthWest, Static, etc */
486 corner = Corner_TopLeft;
487 }
488
489 client_configure(client, corner, x, y, w, h, FALSE, FALSE);
490 }
491
492 if (e->xconfigurerequest.value_mask & CWStackMode) {
493 switch (e->xconfigurerequest.detail) {
494 case Below:
495 case BottomIf:
496 stacking_lower(client);
497 break;
498
499 case Above:
500 case TopIf:
501 default:
502 stacking_raise(client);
503 break;
504 }
505 }
506 break;
507 case UnmapNotify:
508 if (client->ignore_unmaps) {
509 client->ignore_unmaps--;
510 break;
511 }
512 client_unmanage(client);
513 break;
514 case DestroyNotify:
515 client_unmanage(client);
516 break;
517 case ReparentNotify:
518 /* this is when the client is first taken captive in the frame */
519 if (e->xreparent.parent == client->frame->plate) break;
520
521 /*
522 This event is quite rare and is usually handled in unmapHandler.
523 However, if the window is unmapped when the reparent event occurs,
524 the window manager never sees it because an unmap event is not sent
525 to an already unmapped window.
526 */
527
528 /* we don't want the reparent event, put it back on the stack for the
529 X server to deal with after we unmanage the window */
530 XPutBackEvent(ob_display, e);
531
532 client_unmanage(client);
533 break;
534 case MapRequest:
535 g_message("MapRequest for 0x%lx", client->window);
536 if (!client->iconic) break; /* this normally doesn't happen, but if it
537 does, we don't want it! */
538 if (screen_showing_desktop)
539 screen_show_desktop(FALSE);
540 client_iconify(client, FALSE, TRUE);
541 if (!client->frame->visible)
542 /* if its not visible still, then don't mess with it */
543 break;
544 if (client->shaded)
545 client_shade(client, FALSE);
546 client_focus(client);
547 stacking_raise(client);
548 break;
549 case ClientMessage:
550 /* validate cuz we query stuff off the client here */
551 if (!client_validate(client)) break;
552
553 if (e->xclient.format != 32) return;
554
555 msgtype = e->xclient.message_type;
556 if (msgtype == prop_atoms.wm_change_state) {
557 /* compress changes into a single change */
558 while (XCheckTypedWindowEvent(ob_display, e->type,
559 client->window, &ce)) {
560 /* XXX: it would be nice to compress ALL messages of a
561 type, not just messages in a row without other
562 message types between. */
563 if (ce.xclient.message_type != msgtype) {
564 XPutBackEvent(ob_display, &ce);
565 break;
566 }
567 e->xclient = ce.xclient;
568 }
569 client_set_wm_state(client, e->xclient.data.l[0]);
570 } else if (msgtype == prop_atoms.net_wm_desktop) {
571 /* compress changes into a single change */
572 while (XCheckTypedWindowEvent(ob_display, e->type,
573 client->window, &ce)) {
574 /* XXX: it would be nice to compress ALL messages of a
575 type, not just messages in a row without other
576 message types between. */
577 if (ce.xclient.message_type != msgtype) {
578 XPutBackEvent(ob_display, &ce);
579 break;
580 }
581 e->xclient = ce.xclient;
582 }
583 if ((unsigned)e->xclient.data.l[0] < screen_num_desktops ||
584 (unsigned)e->xclient.data.l[0] == DESKTOP_ALL)
585 client_set_desktop(client, (unsigned)e->xclient.data.l[0],
586 FALSE);
587 } else if (msgtype == prop_atoms.net_wm_state) {
588 /* can't compress these */
589 g_message("net_wm_state %s %ld %ld for 0x%lx",
590 (e->xclient.data.l[0] == 0 ? "Remove" :
591 e->xclient.data.l[0] == 1 ? "Add" :
592 e->xclient.data.l[0] == 2 ? "Toggle" : "INVALID"),
593 e->xclient.data.l[1], e->xclient.data.l[2],
594 client->window);
595 client_set_state(client, e->xclient.data.l[0],
596 e->xclient.data.l[1], e->xclient.data.l[2]);
597 } else if (msgtype == prop_atoms.net_close_window) {
598 g_message("net_close_window for 0x%lx", client->window);
599 client_close(client);
600 } else if (msgtype == prop_atoms.net_active_window) {
601 g_message("net_active_window for 0x%lx", client->window);
602 if (screen_showing_desktop)
603 screen_show_desktop(FALSE);
604 if (client->iconic)
605 client_iconify(client, FALSE, TRUE);
606 else if (!client->frame->visible)
607 /* if its not visible for other reasons, then don't mess
608 with it */
609 break;
610 if (client->shaded)
611 client_shade(client, FALSE);
612 client_focus(client);
613 stacking_raise(client);
614 }
615 break;
616 case PropertyNotify:
617 /* validate cuz we query stuff off the client here */
618 if (!client_validate(client)) break;
619
620 /* compress changes to a single property into a single change */
621 while (XCheckTypedWindowEvent(ob_display, e->type,
622 client->window, &ce)) {
623 /* XXX: it would be nice to compress ALL changes to a property,
624 not just changes in a row without other props between. */
625 if (ce.xproperty.atom != e->xproperty.atom) {
626 XPutBackEvent(ob_display, &ce);
627 break;
628 }
629 }
630
631 msgtype = e->xproperty.atom;
632 if (msgtype == XA_WM_NORMAL_HINTS) {
633 client_update_normal_hints(client);
634 /* normal hints can make a window non-resizable */
635 client_setup_decor_and_functions(client);
636 }
637 else if (msgtype == XA_WM_HINTS)
638 client_update_wmhints(client);
639 else if (msgtype == XA_WM_TRANSIENT_FOR) {
640 client_update_transient_for(client);
641 client_get_type(client);
642 /* type may have changed, so update the layer */
643 client_calc_layer(client);
644 client_setup_decor_and_functions(client);
645 }
646 else if (msgtype == prop_atoms.net_wm_name ||
647 msgtype == prop_atoms.wm_name)
648 client_update_title(client);
649 else if (msgtype == prop_atoms.net_wm_icon_name ||
650 msgtype == prop_atoms.wm_icon_name)
651 client_update_icon_title(client);
652 else if (msgtype == prop_atoms.wm_class)
653 client_update_class(client);
654 else if (msgtype == prop_atoms.wm_protocols) {
655 client_update_protocols(client);
656 client_setup_decor_and_functions(client);
657 }
658 else if (msgtype == prop_atoms.net_wm_strut)
659 client_update_strut(client);
660 else if (msgtype == prop_atoms.net_wm_icon)
661 client_update_icons(client);
662 else if (msgtype == prop_atoms.kwm_win_icon)
663 client_update_kwm_icon(client);
664 default:
665 ;
666 #ifdef SHAPE
667 if (extensions_shape && e->type == extensions_shape_event_basep) {
668 client->shaped = ((XShapeEvent*)e)->shaped;
669 frame_adjust_shape(client->frame);
670 }
671 #endif
672 }
673 }
This page took 0.08118 seconds and 4 git commands to generate.