1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
2 *
3 * gnome-rr-labeler.c - Utility to label monitors to identify them
4 * while they are being configured.
5 *
6 * Copyright 2008, Novell, Inc.
7 *
8 * This file is part of the Gnome Library.
9 *
10 * The Gnome Library is free software; you can redistribute it and/or
11 * modify it under the terms of the GNU Library General Public License as
12 * published by the Free Software Foundation; either version 2 of the
13 * License, or (at your option) any later version.
14 *
15 * The Gnome Library is distributed in the hope that it will be useful,
16 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
18 * Library General Public License for more details.
19 *
20 * You should have received a copy of the GNU Library General Public
21 * License along with the Gnome Library; see the file COPYING.LIB. If not,
22 * write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
23 * Boston, MA 02110-1301, USA.
24 *
25 * Author: Federico Mena-Quintero <federico@novell.com>
26 */
27
28 #define GNOME_DESKTOP_USE_UNSTABLE_API
29
30 #include <config.h>
31 #include <glib/gi18n-lib.h>
32 #include <gtk/gtk.h>
33
34 #include <X11/Xproto.h>
35 #include <X11/Xlib.h>
36 #include <X11/Xutil.h>
37 #include <X11/Xatom.h>
38 #include <gdk/gdkx.h>
39
40 #include "gnome-rr-labeler.h"
41
42 struct _GnomeRRLabelerPrivate {
43 GnomeRRConfig *config;
44
45 int num_outputs;
46
47 GdkRGBA *palette;
48 GtkWidget **windows;
49
50 GdkScreen *screen;
51 Atom workarea_atom;
52 };
53
54 enum {
55 PROP_0,
56 PROP_CONFIG,
57 PROP_LAST
58 };
59
60 G_DEFINE_TYPE (GnomeRRLabeler, gnome_rr_labeler, G_TYPE_OBJECT);
61
62 static void gnome_rr_labeler_finalize (GObject *object);
63 static void setup_from_config (GnomeRRLabeler *labeler);
64
65 static GdkFilterReturn
screen_xevent_filter(GdkXEvent * xevent,GdkEvent * event,GnomeRRLabeler * labeler)66 screen_xevent_filter (GdkXEvent *xevent,
67 GdkEvent *event,
68 GnomeRRLabeler *labeler)
69 {
70 XEvent *xev;
71
72 xev = (XEvent *) xevent;
73
74 if (xev->type == PropertyNotify &&
75 xev->xproperty.atom == labeler->priv->workarea_atom) {
76 /* update label positions */
77 if (labeler->priv->windows != NULL) {
78 gnome_rr_labeler_hide (labeler);
79 gnome_rr_labeler_show (labeler);
80 }
81 }
82
83 return GDK_FILTER_CONTINUE;
84 }
85
86 static void
gnome_rr_labeler_init(GnomeRRLabeler * labeler)87 gnome_rr_labeler_init (GnomeRRLabeler *labeler)
88 {
89 GdkWindow *gdkwindow;
90
91 labeler->priv = G_TYPE_INSTANCE_GET_PRIVATE (labeler, GNOME_TYPE_RR_LABELER, GnomeRRLabelerPrivate);
92
93 labeler->priv->workarea_atom = XInternAtom (GDK_DISPLAY_XDISPLAY (gdk_display_get_default ()),
94 "_NET_WORKAREA",
95 True);
96
97 labeler->priv->screen = gdk_screen_get_default ();
98 /* code is not really designed to handle multiple screens so *shrug* */
99 gdkwindow = gdk_screen_get_root_window (labeler->priv->screen);
100 gdk_window_add_filter (gdkwindow, (GdkFilterFunc) screen_xevent_filter, labeler);
101 gdk_window_set_events (gdkwindow, gdk_window_get_events (gdkwindow) | GDK_PROPERTY_CHANGE_MASK);
102 }
103
104 static void
gnome_rr_labeler_set_property(GObject * gobject,guint property_id,const GValue * value,GParamSpec * param_spec)105 gnome_rr_labeler_set_property (GObject *gobject, guint property_id, const GValue *value, GParamSpec *param_spec)
106 {
107 GnomeRRLabeler *self = GNOME_RR_LABELER (gobject);
108
109 switch (property_id) {
110 case PROP_CONFIG:
111 self->priv->config = GNOME_RR_CONFIG (g_value_dup_object (value));
112 return;
113 default:
114 G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, property_id, param_spec);
115 }
116 }
117
118 static GObject *
gnome_rr_labeler_constructor(GType type,guint n_construct_properties,GObjectConstructParam * construct_properties)119 gnome_rr_labeler_constructor (GType type, guint n_construct_properties, GObjectConstructParam *construct_properties)
120 {
121 GnomeRRLabeler *self = (GnomeRRLabeler*) G_OBJECT_CLASS (gnome_rr_labeler_parent_class)->constructor (type, n_construct_properties, construct_properties);
122
123 setup_from_config (self);
124
125 return (GObject*) self;
126 }
127
128 static void
gnome_rr_labeler_class_init(GnomeRRLabelerClass * klass)129 gnome_rr_labeler_class_init (GnomeRRLabelerClass *klass)
130 {
131 GObjectClass *object_class;
132
133 g_type_class_add_private (klass, sizeof (GnomeRRLabelerPrivate));
134
135 object_class = (GObjectClass *) klass;
136
137 object_class->set_property = gnome_rr_labeler_set_property;
138 object_class->finalize = gnome_rr_labeler_finalize;
139 object_class->constructor = gnome_rr_labeler_constructor;
140
141 g_object_class_install_property (object_class, PROP_CONFIG, g_param_spec_object ("config",
142 "Configuration",
143 "RandR configuration to label",
144 GNOME_TYPE_RR_CONFIG,
145 G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY |
146 G_PARAM_STATIC_NICK | G_PARAM_STATIC_BLURB));
147 }
148
149 static void
gnome_rr_labeler_finalize(GObject * object)150 gnome_rr_labeler_finalize (GObject *object)
151 {
152 GnomeRRLabeler *labeler;
153 GdkWindow *gdkwindow;
154
155 labeler = GNOME_RR_LABELER (object);
156
157 gdkwindow = gdk_screen_get_root_window (labeler->priv->screen);
158 gdk_window_remove_filter (gdkwindow, (GdkFilterFunc) screen_xevent_filter, labeler);
159
160 if (labeler->priv->config != NULL) {
161 g_object_unref (labeler->priv->config);
162 }
163
164 if (labeler->priv->windows != NULL) {
165 gnome_rr_labeler_hide (labeler);
166 g_free (labeler->priv->windows);
167 }
168
169 g_free (labeler->priv->palette);
170
171 G_OBJECT_CLASS (gnome_rr_labeler_parent_class)->finalize (object);
172 }
173
174 static int
count_outputs(GnomeRRConfig * config)175 count_outputs (GnomeRRConfig *config)
176 {
177 int i;
178 GnomeRROutputInfo **outputs = gnome_rr_config_get_outputs (config);
179
180 for (i = 0; outputs[i] != NULL; i++)
181 ;
182
183 return i;
184 }
185
186 static void
make_palette(GnomeRRLabeler * labeler)187 make_palette (GnomeRRLabeler *labeler)
188 {
189 /* The idea is that we go around an hue color wheel. We want to start
190 * at red, go around to green/etc. and stop at blue --- because magenta
191 * is evil. Eeeeek, no magenta, please!
192 *
193 * Purple would be nice, though. Remember that we are watered down
194 * (i.e. low saturation), so that would be like Like berries with cream.
195 * Mmmmm, berries.
196 */
197 double start_hue;
198 double end_hue;
199 int i;
200
201 g_assert (labeler->priv->num_outputs > 0);
202
203 labeler->priv->palette = g_new (GdkRGBA, labeler->priv->num_outputs);
204
205 start_hue = 0.0; /* red */
206 end_hue = 2.0/3; /* blue */
207
208 for (i = 0; i < labeler->priv->num_outputs; i++) {
209 double h, s, v;
210 double r, g, b;
211
212 h = start_hue + (end_hue - start_hue) / labeler->priv->num_outputs * i;
213 s = 1.0 / 3;
214 v = 1.0;
215
216 gtk_hsv_to_rgb (h, s, v, &r, &g, &b);
217
218 labeler->priv->palette[i].red = r;
219 labeler->priv->palette[i].green = g;
220 labeler->priv->palette[i].blue = b;
221 labeler->priv->palette[i].alpha = 1.0;
222 }
223 }
224
225 static void
rounded_rectangle(cairo_t * cr,gint x,gint y,gint width,gint height,gint x_radius,gint y_radius)226 rounded_rectangle (cairo_t *cr,
227 gint x,
228 gint y,
229 gint width,
230 gint height,
231 gint x_radius,
232 gint y_radius)
233 {
234 gint x1, x2;
235 gint y1, y2;
236 gint xr1, xr2;
237 gint yr1, yr2;
238
239 x1 = x;
240 x2 = x1 + width;
241 y1 = y;
242 y2 = y1 + height;
243
244 x_radius = MIN (x_radius, width / 2.0);
245 y_radius = MIN (y_radius, width / 2.0);
246
247 xr1 = x_radius;
248 xr2 = x_radius / 2.0;
249 yr1 = y_radius;
250 yr2 = y_radius / 2.0;
251
252 cairo_move_to (cr, x1 + xr1, y1);
253 cairo_line_to (cr, x2 - xr1, y1);
254 cairo_curve_to (cr, x2 - xr2, y1, x2, y1 + yr2, x2, y1 + yr1);
255 cairo_line_to (cr, x2, y2 - yr1);
256 cairo_curve_to (cr, x2, y2 - yr2, x2 - xr2, y2, x2 - xr1, y2);
257 cairo_line_to (cr, x1 + xr1, y2);
258 cairo_curve_to (cr, x1 + xr2, y2, x1, y2 - yr2, x1, y2 - yr1);
259 cairo_line_to (cr, x1, y1 + yr1);
260 cairo_curve_to (cr, x1, y1 + yr2, x1 + xr2, y1, x1 + xr1, y1);
261 cairo_close_path (cr);
262 }
263
264 #define LABEL_WINDOW_EDGE_THICKNESS 2
265 #define LABEL_WINDOW_PADDING 12
266 /* Look for panel-corner in:
267 * http://git.gnome.org/browse/gnome-shell/tree/data/theme/gnome-shell.css
268 * to match the corner radius */
269 #define LABEL_CORNER_RADIUS 6 + LABEL_WINDOW_EDGE_THICKNESS
270
271 static void
label_draw_background_and_frame(GtkWidget * widget,cairo_t * cr,gboolean for_shape)272 label_draw_background_and_frame (GtkWidget *widget, cairo_t *cr, gboolean for_shape)
273 {
274 GdkRGBA shape_color = { 0, 0, 0, 1 };
275 GdkRGBA *rgba;
276 GtkAllocation allocation;
277
278 rgba = g_object_get_data (G_OBJECT (widget), "rgba");
279 gtk_widget_get_allocation (widget, &allocation);
280
281 cairo_save (cr);
282 cairo_set_operator (cr, CAIRO_OPERATOR_SOURCE);
283
284 /* edge outline */
285 if (for_shape)
286 gdk_cairo_set_source_rgba (cr, &shape_color);
287 else
288 cairo_set_source_rgba (cr, 0, 0, 0, 0.5);
289
290 rounded_rectangle (cr,
291 LABEL_WINDOW_EDGE_THICKNESS / 2.0,
292 LABEL_WINDOW_EDGE_THICKNESS / 2.0,
293 allocation.width - LABEL_WINDOW_EDGE_THICKNESS,
294 allocation.height - LABEL_WINDOW_EDGE_THICKNESS,
295 LABEL_CORNER_RADIUS, LABEL_CORNER_RADIUS);
296 cairo_set_line_width (cr, LABEL_WINDOW_EDGE_THICKNESS);
297 cairo_stroke (cr);
298
299 /* fill */
300 if (for_shape) {
301 gdk_cairo_set_source_rgba (cr, &shape_color);
302 } else {
303 rgba->alpha = 0.75;
304 gdk_cairo_set_source_rgba (cr, rgba);
305 }
306
307 rounded_rectangle (cr,
308 LABEL_WINDOW_EDGE_THICKNESS,
309 LABEL_WINDOW_EDGE_THICKNESS,
310 allocation.width - LABEL_WINDOW_EDGE_THICKNESS * 2,
311 allocation.height - LABEL_WINDOW_EDGE_THICKNESS * 2,
312 LABEL_CORNER_RADIUS - LABEL_WINDOW_EDGE_THICKNESS / 2.0,
313 LABEL_CORNER_RADIUS - LABEL_WINDOW_EDGE_THICKNESS / 2.0);
314 cairo_fill (cr);
315
316 cairo_restore (cr);
317 }
318
319 static void
maybe_update_shape(GtkWidget * widget)320 maybe_update_shape (GtkWidget *widget)
321 {
322 cairo_t *cr;
323 cairo_surface_t *surface;
324 cairo_region_t *region;
325
326 /* fallback to XShape only for non-composited clients */
327 if (gtk_widget_is_composited (widget)) {
328 gtk_widget_shape_combine_region (widget, NULL);
329 return;
330 }
331
332 surface = gdk_window_create_similar_surface (gtk_widget_get_window (widget),
333 CAIRO_CONTENT_COLOR_ALPHA,
334 gtk_widget_get_allocated_width (widget),
335 gtk_widget_get_allocated_height (widget));
336
337 cr = cairo_create (surface);
338 label_draw_background_and_frame (widget, cr, TRUE);
339 cairo_destroy (cr);
340
341 region = gdk_cairo_region_create_from_surface (surface);
342 gtk_widget_shape_combine_region (widget, region);
343
344 cairo_surface_destroy (surface);
345 cairo_region_destroy (region);
346 }
347
348 static gboolean
label_window_draw_event_cb(GtkWidget * widget,cairo_t * cr,gpointer data)349 label_window_draw_event_cb (GtkWidget *widget, cairo_t *cr, gpointer data)
350 {
351 if (gtk_widget_is_composited (widget)) {
352 /* clear any content */
353 cairo_save (cr);
354 cairo_set_operator (cr, CAIRO_OPERATOR_SOURCE);
355 cairo_set_source_rgba (cr, 0, 0, 0, 0);
356 cairo_paint (cr);
357 cairo_restore (cr);
358 }
359
360 maybe_update_shape (widget);
361 label_draw_background_and_frame (widget, cr, FALSE);
362
363 return FALSE;
364 }
365
366 static void
position_window(GnomeRRLabeler * labeler,GtkWidget * window,int x,int y)367 position_window (GnomeRRLabeler *labeler,
368 GtkWidget *window,
369 int x,
370 int y)
371 {
372 GdkRectangle workarea;
373 GdkRectangle monitor;
374 int monitor_num;
375
376 monitor_num = gdk_screen_get_monitor_at_point (labeler->priv->screen, x, y);
377 gdk_screen_get_monitor_workarea (labeler->priv->screen, monitor_num, &workarea);
378 gdk_screen_get_monitor_geometry (labeler->priv->screen,
379 monitor_num,
380 &monitor);
381 gdk_rectangle_intersect (&monitor, &workarea, &workarea);
382
383 gtk_window_move (GTK_WINDOW (window), workarea.x, workarea.y);
384 }
385
386 static void
label_window_realize_cb(GtkWidget * widget)387 label_window_realize_cb (GtkWidget *widget)
388 {
389 cairo_region_t *region;
390
391 /* make the whole window ignore events */
392 region = cairo_region_create ();
393 gtk_widget_input_shape_combine_region (widget, region);
394 cairo_region_destroy (region);
395
396 maybe_update_shape (widget);
397 }
398
399 static void
label_window_composited_changed_cb(GtkWidget * widget,GnomeRRLabeler * labeler)400 label_window_composited_changed_cb (GtkWidget *widget, GnomeRRLabeler *labeler)
401 {
402 if (gtk_widget_get_realized (widget))
403 maybe_update_shape (widget);
404 }
405
406 static GtkWidget *
create_label_window(GnomeRRLabeler * labeler,GnomeRROutputInfo * output,GdkRGBA * rgba)407 create_label_window (GnomeRRLabeler *labeler, GnomeRROutputInfo *output, GdkRGBA *rgba)
408 {
409 GtkWidget *window;
410 GtkWidget *widget;
411 char *str;
412 const char *display_name;
413 GdkRGBA black = { 0, 0, 0, 1.0 };
414 int x, y;
415 GdkScreen *screen;
416 GdkVisual *visual;
417
418 window = gtk_window_new (GTK_WINDOW_POPUP);
419 gtk_window_set_type_hint (GTK_WINDOW (window), GDK_WINDOW_TYPE_HINT_TOOLTIP);
420 gtk_window_set_resizable (GTK_WINDOW (window), FALSE);
421 gtk_widget_set_app_paintable (window, TRUE);
422 screen = gtk_widget_get_screen (window);
423 visual = gdk_screen_get_rgba_visual (screen);
424
425 if (visual != NULL)
426 gtk_widget_set_visual (window, visual);
427
428 gtk_container_set_border_width (GTK_CONTAINER (window), LABEL_WINDOW_PADDING + LABEL_WINDOW_EDGE_THICKNESS);
429
430 /* This is semi-dangerous. The color is part of the labeler->palette
431 * array. Note that in gnome_rr_labeler_finalize(), we are careful to
432 * free the palette only after we free the windows.
433 */
434 g_object_set_data (G_OBJECT (window), "rgba", rgba);
435
436 g_signal_connect (window, "draw",
437 G_CALLBACK (label_window_draw_event_cb), labeler);
438 g_signal_connect (window, "realize",
439 G_CALLBACK (label_window_realize_cb), labeler);
440 g_signal_connect (window, "composited-changed",
441 G_CALLBACK (label_window_composited_changed_cb), labeler);
442
443 if (gnome_rr_config_get_clone (labeler->priv->config)) {
444 /* Keep this string in sync with gnome-control-center/capplets/display/xrandr-capplet.c:get_display_name() */
445
446 /* Translators: this is the feature where what you see on your
447 * laptop's screen is the same as your external projector.
448 * Here, "Mirrored" is being used as an adjective. For example,
449 * the Spanish translation could be "Pantallas en Espejo".
450 */
451 display_name = _("Mirrored Displays");
452 } else
453 display_name = gnome_rr_output_info_get_display_name (output);
454
455 str = g_strdup_printf ("<b>%s</b>", display_name);
456 widget = gtk_label_new (NULL);
457 gtk_label_set_markup (GTK_LABEL (widget), str);
458 g_free (str);
459
460 /* Make the label explicitly black. We don't want it to follow the
461 * theme's colors, since the label is always shown against a light
462 * pastel background. See bgo#556050
463 */
464 gtk_widget_override_color (widget,
465 gtk_widget_get_state_flags (widget),
466 &black);
467
468 gtk_container_add (GTK_CONTAINER (window), widget);
469
470 /* Should we center this at the top edge of the monitor, instead of using the upper-left corner? */
471 gnome_rr_output_info_get_geometry (output, &x, &y, NULL, NULL);
472 position_window (labeler, window, x, y);
473
474 gtk_widget_show_all (window);
475
476 return window;
477 }
478
479 static void
setup_from_config(GnomeRRLabeler * labeler)480 setup_from_config (GnomeRRLabeler *labeler)
481 {
482 labeler->priv->num_outputs = count_outputs (labeler->priv->config);
483
484 make_palette (labeler);
485
486 gnome_rr_labeler_show (labeler);
487 }
488
489 /**
490 * gnome_rr_labeler_new:
491 * @config: Configuration of the screens to label
492 *
493 * Create a GUI element that will display colored labels on each connected monitor.
494 * This is useful when users are required to identify which monitor is which, e.g. for
495 * for configuring multiple monitors.
496 * The labels will be shown by default, use gnome_rr_labeler_hide to hide them.
497 *
498 * Returns: A new #GnomeRRLabeler
499 */
500 GnomeRRLabeler *
gnome_rr_labeler_new(GnomeRRConfig * config)501 gnome_rr_labeler_new (GnomeRRConfig *config)
502 {
503 g_return_val_if_fail (GNOME_IS_RR_CONFIG (config), NULL);
504
505 return g_object_new (GNOME_TYPE_RR_LABELER, "config", config, NULL);
506 }
507
508 /**
509 * gnome_rr_labeler_show:
510 * @labeler: A #GnomeRRLabeler
511 *
512 * Show the labels.
513 */
514 void
gnome_rr_labeler_show(GnomeRRLabeler * labeler)515 gnome_rr_labeler_show (GnomeRRLabeler *labeler)
516 {
517 int i;
518 gboolean created_window_for_clone;
519 GnomeRROutputInfo **outputs;
520
521 g_return_if_fail (GNOME_IS_RR_LABELER (labeler));
522
523 if (labeler->priv->windows != NULL)
524 return;
525
526 labeler->priv->windows = g_new (GtkWidget *, labeler->priv->num_outputs);
527
528 created_window_for_clone = FALSE;
529
530 outputs = gnome_rr_config_get_outputs (labeler->priv->config);
531
532 for (i = 0; i < labeler->priv->num_outputs; i++) {
533 if (!created_window_for_clone && gnome_rr_output_info_is_active (outputs[i])) {
534 labeler->priv->windows[i] = create_label_window (labeler, outputs[i], labeler->priv->palette + i);
535
536 if (gnome_rr_config_get_clone (labeler->priv->config))
537 created_window_for_clone = TRUE;
538 } else
539 labeler->priv->windows[i] = NULL;
540 }
541 }
542
543 /**
544 * gnome_rr_labeler_hide:
545 * @labeler: A #GnomeRRLabeler
546 *
547 * Hide ouput labels.
548 */
549 void
gnome_rr_labeler_hide(GnomeRRLabeler * labeler)550 gnome_rr_labeler_hide (GnomeRRLabeler *labeler)
551 {
552 int i;
553 GnomeRRLabelerPrivate *priv;
554
555 g_return_if_fail (GNOME_IS_RR_LABELER (labeler));
556
557 priv = labeler->priv;
558
559 if (priv->windows == NULL)
560 return;
561
562 for (i = 0; i < priv->num_outputs; i++)
563 if (priv->windows[i] != NULL) {
564 gtk_widget_destroy (priv->windows[i]);
565 priv->windows[i] = NULL;
566 }
567 g_free (priv->windows);
568 priv->windows = NULL;
569 }
570
571 /**
572 * gnome_rr_labeler_get_rgba_for_output:
573 * @labeler: A #GnomeRRLabeler
574 * @output: Output device (i.e. monitor) to query
575 * @rgba_out: (out): Color of selected monitor.
576 *
577 * Get the color used for the label on a given output (monitor).
578 */
579 void
gnome_rr_labeler_get_rgba_for_output(GnomeRRLabeler * labeler,GnomeRROutputInfo * output,GdkRGBA * rgba_out)580 gnome_rr_labeler_get_rgba_for_output (GnomeRRLabeler *labeler, GnomeRROutputInfo *output, GdkRGBA *rgba_out)
581 {
582 int i;
583 GnomeRROutputInfo **outputs;
584
585 g_return_if_fail (GNOME_IS_RR_LABELER (labeler));
586 g_return_if_fail (GNOME_IS_RR_OUTPUT_INFO (output));
587 g_return_if_fail (rgba_out != NULL);
588
589 outputs = gnome_rr_config_get_outputs (labeler->priv->config);
590
591 for (i = 0; i < labeler->priv->num_outputs; i++)
592 if (outputs[i] == output) {
593 *rgba_out = labeler->priv->palette[i];
594 return;
595 }
596
597 g_warning ("trying to get the color for unknown GnomeOutputInfo %p; returning magenta!", output);
598
599 rgba_out->red = 1.0;
600 rgba_out->green = 0;
601 rgba_out->blue = 1.0;
602 rgba_out->alpha = 1.0;
603 }
604