make focus cycle target fallback work right by going to the next prev window
[mikachu/openbox.git] / openbox / focus_cycle_popup.c
1 /* -*- indent-tabs-mode: nil; tab-width: 4; c-basic-offset: 4; -*-
2
3    focus_cycle_popup.c for the Openbox window manager
4    Copyright (c) 2006        Mikael Magnusson
5    Copyright (c) 2003-2007   Dana Jansens
6
7    This program is free software; you can redistribute it and/or modify
8    it under the terms of the GNU General Public License as published by
9    the Free Software Foundation; either version 2 of the License, or
10    (at your option) any later version.
11
12    This program 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
15    GNU General Public License for more details.
16
17    See the COPYING file for a copy of the GNU General Public License.
18 */
19
20 #include "focus_cycle_popup.h"
21 #include "focus_cycle.h"
22 #include "popup.h"
23 #include "client.h"
24 #include "screen.h"
25 #include "focus.h"
26 #include "openbox.h"
27 #include "window.h"
28 #include "event.h"
29 #include "render/render.h"
30
31 #include <X11/Xlib.h>
32 #include <glib.h>
33
34 #define ICON_SIZE 40
35 #define ICON_HILITE_WIDTH 2
36 #define ICON_HILITE_MARGIN 1
37 #define OUTSIDE_BORDER 3
38 #define TEXT_BORDER 2
39
40 typedef struct _ObFocusCyclePopup       ObFocusCyclePopup;
41 typedef struct _ObFocusCyclePopupTarget ObFocusCyclePopupTarget;
42
43 struct _ObFocusCyclePopupTarget
44 {
45     ObClient *client;
46     RrImage *icon;
47     gchar *text;
48     Window win;
49 };
50
51 struct _ObFocusCyclePopup
52 {
53     ObWindow obwin;
54     Window bg;
55
56     Window text;
57
58     GList *targets;
59     gint n_targets;
60
61     const ObFocusCyclePopupTarget *last_target;
62
63     gint maxtextw;
64
65     RrAppearance *a_bg;
66     RrAppearance *a_text;
67     RrAppearance *a_icon;
68
69     RrPixel32 *hilite_rgba;
70
71     gboolean mapped;
72 };
73
74 /*! This popup shows all possible windows */
75 static ObFocusCyclePopup popup;
76 /*! This popup shows a single window */
77 static ObIconPopup *single_popup;
78
79 static gchar   *popup_get_name (ObClient *c);
80 static gboolean popup_setup    (ObFocusCyclePopup *p,
81                                 gboolean create_targets,
82                                 gboolean refresh_targets);
83 static void     popup_render   (ObFocusCyclePopup *p,
84                                 const ObClient *c);
85
86 static Window create_window(Window parent, guint bwidth, gulong mask,
87                             XSetWindowAttributes *attr)
88 {
89     return XCreateWindow(ob_display, parent, 0, 0, 1, 1, bwidth,
90                          RrDepth(ob_rr_inst), InputOutput,
91                          RrVisual(ob_rr_inst), mask, attr);
92 }
93
94 void focus_cycle_popup_startup(gboolean reconfig)
95 {
96     XSetWindowAttributes attrib;
97
98     single_popup = icon_popup_new();
99
100     popup.obwin.type = Window_Internal;
101     popup.a_bg = RrAppearanceCopy(ob_rr_theme->osd_hilite_bg);
102     popup.a_text = RrAppearanceCopy(ob_rr_theme->osd_hilite_label);
103     popup.a_icon = RrAppearanceCopy(ob_rr_theme->a_clear_tex);
104
105     popup.a_text->surface.parent = popup.a_bg;
106     popup.a_icon->surface.parent = popup.a_bg;
107
108     RrAppearanceClearTextures(popup.a_icon);
109     popup.a_icon->texture[0].type = RR_TEXTURE_IMAGE;
110
111     RrAppearanceAddTextures(popup.a_bg, 1);
112     popup.a_bg->texture[0].type = RR_TEXTURE_RGBA;
113
114     attrib.override_redirect = True;
115     attrib.border_pixel=RrColorPixel(ob_rr_theme->osd_border_color);
116     popup.bg = create_window(RootWindow(ob_display, ob_screen),
117                              ob_rr_theme->obwidth,
118                              CWOverrideRedirect | CWBorderPixel, &attrib);
119
120     popup.text = create_window(popup.bg, 0, 0, NULL);
121
122     popup.targets = NULL;
123     popup.n_targets = 0;
124     popup.last_target = NULL;
125
126     popup.hilite_rgba = NULL;
127
128     XMapWindow(ob_display, popup.text);
129
130     stacking_add(INTERNAL_AS_WINDOW(&popup));
131     g_hash_table_insert(window_map, &popup.bg, &popup);
132 }
133
134 void focus_cycle_popup_shutdown(gboolean reconfig)
135 {
136     icon_popup_free(single_popup);
137
138     g_hash_table_remove(window_map, &popup.bg);
139     stacking_remove(INTERNAL_AS_WINDOW(&popup));
140
141     while(popup.targets) {
142         ObFocusCyclePopupTarget *t = popup.targets->data;
143
144         RrImageUnref(t->icon);
145         g_free(t->text);
146         XDestroyWindow(ob_display, t->win);
147         g_free(t);
148
149         popup.targets = g_list_delete_link(popup.targets, popup.targets);
150     }
151
152     g_free(popup.hilite_rgba);
153     popup.hilite_rgba = NULL;
154
155     XDestroyWindow(ob_display, popup.text);
156     XDestroyWindow(ob_display, popup.bg);
157
158     RrAppearanceFree(popup.a_icon);
159     RrAppearanceFree(popup.a_text);
160     RrAppearanceFree(popup.a_bg);
161 }
162
163 static void popup_target_free(ObFocusCyclePopupTarget *t)
164 {
165     RrImageUnref(t->icon);
166     g_free(t->text);
167     XDestroyWindow(ob_display, t->win);
168     g_free(t);
169 }
170
171 static gboolean popup_setup(ObFocusCyclePopup *p, gboolean create_targets,
172                             gboolean refresh_targets)
173 {
174     gint maxwidth, n;
175     GList *it;
176     GList *rtargets; /* old targets for refresh */
177     GList *rtlast;
178     gboolean change;
179
180     if (refresh_targets) {
181         rtargets = p->targets;
182         rtlast = g_list_last(rtargets);
183         p->targets = NULL;
184         p->n_targets = 0;
185         change = FALSE;
186     }
187     else {
188         rtargets = rtlast = NULL;
189         change = TRUE;
190     }
191
192     g_assert(p->targets == NULL);
193     g_assert(p->n_targets == 0);
194
195     /* make its width to be the width of all the possible titles */
196
197     /* build a list of all the valid focus targets and measure their strings,
198        and count them */
199     maxwidth = 0;
200     n = 0;
201     for (it = g_list_last(focus_order); it; it = g_list_previous(it)) {
202         ObClient *ft = it->data;
203
204         if (focus_cycle_valid(ft)) {
205             GList *rit;
206
207             /* reuse the target if possible during refresh */
208             for (rit = rtlast; rit; rit = g_list_previous(rit)) {
209                 ObFocusCyclePopupTarget *t = rit->data;
210                 if (t->client == ft) {
211                     if (rit == rtlast)
212                         rtlast = g_list_previous(rit);
213                     rtargets = g_list_remove_link(rtargets, rit);
214
215                     p->targets = g_list_concat(rit, p->targets);
216                     ++n;
217
218                     if (rit != rtlast)
219                         change = TRUE; /* order changed */
220                     break;
221                 }
222             }
223
224             if (!rit) {
225                 gchar *text = popup_get_name(ft);
226
227                 /* measure */
228                 p->a_text->texture[0].data.text.string = text;
229                 maxwidth = MAX(maxwidth, RrMinWidth(p->a_text));
230
231                 if (!create_targets)
232                     g_free(text);
233                 else {
234                     ObFocusCyclePopupTarget *t =
235                         g_new(ObFocusCyclePopupTarget, 1);
236
237                     t->client = ft;
238                     t->text = text;
239                     t->icon = client_icon(t->client);
240                     RrImageRef(t->icon); /* own the icon so it won't go away */
241                     t->win = create_window(p->bg, 0, 0, NULL);
242
243                     XMapWindow(ob_display, t->win);
244
245                     p->targets = g_list_prepend(p->targets, t);
246                     ++n;
247
248                     change = TRUE; /* added a window */
249                 }
250             }
251         }
252     }
253
254     if (rtargets) {
255         change = TRUE; /* removed a window */
256
257         while (rtargets) {
258             popup_target_free(rtargets->data);
259             rtargets = g_list_delete_link(rtargets, rtargets);
260         }
261     }
262
263     p->n_targets = n;
264     p->maxtextw = maxwidth;
265
266     return change;
267 }
268
269 static void popup_cleanup(void)
270 {
271     while(popup.targets) {
272         popup_target_free(popup.targets->data);
273         popup.targets = g_list_delete_link(popup.targets, popup.targets);
274     }
275     popup.n_targets = 0;
276     popup.last_target = NULL;
277 }
278
279 static gchar *popup_get_name(ObClient *c)
280 {
281     ObClient *p;
282     gchar *title;
283     const gchar *desk = NULL;
284     gchar *ret;
285
286     /* find our highest direct parent */
287     p = client_search_top_direct_parent(c);
288
289     if (c->desktop != DESKTOP_ALL && c->desktop != screen_desktop)
290         desk = screen_desktop_names[c->desktop];
291
292     title = c->iconic ? c->icon_title : c->title;
293
294     /* use the transient's parent's title/icon if we don't have one */
295     if (p != c && title[0] == '\0')
296         title = p->iconic ? p->icon_title : p->title;
297
298     if (desk)
299         ret = g_strdup_printf("%s [%s]", title, desk);
300     else
301         ret = g_strdup(title);
302
303     return ret;
304 }
305
306 static void popup_render(ObFocusCyclePopup *p, const ObClient *c)
307 {
308     gint ml, mt, mr, mb;
309     gint l, t, r, b;
310     gint x, y, w, h;
311     Rect *screen_area = NULL;
312     gint icons_per_row;
313     gint icon_rows;
314     gint textx, texty, textw, texth;
315     gint rgbax, rgbay, rgbaw, rgbah;
316     gint icons_center_x;
317     gint innerw, innerh;
318     gint i;
319     GList *it;
320     const ObFocusCyclePopupTarget *newtarget;
321     gint newtargetx, newtargety;
322
323     screen_area = screen_physical_area_primary(FALSE);
324
325     /* get the outside margins */
326     RrMargins(p->a_bg, &ml, &mt, &mr, &mb);
327
328     /* get our outside borders */
329     l = ml + OUTSIDE_BORDER;
330     r = mr + OUTSIDE_BORDER;
331     t = mt + OUTSIDE_BORDER;
332     b = mb + OUTSIDE_BORDER;
333
334     /* get the icon pictures' sizes */
335     innerw = ICON_SIZE - (ICON_HILITE_WIDTH + ICON_HILITE_MARGIN) * 2;
336     innerh = ICON_SIZE - (ICON_HILITE_WIDTH + ICON_HILITE_MARGIN) * 2;
337
338     /* get the width from the text and keep it within limits */
339     w = l + r + p->maxtextw;
340     w = MIN(w, MAX(screen_area->width/3, POPUP_WIDTH)); /* max width */
341     w = MAX(w, POPUP_WIDTH); /* min width */
342
343     /* how many icons will fit in that row? make the width fit that */
344     w -= l + r;
345     icons_per_row = (w + ICON_SIZE - 1) / ICON_SIZE;
346     w = icons_per_row * ICON_SIZE + l + r;
347
348     /* how many rows do we need? */
349     icon_rows = (p->n_targets-1) / icons_per_row + 1;
350
351     /* get the text dimensions */
352     textw = w - l - r;
353     texth = RrMinHeight(p->a_text) + TEXT_BORDER * 2;
354
355     /* find the height of the dialog */
356     h = t + b + (icon_rows * ICON_SIZE) + (OUTSIDE_BORDER + texth);
357
358     /* get the position of the text */
359     textx = l;
360     texty = h - texth - b;
361
362     /* find the position for the popup (include the outer borders) */
363     x = screen_area->x + (screen_area->width -
364                           (w + ob_rr_theme->obwidth * 2)) / 2;
365     y = screen_area->y + (screen_area->height -
366                           (h + ob_rr_theme->obwidth * 2)) / 2;
367
368     /* get the dimensions of the target hilite texture */
369     rgbax = ml;
370     rgbay = mt;
371     rgbaw = w - ml - mr;
372     rgbah = h - mt - mb;
373
374     /* center the icons if there is less than one row */
375     if (icon_rows == 1)
376         icons_center_x = (w - p->n_targets * ICON_SIZE) / 2;
377     else
378         icons_center_x = 0;
379
380     if (!p->mapped) {
381         /* position the background but don't draw it*/
382         XMoveResizeWindow(ob_display, p->bg, x, y, w, h);
383
384         /* set up the hilite texture for the background */
385         p->a_bg->texture[0].data.rgba.width = rgbaw;
386         p->a_bg->texture[0].data.rgba.height = rgbah;
387         p->a_bg->texture[0].data.rgba.alpha = 0xff;
388         p->hilite_rgba = g_new(RrPixel32, rgbaw * rgbah);
389         p->a_bg->texture[0].data.rgba.data = p->hilite_rgba;
390
391         /* position the text, but don't draw it */
392         XMoveResizeWindow(ob_display, p->text, textx, texty, textw, texth);
393         p->a_text->surface.parentx = textx;
394         p->a_text->surface.parenty = texty;
395     }
396
397     /* find the focused target */
398     for (i = 0, it = p->targets; it; ++i, it = g_list_next(it)) {
399         const ObFocusCyclePopupTarget *target = it->data;
400         const gint row = i / icons_per_row; /* starting from 0 */
401         const gint col = i % icons_per_row; /* starting from 0 */
402
403         if (target->client == c) {
404             /* save the target */
405             newtarget = target;
406             newtargetx = icons_center_x + l + (col * ICON_SIZE);
407             newtargety = t + (row * ICON_SIZE);
408
409             if (!p->mapped)
410                 break; /* if we're not dimensioning, then we're done */
411         }
412     }
413
414     g_assert(newtarget != NULL);
415
416     /* create the hilite under the target icon */
417     {
418         RrPixel32 color;
419         gint i, j, o;
420
421         color = ((ob_rr_theme->osd_color->r & 0xff) << RrDefaultRedOffset) +
422             ((ob_rr_theme->osd_color->g & 0xff) << RrDefaultGreenOffset) +
423             ((ob_rr_theme->osd_color->b & 0xff) << RrDefaultBlueOffset);
424
425         o = 0;
426         for (i = 0; i < rgbah; ++i)
427             for (j = 0; j < rgbaw; ++j) {
428                 guchar a;
429                 const gint x = j + rgbax - newtargetx;
430                 const gint y = i + rgbay - newtargety;
431
432                 if (x < 0 || x >= ICON_SIZE ||
433                     y < 0 || y >= ICON_SIZE)
434                 {
435                     /* outside the target */
436                     a = 0x00;
437                 }
438                 else if (x < ICON_HILITE_WIDTH ||
439                          x >= ICON_SIZE - ICON_HILITE_WIDTH ||
440                          y < ICON_HILITE_WIDTH ||
441                          y >= ICON_SIZE - ICON_HILITE_WIDTH)
442                 {
443                     /* the border of the target */
444                     a = 0x88;
445                 }
446                 else {
447                     /* the background of the target */
448                     a = 0x22;
449                 }
450
451                 p->hilite_rgba[o++] =
452                     color + (a << RrDefaultAlphaOffset);
453             }
454     }
455
456     /* * * draw everything * * */
457
458     /* draw the background */
459     RrPaint(p->a_bg, p->bg, w, h);
460
461     /* draw the icons */
462     for (i = 0, it = p->targets; it; ++i, it = g_list_next(it)) {
463         const ObFocusCyclePopupTarget *target = it->data;
464
465         /* have to redraw the targetted icon and last targetted icon,
466            they can pick up the hilite changes in the backgroud */
467         if (!p->mapped || newtarget == target || p->last_target == target) {
468             const gint row = i / icons_per_row; /* starting from 0 */
469             const gint col = i % icons_per_row; /* starting from 0 */
470             gint innerx, innery;
471
472             /* find the dimensions of the icon inside it */
473             innerx = icons_center_x + l + (col * ICON_SIZE);
474             innerx += ICON_HILITE_WIDTH + ICON_HILITE_MARGIN;
475             innery = t + (row * ICON_SIZE);
476             innery += ICON_HILITE_WIDTH + ICON_HILITE_MARGIN;
477
478             /* move the icon */
479             XMoveResizeWindow(ob_display, target->win,
480                               innerx, innery, innerw, innerh);
481
482             /* get the icon from the client */
483             p->a_icon->texture[0].data.image.alpha =
484                 target->client->iconic ? OB_ICONIC_ALPHA : 0xff;
485             p->a_icon->texture[0].data.image.image = target->icon;
486
487             /* draw the icon */
488             p->a_icon->surface.parentx = innerx;
489             p->a_icon->surface.parenty = innery;
490             RrPaint(p->a_icon, target->win, innerw, innerh);
491         }
492     }
493
494     /* draw the text */
495     p->a_text->texture[0].data.text.string = newtarget->text;
496     p->a_text->surface.parentx = textx;
497     p->a_text->surface.parenty = texty;
498     RrPaint(p->a_text, p->text, textw, texth);
499
500     p->last_target = newtarget;
501
502     g_free(screen_area);
503 }
504
505 void focus_cycle_popup_show(ObClient *c, gboolean iconic_windows,
506                             gboolean all_desktops, gboolean dock_windows,
507                             gboolean desktop_windows)
508 {
509     g_assert(c != NULL);
510
511     /* do this stuff only when the dialog is first showing */
512     if (!popup.mapped)
513         popup_setup(&popup, TRUE, FALSE);
514     g_assert(popup.targets != NULL);
515
516     popup_render(&popup, c);
517
518     if (!popup.mapped) {
519         /* show the dialog */
520         XMapWindow(ob_display, popup.bg);
521         XFlush(ob_display);
522         popup.mapped = TRUE;
523         screen_hide_desktop_popup();
524     }
525 }
526
527 void focus_cycle_popup_hide(void)
528 {
529     gulong ignore_start;
530
531     ignore_start = event_start_ignore_all_enters();
532
533     XUnmapWindow(ob_display, popup.bg);
534     XFlush(ob_display);
535
536     event_end_ignore_all_enters(ignore_start);
537
538     popup.mapped = FALSE;
539
540     popup_cleanup();
541
542     g_free(popup.hilite_rgba);
543     popup.hilite_rgba = NULL;
544 }
545
546 void focus_cycle_popup_single_show(struct _ObClient *c,
547                                    gboolean iconic_windows,
548                                    gboolean all_desktops,
549                                    gboolean dock_windows,
550                                    gboolean desktop_windows)
551 {
552     gchar *text;
553
554     g_assert(c != NULL);
555
556     /* do this stuff only when the dialog is first showing */
557     if (!popup.mapped) {
558         Rect *a;
559
560         popup_setup(&popup, FALSE, FALSE);
561         g_assert(popup.targets == NULL);
562
563         /* position the popup */
564         a = screen_physical_area_primary(FALSE);
565         icon_popup_position(single_popup, CenterGravity,
566                             a->x + a->width / 2, a->y + a->height / 2);
567         icon_popup_height(single_popup, POPUP_HEIGHT);
568         icon_popup_min_width(single_popup, POPUP_WIDTH);
569         icon_popup_max_width(single_popup, MAX(a->width/3, POPUP_WIDTH));
570         icon_popup_text_width(single_popup, popup.maxtextw);
571         g_free(a);
572     }
573
574     text = popup_get_name(c);
575     icon_popup_show(single_popup, text, client_icon(c));
576     g_free(text);
577     screen_hide_desktop_popup();
578 }
579
580 void focus_cycle_popup_single_hide(void)
581 {
582     icon_popup_hide(single_popup);
583 }
584
585 gboolean focus_cycle_popup_is_showing(ObClient *c)
586 {
587     if (popup.mapped) {
588         GList *it;
589
590         for (it = popup.targets; it; it = g_list_next(it)) {
591             ObFocusCyclePopupTarget *t = it->data;
592             if (t->client == c)
593                 return TRUE;
594         }
595     }
596     return FALSE;
597 }
598
599 static ObClient* popup_revert(ObClient *target)
600 {
601     GList *it, *itt;
602
603     for (it = popup.targets; it; it = g_list_next(it)) {
604         ObFocusCyclePopupTarget *t = it->data;
605         if (t->client == target) {
606             /* move to a previous window if possible */
607             for (itt = it->prev; itt; itt = g_list_previous(itt)) {
608                 ObFocusCyclePopupTarget *t2 = itt->data;
609                 if (focus_cycle_valid(t2->client))
610                     return t2->client;
611             }
612
613             /* otherwise move to a following window if possible */
614             for (itt = it->next; itt; itt = g_list_next(itt)) {
615                 ObFocusCyclePopupTarget *t2 = itt->data;
616                 if (focus_cycle_valid(t2->client))
617                     return t2->client;
618             }
619
620             /* otherwise, we can't go anywhere there is nowhere valid to go */
621             return NULL;
622         }
623     }
624     return NULL;
625 }
626
627 ObClient* focus_cycle_popup_refresh(ObClient *target,
628                                     gboolean redraw)
629 {
630     if (!popup.mapped) return NULL;
631
632     if (!focus_cycle_valid(target))
633         target = popup_revert(target);
634
635     redraw = popup_setup(&popup, TRUE, TRUE) && redraw;
636
637     if (!target && popup.targets)
638         target = ((ObFocusCyclePopupTarget*)popup.targets->data)->client;
639
640     if (target && redraw) {
641         popup.mapped = FALSE;
642         popup_render(&popup, target);
643         XFlush(ob_display);
644         popup.mapped = TRUE;
645     }
646
647     return target;
648 }