1 /*
2 ** Custom button widget, that supports having a secondary click-function associated with being
3 ** clicked by the middle mouse button. Very much inspired by Directory Opus on the Amiga.
4 **
5 ** Original GTK+ 1.2.x version by Johan Hanson, re-written for GTK+ 2.0 and 3.0 by Emil Brink.
6 */
7
8 #include <stdio.h>
9 #include <stdlib.h>
10
11 #include "config.h"
12
13 #include <gtk/gtk.h>
14
15 #include "odmultibutton.h"
16
17 static void od_multibutton_class_init(ODMultiButtonClass *mbc);
18 static void od_multibutton_init(ODMultiButton *mb);
19
20 /* This seems to be customary in GTK+-land. I find it a bit... weird. */
21 static GtkButtonClass *button_class = NULL;
22
od_multibutton_set_trace(ODMultiButton * widget,unsigned int trace_mask)23 void od_multibutton_set_trace(ODMultiButton *widget, unsigned int trace_mask)
24 {
25 g_return_if_fail(widget != NULL);
26 g_return_if_fail(OD_IS_MULTIBUTTON(widget));
27
28 widget->trace_mask = trace_mask;
29 }
30
od_multibutton_get_type(void)31 GType od_multibutton_get_type(void)
32 {
33 static GType od_multibutton_type = 0;
34
35 if(od_multibutton_type == 0)
36 {
37 static const GTypeInfo mb_info =
38 {
39 sizeof (ODMultiButtonClass),
40 NULL,
41 NULL,
42 (GClassInitFunc) od_multibutton_class_init,
43 NULL,
44 NULL,
45 sizeof (ODMultiButton),
46 8,
47 (GInstanceInitFunc) od_multibutton_init,
48 };
49 od_multibutton_type = g_type_register_static(GTK_TYPE_BUTTON, "ODMultiButton", &mb_info, 0);
50 }
51 return od_multibutton_type;
52 }
53
54 /* Set which "page" should be displayed. Typically called as user clicks on the button widget. */
od_multibutton_set_page(GtkWidget * widget,guint index)55 static void od_multibutton_set_page(GtkWidget *widget, guint index)
56 {
57 ODMultiButton *mb;
58 GtkWidget *p, *op;
59
60 g_return_if_fail(widget != NULL);
61 g_return_if_fail(index < sizeof mb->page / sizeof *mb->page);
62 g_return_if_fail(OD_IS_MULTIBUTTON(widget));
63
64 mb = OD_MULTIBUTTON(widget);
65
66 p = mb->page[index].widget;
67 op = gtk_bin_get_child(GTK_BIN(mb));
68
69 if(op != p)
70 {
71 if(op != NULL)
72 {
73 g_object_ref(G_OBJECT(op));
74 gtk_container_remove(GTK_CONTAINER(mb), op);
75 }
76 if(p != NULL)
77 {
78 GtkWidget *pparent = gtk_widget_get_parent(p);
79
80 if(gtk_widget_get_state_flags(widget) != gtk_widget_get_state_flags(p))
81 gtk_widget_set_state_flags(p, gtk_widget_get_state_flags(widget), TRUE);
82 gtk_widget_show(p);
83 if(pparent != NULL)
84 {
85 /* Manual reparenting. Semi-legacy code, not sure when this is needed at all. :/ */
86 g_object_ref(p);
87 gtk_container_remove(GTK_CONTAINER(pparent), p);
88 gtk_container_add(GTK_CONTAINER(widget), p);
89 g_object_unref(p);
90 }
91 else
92 {
93 gtk_container_add(GTK_CONTAINER(mb), p);
94 g_object_unref(G_OBJECT(p));
95 }
96 }
97 mb->last_index = index;
98 }
99 if(gtk_widget_is_drawable(widget))
100 gtk_widget_queue_draw(widget);
101 }
102
103 /* Compute size of the page widgets, and set width & height members to the maximum page size. */
od_multibutton_page_size_calc(ODMultiButton * mb)104 static void od_multibutton_page_size_calc(ODMultiButton *mb)
105 {
106 guint i;
107
108 mb->width = mb->height = 0;
109 for(i = 0; i < sizeof mb->page / sizeof *mb->page; i++)
110 {
111 GtkWidget *w;
112
113 if((w = mb->page[i].widget) != NULL)
114 {
115 GtkRequisition req_min, req_max;
116
117 gtk_widget_get_preferred_size(w, &req_min, &req_max);
118 mb->width = MAX(mb->width, req_min.width);
119 mb->height = MAX(mb->height, req_min.height);
120 }
121 }
122 }
123
od_multibutton_get_preferred_width(GtkWidget * widget,gint * min_width,gint * max_width)124 static void od_multibutton_get_preferred_width(GtkWidget *widget, gint *min_width, gint *max_width)
125 {
126 ODMultiButton *mb;
127 gint width;
128
129 g_return_if_fail(widget != NULL);
130 g_return_if_fail(OD_IS_MULTIBUTTON(widget));
131
132 mb = OD_MULTIBUTTON(widget);
133 od_multibutton_page_size_calc(mb);
134 width = mb->width;
135
136 width += 2 * gtk_container_get_border_width(GTK_CONTAINER(widget));
137 /* FIXME: Not sure if we need to dig up CSS box model spacing here (margin/padding). */
138
139 if(min_width != NULL)
140 *min_width = width;
141 if(max_width != NULL)
142 *max_width = width;
143 }
144
145 /* Paint the "dog ear" that indicates second-mouse-button binding. This code is pretty much lifted
146 ** from Johan Hanson's original ODEmilButton widget, although not a straight copy and paste.
147 */
od_multibutton_paint_dog_ear(GtkWidget * widget,cairo_t * cr,const GtkAllocation * alloc,gint bw)148 static void od_multibutton_paint_dog_ear(GtkWidget *widget, cairo_t *cr, const GtkAllocation *alloc, gint bw)
149 {
150 const GtkStateFlags sflags = gtk_widget_get_state_flags(widget);
151
152 if(gtk_widget_is_drawable(widget) && sflags != GTK_STATE_FLAG_ACTIVE)
153 {
154 const gint EAR_SIZE = 5;
155
156 cairo_move_to(cr, alloc->width - 2 * bw - (EAR_SIZE + 1), 0);
157 cairo_rel_line_to(cr, 0, EAR_SIZE);
158 cairo_rel_line_to(cr, EAR_SIZE, 0);
159 cairo_close_path(cr);
160 cairo_set_line_width(cr, 0.5);
161 cairo_stroke(cr);
162 }
163 }
164
od_multibutton_paint_foreground(GtkWidget * widget,cairo_t * cr)165 static void od_multibutton_paint_foreground(GtkWidget *widget, cairo_t *cr)
166 {
167 g_return_if_fail(widget != NULL);
168 g_return_if_fail(cr != NULL);
169 g_return_if_fail(OD_IS_MULTIBUTTON(widget));
170
171 if(gtk_widget_is_drawable(widget))
172 {
173 const ODMultiButton *mb = OD_MULTIBUTTON(widget);
174 GtkAllocation alloc;
175 const gint bw = gtk_container_get_border_width(GTK_CONTAINER(widget));
176
177 gtk_widget_get_allocation(widget, &alloc);
178 if(mb->page[1].widget != NULL)
179 od_multibutton_paint_dog_ear(widget, cr, &alloc, bw);
180 if(mb->config && mb->config_down)
181 {
182 GtkStyleContext *sctx = gtk_widget_get_style_context(widget);
183
184 /* Trivial border-rendering, instead of faked sunken-in. */
185 gtk_render_focus(sctx, cr, 0, 0, alloc.width - 2 * bw, alloc.height - 2 * bw);
186 }
187 }
188 }
189
190
191 /* This is the core expose handler. It simply relies on the superclass (GtkButton) to do most
192 ** of the drawing, taking care to fool it into doing what we want by poking a little. We also
193 ** have our own foreground painting routine, for that oh-so-cute dog ear.
194 */
od_multibutton_draw(GtkWidget * widget,cairo_t * cr)195 static gboolean od_multibutton_draw(GtkWidget *widget, cairo_t *cr)
196 {
197 g_return_val_if_fail(widget != NULL, FALSE);
198 g_return_val_if_fail(cr != NULL, FALSE);
199 g_return_val_if_fail(OD_IS_MULTIBUTTON(widget), FALSE);
200
201 /* First let GtkButton draw, getting the basic button painted. */
202 GTK_WIDGET_CLASS(button_class)->draw(widget, cr);
203 /* Then paint our own modifying graphics on top, to get the dog ear and config border. */
204 od_multibutton_paint_foreground(widget, cr);
205
206 return FALSE;
207 }
208
209 /* (Mouse) button press handler. Switch to page 2 if it's the middle button. */
od_multibutton_button_press_event(GtkWidget * widget,GdkEventButton * evt)210 static gboolean od_multibutton_button_press_event(GtkWidget *widget, GdkEventButton *evt)
211 {
212 ODMultiButton *mb;
213
214 g_return_val_if_fail(widget != NULL, FALSE);
215 g_return_val_if_fail(evt != NULL, FALSE);
216 g_return_val_if_fail(OD_IS_MULTIBUTTON(widget), FALSE);
217
218 if(evt->button >= 3)
219 return FALSE;
220
221 mb = OD_MULTIBUTTON(widget);
222
223 /* Ignore presses of middle button in config mode. Otherwise, don't be picky about page being defined. */
224 if(mb->config)
225 {
226 if(evt->button > 1)
227 return FALSE;
228 }
229 else if(mb->page[evt->button - 1].widget != NULL)
230 {
231 od_multibutton_set_page(widget, evt->button - 1);
232 /* GtkButton only handles button-1-presses, so fool it. :) */
233 evt->button = 1;
234 }
235 else
236 return FALSE;
237 return GTK_WIDGET_CLASS(button_class)->button_press_event(widget, evt);
238 }
239
240 /* (Mouse) button release handler. Doesn't do much. */
od_multibutton_button_release_event(GtkWidget * widget,GdkEventButton * evt)241 static gboolean od_multibutton_button_release_event(GtkWidget *widget, GdkEventButton *evt)
242 {
243 g_return_val_if_fail(widget != NULL, FALSE);
244 g_return_val_if_fail(evt != NULL, FALSE);
245 g_return_val_if_fail(OD_IS_MULTIBUTTON(widget), FALSE);
246
247 if(evt->button > 2)
248 return FALSE;
249
250 /* In config mode, toggle lock on release. */
251 if(OD_MULTIBUTTON(widget)->config)
252 {
253 if(evt->button > 1)
254 return FALSE;
255 OD_MULTIBUTTON(widget)->config_down ^= 1/*GTK_BUTTON(widget)->in_button*/; /*FIXME?*/
256 }
257
258 /* GtkButton wants to believe button 1 was pressed, so let's make it so. */
259 evt->button = 1;
260 GTK_WIDGET_CLASS(button_class)->button_release_event(widget, evt);
261 /* Reset to initial page. */
262 od_multibutton_set_page(widget, 0);
263
264 return FALSE;
265 }
266
267 /* Initialize class. */
od_multibutton_class_init(ODMultiButtonClass * mbc)268 static void od_multibutton_class_init(ODMultiButtonClass *mbc)
269 {
270 GtkWidgetClass *widget = (GtkWidgetClass *) mbc;
271
272 /* It seems common practice to keep a superclass reference around as a global. */
273 button_class = g_type_class_peek_parent(mbc);
274
275 /* Override methods. */
276 widget->draw = od_multibutton_draw;
277 widget->get_preferred_width = od_multibutton_get_preferred_width;
278
279 widget->button_press_event = od_multibutton_button_press_event;
280 widget->button_release_event = od_multibutton_button_release_event;
281 }
282
283 /* Initialize a brand new multibutton instance. */
od_multibutton_init(ODMultiButton * mb)284 static void od_multibutton_init(ODMultiButton *mb)
285 {
286 guint i;
287
288 gtk_widget_set_can_focus(GTK_WIDGET(mb), FALSE);
289 gtk_widget_set_can_default(GTK_WIDGET(mb), FALSE);
290 gtk_widget_set_receives_default(GTK_WIDGET(mb), FALSE);
291
292 for(i = 0; i < sizeof mb->page / sizeof *mb->page; i++)
293 {
294 mb->page[i].widget = NULL;
295 mb->page[i].user = NULL;
296 }
297 mb->width = mb->height = 0;
298 mb->config = FALSE;
299 mb->config_down = FALSE;
300 mb->last_index = 0;
301 }
302
303 /* I like GTK+ 2 constructors. They're short and sweet, and just pass the buck. */
od_multibutton_new(void)304 GtkWidget * od_multibutton_new(void)
305 {
306 return g_object_new(OD_MULTIBUTTON_TYPE, NULL);
307 }
308
309 /* Enable or disable the special togglebutton-like "configuration mode". Used in gentoo's config UI. */
od_multibutton_set_config(ODMultiButton * mb,gboolean config)310 void od_multibutton_set_config(ODMultiButton *mb, gboolean config)
311 {
312 g_return_if_fail(mb != NULL);
313 g_return_if_fail(OD_IS_MULTIBUTTON(mb));
314
315 if(mb->config && !config)
316 od_multibutton_set_config_selected(mb, FALSE);
317 mb->config = config;
318 }
319
320 /* Set the config toggle state. In practice (in gentoo), this is only ever used to deselect a button. */
od_multibutton_set_config_selected(ODMultiButton * mb,gboolean selected)321 void od_multibutton_set_config_selected(ODMultiButton *mb, gboolean selected)
322 {
323 g_return_if_fail(mb != NULL);
324 g_return_if_fail(OD_IS_MULTIBUTTON(mb));
325 g_return_if_fail(OD_MULTIBUTTON(mb)->config);
326
327 if(mb->config && selected != mb->config_down)
328 {
329 mb->config_down = selected;
330 gtk_widget_queue_draw(GTK_WIDGET(mb));
331 }
332 }
333
od_multibutton_remove_widget(ODMultiButton * mb,guint index)334 static void od_multibutton_remove_widget(ODMultiButton *mb, guint index)
335 {
336 GtkWidget *widget;
337
338 g_return_if_fail(mb != NULL);
339 g_return_if_fail(OD_IS_MULTIBUTTON(mb));
340 g_return_if_fail(index > sizeof mb->page / sizeof *mb->page);
341
342 widget = mb->page[index].widget;
343 if(gtk_bin_get_child(GTK_BIN(mb)) == widget)
344 gtk_container_remove(GTK_CONTAINER(mb), widget);
345 else
346 g_object_unref(G_OBJECT(widget));
347 gtk_widget_queue_draw(GTK_WIDGET(mb));
348 }
349
od_multibutton_set_widget(ODMultiButton * mb,guint index,GtkWidget * widget,gpointer user)350 static void od_multibutton_set_widget(ODMultiButton *mb, guint index, GtkWidget *widget, gpointer user)
351 {
352 g_return_if_fail(mb != NULL);
353 g_return_if_fail(OD_IS_MULTIBUTTON(mb));
354 g_return_if_fail(index < sizeof mb->page / sizeof *mb->page);
355
356 if(mb->page[index].widget != widget)
357 {
358 if(mb->page[index].widget != NULL)
359 od_multibutton_remove_widget(mb, index);
360
361 mb->page[index].widget = widget;
362 g_object_ref(G_OBJECT(widget));
363 }
364 mb->page[index].user = user;
365
366 if(index == 0)
367 od_multibutton_set_page(GTK_WIDGET(mb), 0);
368 }
369
370 /* Build a color attribute, suitable for a Pango markup <span> element, of the given name. */
format_color_attribute(gchar * buf,gsize bufsize,const gchar * name,const GdkColor * color)371 static gboolean format_color_attribute(gchar *buf, gsize bufsize, const gchar *name, const GdkColor *color)
372 {
373 if(buf == NULL || name == NULL || color == NULL)
374 return FALSE;
375 return g_snprintf(buf, bufsize, "%s=\"#%02X%02X%02X\"",
376 name, color->red >> 8, color->green >> 8, color->blue >> 8) < bufsize;
377 }
378
379 /* Re-set the label text for the given button's indicated face. This rebuilds the formatting
380 ** markup in the label to accomodate colors, if set.
381 */
od_multibutton_reset_label(const ODMultiButton * mb,guint index,GtkLabel * label,const gchar * text,const GdkColor * bg,const GdkColor * fg)382 static void od_multibutton_reset_label(const ODMultiButton *mb, guint index, GtkLabel *label, const gchar *text, const GdkColor *bg, const GdkColor *fg)
383 {
384 if(bg != NULL || fg != NULL)
385 {
386 gchar tmp[1024], bga[32], fga[32];
387
388 /* Warm up by building attributes. */
389 bga[0] = fga[0] = '\0';
390 if(bg != NULL)
391 format_color_attribute(bga, sizeof bga, " background", bg);
392 if(fg != NULL)
393 format_color_attribute(fga, sizeof fga, " color", fg);
394 /* Then build final label, ignoring any incoming span. Non-used colors are empty. */
395 g_snprintf(tmp, sizeof tmp, "<span%s%s>%s</span>", fga, bga, text);
396 gtk_label_set_markup_with_mnemonic(label, tmp);
397 }
398 else
399 gtk_label_set_text_with_mnemonic(label, text);
400 }
401
402 /* Set the textual content of one of the faces of the button. The userdata is handy when clicked. */
od_multibutton_set_text(ODMultiButton * mb,guint index,const gchar * text,const GdkColor * bg,const GdkColor * fg,gpointer user)403 void od_multibutton_set_text(ODMultiButton *mb, guint index, const gchar *text, const GdkColor *bg, const GdkColor *fg, gpointer user)
404 {
405 GtkWidget *w;
406
407 g_return_if_fail(mb != NULL);
408 g_return_if_fail(OD_IS_MULTIBUTTON(mb));
409 g_return_if_fail(index < sizeof mb->page / sizeof *mb->page);
410
411 /* If there is a widget set, and it's a label: just change it in place. */
412 if((w = mb->page[index].widget) != NULL)
413 {
414 if(GTK_IS_LABEL(w))
415 {
416 od_multibutton_reset_label(mb, index, GTK_LABEL(w), text, bg, fg);
417 if(gtk_widget_get_parent(GTK_WIDGET(mb)))
418 gtk_widget_queue_resize(GTK_WIDGET(mb));
419 if(gtk_widget_is_drawable(GTK_WIDGET(mb)))
420 gtk_widget_queue_draw(GTK_WIDGET(mb));
421 }
422 }
423 else /* If no widget set, create label. */
424 {
425 w = gtk_label_new("");
426 od_multibutton_reset_label(mb, index, GTK_LABEL(w), text, bg, fg);
427 }
428 if(GTK_IS_LABEL(w))
429 {
430 gtk_label_set_xalign(GTK_LABEL(w), 0.5f);
431 gtk_label_set_yalign(GTK_LABEL(w), 0.5f);
432 }
433 od_multibutton_set_widget(mb, index, w, user);
434 }
435
436 /* Returns index of last active page. */
od_multibutton_get_index(const ODMultiButton * mb)437 gint od_multibutton_get_index(const ODMultiButton *mb)
438 {
439 g_return_val_if_fail(mb != NULL, -1);
440 g_return_val_if_fail(OD_IS_MULTIBUTTON(mb), -1);
441
442 return OD_MULTIBUTTON(mb)->last_index;
443 }
444
od_multibutton_get_userdata_last(const ODMultiButton * mb)445 gpointer od_multibutton_get_userdata_last(const ODMultiButton *mb)
446 {
447 g_return_val_if_fail(mb != NULL, NULL);
448 g_return_val_if_fail(OD_IS_MULTIBUTTON(mb), NULL);
449
450 return OD_MULTIBUTTON(mb)->page[OD_MULTIBUTTON(mb)->last_index].user;
451 }
452
453 /* ------------------------------------------------------------------------------------------------------------------------- */
454
455 #if defined ODMULTIBUTTON_STANDALONE
456
evt_clicked(GtkWidget * wid,gpointer user)457 static void evt_clicked(GtkWidget *wid, gpointer user)
458 {
459 g_printf("clicked, index=%d, data=%p\n", od_multibutton_get_index(OD_MULTIBUTTON(wid)),
460 od_multibutton_get_userdata_last(OD_MULTIBUTTON(wid)));
461 }
462
mb_random_color(GtkWidget * wid,unsigned int face)463 static void mb_random_color(GtkWidget *wid, unsigned int face)
464 {
465 GdkColor col;
466
467 col.red = rand();
468 col.green = rand();
469 col.blue = rand();
470 od_multibutton_set_background(OD_MULTIBUTTON(wid), face, &col);
471 }
472
evt_color_clicked(GtkWidget * wid,gpointer user)473 static void evt_color_clicked(GtkWidget *wid, gpointer user)
474 {
475 mb_random_color(user, 0);
476 mb_random_color(user, 1);
477 }
478
evt_clear_clicked(GtkWidget * wid,gpointer user)479 static void evt_clear_clicked(GtkWidget *wid, gpointer user)
480 {
481 od_multibutton_set_config_selected(OD_MULTIBUTTON(user), FALSE);
482 }
483
evt_exit_config_clicked(GtkWidget * wid,gpointer user)484 static void evt_exit_config_clicked(GtkWidget *wid, gpointer user)
485 {
486 od_multibutton_set_config(OD_MULTIBUTTON(user), FALSE);
487 }
488
main(int argc,char * argv[])489 int main(int argc, char *argv[])
490 {
491 GtkWidget *win, *box, *test, *btn;
492 GdkColormap *cmap;
493 guint i;
494
495 gtk_init(&argc, &argv);
496
497 win = gtk_window_new(GTK_WINDOW_TOPLEVEL);
498 box = gtk_hbox_new(FALSE, 0);
499
500 btn = gtk_button_new_with_label("Filler");
501 gtk_box_pack_start(GTK_BOX(box), btn, TRUE, TRUE, 0);
502
503 test = od_multibutton_new();
504 od_multibutton_set_config(OD_MULTIBUTTON(test), TRUE);
505 // gtk_container_set_border_width(GTK_CONTAINER(test), 5);
506 // od_multibutton_set_widget_text(OD_MULTIBUTTON(test), 0, "Achtung", NULL);
507 od_multibutton_set_widget_text(OD_MULTIBUTTON(test), 0, "Delete", NULL);
508 od_multibutton_set_widget_text(OD_MULTIBUTTON(test), 1, "Copy <span color=\"#ff0000\">As</span>", NULL);
509 g_signal_connect(G_OBJECT(test), "clicked", G_CALLBACK(evt_clicked), NULL);
510 gtk_box_pack_start(GTK_BOX(box), test, TRUE, TRUE, 0);
511
512 btn = gtk_button_new_with_label("Random Color");
513 g_signal_connect(G_OBJECT(btn), "clicked", G_CALLBACK(evt_color_clicked), test);
514 gtk_box_pack_start(GTK_BOX(box), btn, TRUE, TRUE, 0);
515
516 btn = gtk_button_new_with_label("Clear Lock");
517 g_signal_connect(G_OBJECT(btn), "clicked", G_CALLBACK(evt_clear_clicked), test);
518 gtk_box_pack_start(GTK_BOX(box), btn, TRUE, TRUE, 0);
519
520 {
521 GtkWidget *btn, *evb, *lab;
522 GdkColor test;
523
524 lab = gtk_label_new("Event Test");
525 evb = gtk_event_box_new();
526 gtk_container_add(GTK_CONTAINER(evb), lab);
527 gdk_color_parse("red", &test);
528 gtk_widget_modify_bg(evb, GTK_STATE_NORMAL, &test);
529 gtk_widget_modify_bg(evb, GTK_STATE_PRELIGHT, &test);
530 gtk_widget_modify_bg(evb, GTK_STATE_ACTIVE, &test);
531 btn = gtk_button_new();
532 gtk_container_add(GTK_CONTAINER(btn), evb);
533 gtk_box_pack_start(GTK_BOX(box), btn, TRUE, TRUE, 0);
534 }
535
536 /* btn = od_multibutton_new();
537 od_multibutton_set_config(OD_MULTIBUTTON(btn), TRUE);
538 od_multibutton_set_widget_text(OD_MULTIBUTTON(btn), 0, "Test Test", NULL);
539 mb_random_colors(btn, 0);
540 gtk_box_pack_start(GTK_BOX(box), btn, TRUE, TRUE, 0);
541 */
542 btn = gtk_button_new_with_label("Exit Config");
543 g_signal_connect(G_OBJECT(btn), "clicked", G_CALLBACK(evt_exit_config_clicked), test);
544 gtk_box_pack_start(GTK_BOX(box), btn, TRUE, TRUE, 0);
545
546 gtk_container_add(GTK_CONTAINER(win), box);
547
548 gtk_widget_show_all(win);
549 gtk_main();
550
551 return EXIT_SUCCESS;
552 }
553
554 #endif /* ODMULTIBUTTON_STANDALONE */
555