Prompt should not always use the currently focused button as its result (Fix bug...
[dana/openbox.git] / openbox / prompt.c
1 /* -*- indent-tabs-mode: nil; tab-width: 4; c-basic-offset: 4; -*-
2
3    prompt.c for the Openbox window manager
4    Copyright (c) 2008        Dana Jansens
5
6    This program is free software; you can redistribute it and/or modify
7    it under the terms of the GNU General Public License as published by
8    the Free Software Foundation; either version 2 of the License, or
9    (at your option) any later version.
10
11    This program is distributed in the hope that it will be useful,
12    but WITHOUT ANY WARRANTY; without even the implied warranty of
13    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14    GNU General Public License for more details.
15
16    See the COPYING file for a copy of the GNU General Public License.
17 */
18
19 #include "prompt.h"
20 #include "openbox.h"
21 #include "screen.h"
22 #include "client.h"
23 #include "group.h"
24 #include "event.h"
25 #include "obt/display.h"
26 #include "obt/keyboard.h"
27 #include "obt/prop.h"
28 #include "gettext.h"
29
30 static GList *prompt_list = NULL;
31
32 /* we construct these */
33 static RrAppearance *prompt_a_bg;
34 static RrAppearance *prompt_a_button;
35 static RrAppearance *prompt_a_focus;
36 static RrAppearance *prompt_a_press;
37 /* we change the max width which would screw with others */
38 static RrAppearance *prompt_a_msg;
39
40 /* sizing stuff */
41 #define OUTSIDE_MARGIN 4
42 #define MSG_BUTTON_SEPARATION 4
43 #define BUTTON_SEPARATION 4
44 #define BUTTON_VMARGIN 4
45 #define BUTTON_HMARGIN 12
46 #define MAX_WIDTH 400
47
48 static void prompt_layout(ObPrompt *self);
49 static void render_all(ObPrompt *self);
50 static void render_button(ObPrompt *self, ObPromptElement *e);
51 static void prompt_resize(ObPrompt *self, gint w, gint h);
52 static void prompt_run_callback(ObPrompt *self, gint result);
53
54 void prompt_startup(gboolean reconfig)
55 {
56     /* note: this is not a copy, don't free it */
57     prompt_a_bg = ob_rr_theme->osd_bg;
58
59     prompt_a_button = RrAppearanceCopy(ob_rr_theme->osd_unpressed_button);
60     prompt_a_focus = RrAppearanceCopy(ob_rr_theme->osd_focused_button);
61     prompt_a_press = RrAppearanceCopy(ob_rr_theme->osd_pressed_button);    
62
63     prompt_a_msg = RrAppearanceCopy(ob_rr_theme->osd_hilite_label);
64     prompt_a_msg->texture[0].data.text.flow = TRUE;
65
66     if (reconfig) {
67         GList *it;
68         for (it = prompt_list; it; it = g_list_next(it)) {
69             ObPrompt *p = it->data;
70             prompt_layout(p);
71             render_all(p);
72         }
73     }
74 }
75
76 void prompt_shutdown(gboolean reconfig)
77 {
78     GList *it, *next;
79
80     if (!reconfig) {
81         for (it = prompt_list; it; it = next) {
82             ObPrompt *p = it->data;
83             next = it->next;
84             if (p->cleanup) p->cleanup(p, p->data);
85         }
86
87         g_assert(prompt_list == NULL);
88     }
89
90     RrAppearanceFree(prompt_a_button);
91     RrAppearanceFree(prompt_a_focus);
92     RrAppearanceFree(prompt_a_press);
93     RrAppearanceFree(prompt_a_msg);
94 }
95
96 ObPrompt* prompt_new(const gchar *msg, const gchar *title,
97                      const ObPromptAnswer *answers, gint n_answers,
98                      gint default_result, gint cancel_result,
99                      ObPromptCallback func, ObPromptCleanup cleanup,
100                      gpointer data)
101 {
102     ObPrompt *self;
103     XSetWindowAttributes attrib;
104     gint i;
105
106     attrib.override_redirect = FALSE;
107
108     self = g_slice_new0(ObPrompt);
109     self->ref = 1;
110     self->func = func;
111     self->cleanup = cleanup;
112     self->data = data;
113     self->default_result = default_result;
114     self->cancel_result = cancel_result;
115     self->super.type = OB_WINDOW_CLASS_PROMPT;
116     self->super.window = XCreateWindow(obt_display, obt_root(ob_screen),
117                                        0, 0, 1, 1, 0,
118                                        CopyFromParent, InputOutput,
119                                        CopyFromParent,
120                                        CWOverrideRedirect,
121                                        &attrib);
122     self->ic = obt_keyboard_context_new(self->super.window,
123                                         self->super.window);
124
125     /* make it a dialog type window */
126     OBT_PROP_SET32(self->super.window, NET_WM_WINDOW_TYPE, ATOM,
127                    OBT_PROP_ATOM(NET_WM_WINDOW_TYPE_DIALOG));
128
129     /* set the window's title */
130     if (title)
131         OBT_PROP_SETS(self->super.window, NET_WM_NAME, title);
132
133     /* listen for key presses on the window */
134     self->event_mask = KeyPressMask;
135
136     /* set up the text message widow */
137     self->msg.text = g_strdup(msg);
138     self->msg.window = XCreateWindow(obt_display, self->super.window,
139                                      0, 0, 1, 1, 0,
140                                      CopyFromParent, InputOutput,
141                                      CopyFromParent, 0, NULL);
142     XMapWindow(obt_display, self->msg.window);
143
144     /* set up the buttons from the answers */
145
146     self->n_buttons = n_answers;
147     if (!self->n_buttons)
148         self->n_buttons = 1;
149
150     self->button = g_new0(ObPromptElement, self->n_buttons);
151
152     if (n_answers == 0) {
153         g_assert(self->n_buttons == 1); /* should be set to this above.. */
154         self->button[0].text = g_strdup(_("OK"));
155     }
156     else {
157         g_assert(self->n_buttons > 0);
158         for (i = 0; i < self->n_buttons; ++i) {
159             self->button[i].text = g_strdup(answers[i].text);
160             self->button[i].result = answers[i].result;
161         }
162     }
163
164     for (i = 0; i < self->n_buttons; ++i) {
165         self->button[i].window = XCreateWindow(obt_display, self->super.window,
166                                                0, 0, 1, 1, 0,
167                                                CopyFromParent, InputOutput,
168                                                CopyFromParent, 0, NULL);
169         XMapWindow(obt_display, self->button[i].window);
170         window_add(&self->button[i].window, PROMPT_AS_WINDOW(self));
171
172         /* listen for button presses on the buttons */
173         XSelectInput(obt_display, self->button[i].window,
174                      ButtonPressMask | ButtonReleaseMask | ButtonMotionMask);
175     }
176
177     prompt_list = g_list_prepend(prompt_list, self);
178
179     return self;
180 }
181
182 void prompt_ref(ObPrompt *self)
183 {
184     ++self->ref;
185 }
186
187 void prompt_unref(ObPrompt *self)
188 {
189     if (self && --self->ref == 0) {
190         gint i;
191
192         if (self->mapped)
193             prompt_hide(self);
194
195         prompt_list = g_list_remove(prompt_list, self);
196
197         obt_keyboard_context_unref(self->ic);
198
199         for (i = 0; i < self->n_buttons; ++i) {
200             window_remove(self->button[i].window);
201             XDestroyWindow(obt_display, self->button[i].window);
202         }
203
204         XDestroyWindow(obt_display, self->msg.window);
205         XDestroyWindow(obt_display, self->super.window);
206         g_slice_free(ObPrompt, self);
207     }
208 }
209
210 static void prompt_layout(ObPrompt *self)
211 {
212     gint l, r, t, b;
213     gint i;
214     gint allbuttonsw, allbuttonsh, buttonx;
215     gint w, h;
216     gint maxw;
217
218     RrMargins(prompt_a_bg, &l, &t, &r, &b);
219     l += OUTSIDE_MARGIN;
220     t += OUTSIDE_MARGIN;
221     r += OUTSIDE_MARGIN;
222     b += OUTSIDE_MARGIN;
223
224     {
225         const Rect *area = screen_physical_area_all_monitors();
226         maxw = MIN(MAX_WIDTH, area->width*4/5);
227     }
228
229     /* find the button sizes and how much space we need for them */
230     allbuttonsw = allbuttonsh = 0;
231     for (i = 0; i < self->n_buttons; ++i) {
232         gint bw, bh;
233
234         prompt_a_button->texture[0].data.text.string = self->button[i].text;
235         prompt_a_focus->texture[0].data.text.string = self->button[i].text;
236         prompt_a_press->texture[0].data.text.string = self->button[i].text;
237         RrMinSize(prompt_a_button, &bw, &bh);
238         self->button[i].width = bw;
239         self->button[i].height = bh;
240         RrMinSize(prompt_a_focus, &bw, &bh);
241         self->button[i].width = MAX(self->button[i].width, bw);
242         self->button[i].height = MAX(self->button[i].height, bh);
243         RrMinSize(prompt_a_press, &bw, &bh);
244         self->button[i].width = MAX(self->button[i].width, bw);
245         self->button[i].height = MAX(self->button[i].height, bh);
246
247         self->button[i].width += BUTTON_HMARGIN * 2;
248         self->button[i].height += BUTTON_VMARGIN * 2;
249
250         allbuttonsw += self->button[i].width + (i > 0 ? BUTTON_SEPARATION : 0);
251         allbuttonsh = MAX(allbuttonsh, self->button[i].height);
252     }
253
254     self->msg_wbound = MAX(allbuttonsw, maxw);
255
256     /* measure the text message area */
257     prompt_a_msg->texture[0].data.text.string = self->msg.text;
258     prompt_a_msg->texture[0].data.text.maxwidth = self->msg_wbound;
259     RrMinSize(prompt_a_msg, &self->msg.width, &self->msg.height);
260
261     /* width and height inside the outer margins */
262     w = MAX(self->msg.width, allbuttonsw);
263     h = self->msg.height + MSG_BUTTON_SEPARATION + allbuttonsh;
264
265     /* position the text message */
266     self->msg.x = l + (w - self->msg.width) / 2;
267     self->msg.y = t;
268
269     /* position the button buttons on the right of the dialog */
270     buttonx = l + w;
271     for (i = self->n_buttons - 1; i >= 0; --i) {
272         self->button[i].x = buttonx - self->button[i].width;
273         buttonx -= self->button[i].width + BUTTON_SEPARATION;
274         self->button[i].y = t + h - allbuttonsh;
275         self->button[i].y += (allbuttonsh - self->button[i].height) / 2;
276     }
277
278     /* size and position the toplevel window */
279     prompt_resize(self, w + l + r, h + t + b);
280
281     /* move and resize the internal windows */
282     XMoveResizeWindow(obt_display, self->msg.window,
283                       self->msg.x, self->msg.y,
284                       self->msg.width, self->msg.height);
285     for (i = 0; i < self->n_buttons; ++i)
286         XMoveResizeWindow(obt_display, self->button[i].window,
287                           self->button[i].x, self->button[i].y,
288                           self->button[i].width, self->button[i].height);
289 }
290
291 static void prompt_resize(ObPrompt *self, gint w, gint h)
292 {
293     XConfigureRequestEvent req;
294     XSizeHints hints;
295
296     self->width = w;
297     self->height = h;
298
299     /* the user can't resize the prompt */
300     hints.flags = PMinSize | PMaxSize;
301     hints.min_width = hints.max_width = w;
302     hints.min_height = hints.max_height = h;
303     XSetWMNormalHints(obt_display, self->super.window, &hints);
304
305     if (self->mapped) {
306         /* send a configure request like a normal client would */
307         req.type = ConfigureRequest;
308         req.display = obt_display;
309         req.parent = obt_root(ob_screen);
310         req.window = self->super.window;
311         req.width = w;
312         req.height = h;
313         req.value_mask = CWWidth | CWHeight;
314         XSendEvent(req.display, req.window, FALSE, StructureNotifyMask,
315                    (XEvent*)&req);
316     }
317     else
318         XResizeWindow(obt_display, self->super.window, w, h);
319 }
320
321 static void setup_button_focus_tex(ObPromptElement *e, RrAppearance *a,
322                                    gboolean on)
323 {
324     gint i, l, r, t, b;
325
326     for (i = 1; i < 5; ++i)
327         a->texture[i].type = on ? RR_TEXTURE_LINE_ART : RR_TEXTURE_NONE;
328
329     if (!on) return;
330
331     RrMargins(a, &l, &t, &r, &b);
332     l += MIN(BUTTON_HMARGIN, BUTTON_VMARGIN) / 2;
333     r += MIN(BUTTON_HMARGIN, BUTTON_VMARGIN) / 2;
334     t += MIN(BUTTON_HMARGIN, BUTTON_VMARGIN) / 2;
335     b += MIN(BUTTON_HMARGIN, BUTTON_VMARGIN) / 2;
336
337     /* top line */
338     a->texture[1].data.lineart.x1 = l;
339     a->texture[1].data.lineart.x2 = e->width - r - 1;
340     a->texture[1].data.lineart.y1 = t;
341     a->texture[1].data.lineart.y2 = t;
342
343     /* bottom line */
344     a->texture[2].data.lineart.x1 = l;
345     a->texture[2].data.lineart.x2 = e->width - r - 1;
346     a->texture[2].data.lineart.y1 = e->height - b - 1;
347     a->texture[2].data.lineart.y2 = e->height - b - 1;
348
349     /* left line */
350     a->texture[3].data.lineart.x1 = l;
351     a->texture[3].data.lineart.x2 = l;
352     a->texture[3].data.lineart.y1 = t;
353     a->texture[3].data.lineart.y2 = e->height - b - 1;
354
355     /* right line */
356     a->texture[4].data.lineart.x1 = e->width - r - 1;
357     a->texture[4].data.lineart.x2 = e->width - r - 1;
358     a->texture[4].data.lineart.y1 = t;
359     a->texture[4].data.lineart.y2 = e->height - b - 1;
360 }
361
362 static void render_button(ObPrompt *self, ObPromptElement *e)
363 {
364     RrAppearance *a;
365
366     if (e->hover && e->pressed)       a = prompt_a_press;
367     else if (self->focus == e)        a = prompt_a_focus;
368     else                              a = prompt_a_button;
369
370     a->surface.parent = prompt_a_bg;
371     a->surface.parentx = e->x;
372     a->surface.parenty = e->y;
373
374     /* draw the keyfocus line */
375     if (self->focus == e)
376         setup_button_focus_tex(e, a, TRUE);
377
378     a->texture[0].data.text.string = e->text;
379     RrPaint(a, e->window, e->width, e->height);
380
381     /* turn off the keyfocus line so that it doesn't affect size calculations
382      */
383     if (self->focus == e)
384         setup_button_focus_tex(e, a, FALSE);
385 }
386
387 static void render_all(ObPrompt *self)
388 {
389     gint i;
390
391     RrPaint(prompt_a_bg, self->super.window, self->width, self->height);
392
393     prompt_a_msg->surface.parent = prompt_a_bg;
394     prompt_a_msg->surface.parentx = self->msg.x;
395     prompt_a_msg->surface.parenty = self->msg.y;
396
397     prompt_a_msg->texture[0].data.text.string = self->msg.text;
398     prompt_a_msg->texture[0].data.text.maxwidth = self->msg_wbound;
399     RrPaint(prompt_a_msg, self->msg.window, self->msg.width, self->msg.height);
400
401     for (i = 0; i < self->n_buttons; ++i)
402         render_button(self, &self->button[i]);
403 }
404
405 void prompt_show(ObPrompt *self, ObClient *parent, gboolean modal)
406 {
407     gint i;
408
409     if (self->mapped) {
410         /* activate the prompt */
411         OBT_PROP_MSG(ob_screen, self->super.window, NET_ACTIVE_WINDOW,
412                      1, /* from an application.. */
413                      event_time(),
414                      0,
415                      0, 0);
416         return;
417     }
418
419     /* set the focused button (if not found then the first button is used) */
420     self->focus = &self->button[0];
421     for (i = 0; i < self->n_buttons; ++i)
422         if (self->button[i].result == self->default_result) {
423             self->focus = &self->button[i];
424             break;
425         }
426
427     if (parent) {
428         Atom states[1];
429         gint nstates;
430         Window p;
431         XWMHints h;
432
433         if (parent->group) {
434             /* make it transient for the window's group */
435             h.flags = WindowGroupHint;
436             h.window_group = parent->group->leader;
437             p = obt_root(ob_screen);
438         }
439         else {
440             /* make it transient for the window directly */
441             h.flags = 0;
442             p = parent->window;
443         }
444
445         XSetWMHints(obt_display, self->super.window, &h);
446         OBT_PROP_SET32(self->super.window, WM_TRANSIENT_FOR, WINDOW, p);
447
448         states[0] = OBT_PROP_ATOM(NET_WM_STATE_MODAL);
449         nstates = (modal ? 1 : 0);
450         OBT_PROP_SETA32(self->super.window, NET_WM_STATE, ATOM,
451                         states, nstates);
452     }
453     else
454         OBT_PROP_ERASE(self->super.window, WM_TRANSIENT_FOR);
455
456     /* set up the dialog and render it */
457     prompt_layout(self);
458     render_all(self);
459
460     client_manage(self->super.window, self);
461
462     self->mapped = TRUE;
463 }
464
465 void prompt_hide(ObPrompt *self)
466 {
467     XUnmapWindow(obt_display, self->super.window);
468     self->mapped = FALSE;
469 }
470
471 gboolean prompt_key_event(ObPrompt *self, XEvent *e)
472 {
473     gboolean shift;
474     guint shift_mask, mods;
475     KeySym sym;
476
477     if (e->type != KeyPress) return FALSE;
478
479     shift_mask = obt_keyboard_modkey_to_modmask(OBT_KEYBOARD_MODKEY_SHIFT);
480     mods = obt_keyboard_only_modmasks(e->xkey.state);
481     shift = !!(mods & shift_mask);
482
483     /* only accept shift */
484     if (mods != 0 && mods != shift_mask)
485         return FALSE;
486
487     sym = obt_keyboard_keypress_to_keysym(e);
488
489     if (sym == XK_Escape)
490         prompt_cancel(self);
491     else if (sym == XK_Return || sym == XK_KP_Enter || sym == XK_space)
492         prompt_run_callback(self, self->focus->result);
493     else if (sym == XK_Tab || sym == XK_Left || sym == XK_Right) {
494         gint i;
495         gboolean left;
496         ObPromptElement *oldfocus;
497
498         left = (sym == XK_Left) || ((sym == XK_Tab) && shift);
499         oldfocus = self->focus;
500
501         for (i = 0; i < self->n_buttons; ++i)
502             if (self->focus == &self->button[i]) break;
503         i += (left ? -1 : 1);
504         if (i < 0) i = self->n_buttons - 1;
505         else if (i >= self->n_buttons) i = 0;
506         self->focus = &self->button[i];
507
508         if (oldfocus != self->focus) render_button(self, oldfocus);
509         render_button(self, self->focus);
510     }
511     return TRUE;
512 }
513
514 gboolean prompt_mouse_event(ObPrompt *self, XEvent *e)
515 {
516     gint i;
517     ObPromptElement *but;
518
519     if (e->type != ButtonPress && e->type != ButtonRelease &&
520         e->type != MotionNotify) return FALSE;
521
522     /* find the button */
523     but = NULL;
524     for (i = 0; i < self->n_buttons; ++i)
525         if (self->button[i].window ==
526             (e->type == MotionNotify ? e->xmotion.window : e->xbutton.window))
527         {
528             but = &self->button[i];
529             break;
530         }
531     if (!but) return FALSE;
532
533     if (e->type == ButtonPress) {
534         ObPromptElement *oldfocus;
535
536         oldfocus = self->focus;
537
538         but->pressed = but->hover = TRUE;
539         self->focus = but;
540
541         if (oldfocus != but) render_button(self, oldfocus);
542         render_button(self, but);
543     }
544     else if (e->type == ButtonRelease) {
545         if (but->hover)
546             prompt_run_callback(self, but->result);
547         but->pressed = FALSE;
548     }
549     else if (e->type == MotionNotify) {
550         if (but->pressed) {
551             gboolean hover;
552
553             hover = (e->xmotion.x >= 0 && e->xmotion.y >= 0 &&
554                      e->xmotion.x < but->width && e->xmotion.y < but->height);
555
556             if (hover != but->hover) {
557                 but->hover = hover;
558                 render_button(self, but);
559             }
560         }
561     }
562     return TRUE;
563 }
564
565 void prompt_cancel(ObPrompt *self)
566 {
567     prompt_run_callback(self, self->cancel_result);
568 }
569
570 static gboolean prompt_show_message_cb(ObPrompt *p, int res, gpointer data)
571 {
572     return TRUE; /* call the cleanup func */
573 }
574
575 static void prompt_show_message_cleanup(ObPrompt *p, gpointer data)
576 {
577     prompt_unref(p);
578 }
579
580 ObPrompt* prompt_show_message(const gchar *msg, const gchar *title,
581                               const gchar *answer)
582 {
583     ObPrompt *p;
584     ObPromptAnswer ans[] = {
585         { answer, 0 }
586     };
587
588     p = prompt_new(msg, title, ans, 1, 0, 0,
589                    prompt_show_message_cb, prompt_show_message_cleanup, NULL);
590     prompt_show(p, NULL, FALSE);
591     return p;
592 }
593
594 static void prompt_run_callback(ObPrompt *self, gint result)
595 {
596     prompt_ref(self);
597     if (self->func) {
598         gboolean clean = self->func(self, result, self->data);
599         if (clean && self->cleanup)
600             self->cleanup(self, self->data);
601     }
602     prompt_hide(self);
603     prompt_unref(self);
604 }