1 /*
2  * Copyright ?? 2012 Red Hat, Inc.
3  *
4  * Permission to use, copy, modify, distribute, and sell this software
5  * and its documentation for any purpose is hereby granted without
6  * fee, provided that the above copyright notice appear in all copies
7  * and that both that copyright notice and this permission notice
8  * appear in supporting documentation, and that the name of Red Hat
9  * not be used in advertising or publicity pertaining to distribution
10  * of the software without specific, written prior permission.  Red
11  * Hat makes no representations about the suitability of this software
12  * for any purpose.  It is provided "as is" without express or implied
13  * warranty.
14  *
15  * THE AUTHORS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
16  * INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN
17  * NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY SPECIAL, INDIRECT OR
18  * CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
19  * OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
20  * NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
21  * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
22  *
23  * Authors:
24  *        Olivier Fourdan <ofourdan@redhat.com>
25  */
26 
27 #ifdef HAVE_CONFIG_H
28 #include "config.h"
29 #endif
30 
31 #include <stdio.h>
32 #include <stdlib.h>
33 #include <string.h>
34 
35 #include <glib.h>
36 #include <gdk/gdkx.h>
37 #include <gtk/gtk.h>
38 #include <cairo.h>
39 #include <librsvg/rsvg.h>
40 #include "libwacom.h"
41 
42 #ifndef LIBRSVG_CHECK_VERSION
43 #include <librsvg/librsvg-features.h>
44 #endif
45 #if !LIBRSVG_CHECK_VERSION(2,36,2)
46 #include <librsvg/rsvg-cairo.h>
47 #endif
48 
49 #define INACTIVE_COLOR		"#ededed"
50 #define ACTIVE_COLOR		"#729fcf"
51 #define STROKE_COLOR		"#000000"
52 #define DARK_COLOR		"#535353"
53 #define BACK_COLOR		"#000000"
54 
55 /* Convenient struct to store our stuff around */
56 typedef struct
57 {
58 	RsvgHandle  *handle;
59 	GtkWidget   *widget;
60 	guint        timeout;
61 	WacomDevice *device;
62 	GdkRectangle area;
63 	char         active_button;
64 	int          num_buttons;
65 } Tablet;
66 
67 static gboolean
get_sub_location(cairo_t * cairo_context,RsvgHandle * handle,const char * sub,double * x,double * y,double * width,double * height)68 get_sub_location (cairo_t *cairo_context, RsvgHandle *handle, const char *sub, double *x, double *y, double *width, double *height)
69 {
70 	if (x || y) {
71 		RsvgPositionData  position;
72 		double tx, ty;
73 
74 		if (!rsvg_handle_get_position_sub (handle, &position, sub)) {
75 			g_warning ("Failed to retrieve '%s' position", sub);
76 			return FALSE;
77 		}
78 
79 		tx = (double) position.x;
80 		ty = (double) position.y;
81 		cairo_user_to_device (cairo_context, &tx, &ty);
82 
83 		if (x)
84 			*x = tx;
85 		if (y)
86 			*y = ty;
87 	}
88 
89 	if (width || height) {
90 		RsvgDimensionData dimensions;
91 		double twidth, theight;
92 
93 		if (!rsvg_handle_get_dimensions_sub (handle, &dimensions, sub)) {
94 			g_warning ("Failed to retrieve '%s' dimension", sub);
95 			return FALSE;
96 		}
97 
98 		twidth = (double) dimensions.width;
99 		theight = (double) dimensions.height;
100 		cairo_user_to_device_distance (cairo_context, &twidth, &theight);
101 
102 		if (width)
103 			*width = twidth;
104 		if (height)
105 			*height = theight;
106 	}
107 
108 	return TRUE;
109 }
110 
111 static void
print_label(cairo_t * cairo_context,Tablet * tablet,const char * sub,const char * markup,WacomButtonFlags flags)112 print_label (cairo_t *cairo_context, Tablet *tablet, const char *sub, const char *markup, WacomButtonFlags flags)
113 {
114 	GtkWidget        *widget;
115 	GtkAllocation     allocation;
116 	GtkStyle         *style;
117 	PangoContext     *pango_context;
118 	PangoLayout      *pango_layout;
119 	PangoRectangle    pango_rect;
120 	double            label_x, label_y;
121 	int               x, y;
122 
123 	if (!get_sub_location (cairo_context, tablet->handle, sub, &label_x, &label_y, NULL, NULL))
124 		return;
125 
126 	widget = GTK_WIDGET(tablet->widget);
127 	gtk_widget_get_allocation(widget, &allocation);
128 	style = gtk_widget_get_style (widget);
129 	pango_context = gtk_widget_get_pango_context (widget);
130 	pango_layout  = pango_layout_new (pango_context);
131 
132 	pango_layout_set_markup (pango_layout, markup, -1);
133 	pango_layout_get_pixel_extents (pango_layout, NULL, &pango_rect);
134 
135 	if (flags & WACOM_BUTTON_POSITION_LEFT) {
136 		pango_layout_set_alignment (pango_layout, PANGO_ALIGN_LEFT);
137 		x = (int) label_x + pango_rect.x;
138 		y = (int) label_y + pango_rect.y - pango_rect.height / 2;
139 	} else if (flags & WACOM_BUTTON_POSITION_RIGHT) {
140 		pango_layout_set_alignment (pango_layout, PANGO_ALIGN_RIGHT);
141 		x = (int) label_x + pango_rect.x - pango_rect.width;
142 		y = (int) label_y + pango_rect.y - pango_rect.height / 2;
143 	} else {
144 		pango_layout_set_alignment (pango_layout, PANGO_ALIGN_CENTER);
145 		x = (int) label_x + pango_rect.x - pango_rect.width / 2;
146 		y = (int) label_y + pango_rect.y;
147 	}
148 
149 	gtk_paint_layout (style,
150 		          gtk_widget_get_window (widget),
151 		          0,
152 		          TRUE,
153 			  &allocation,
154 		          widget,
155 		          NULL,
156 		          x,
157 		          y,
158 		          pango_layout);
159 
160 	g_object_unref (pango_layout);
161 }
162 
163 static void
print_button_labels(cairo_t * cairo_context,Tablet * tablet)164 print_button_labels (cairo_t *cairo_context, Tablet *tablet)
165 {
166 	char button;
167 
168 	for (button = 'A'; button < 'A' + tablet->num_buttons; button++) {
169 		WacomButtonFlags  flags;
170 		gchar            *sub;
171 		gchar            *label;
172 
173 		flags = libwacom_get_button_flag(tablet->device, button);
174 		sub = g_strdup_printf ("#Label%c", button);
175 		label = g_strdup_printf ("<span foreground=\"%s\" >Button %c</span>",
176 		                         (button == tablet->active_button) ? ACTIVE_COLOR : INACTIVE_COLOR,
177 		                         button);
178 		print_label (cairo_context, tablet, sub, label, flags);
179 		g_free (label);
180 		g_free (sub);
181 	}
182 
183 	/* Touch rings */
184 	if (libwacom_has_ring(tablet->device)) {
185 		print_label (cairo_context, tablet, "#LabelRingCCW", "<span foreground=\"" INACTIVE_COLOR "\" >Ring Counter Clockwise</span>", WACOM_BUTTON_POSITION_LEFT);
186 		print_label (cairo_context, tablet, "#LabelRingCW", "<span foreground=\"" INACTIVE_COLOR "\" >Ring Clockwise</span>", WACOM_BUTTON_POSITION_LEFT);
187 	}
188 	if (libwacom_has_ring2(tablet->device)) {
189 		print_label (cairo_context, tablet, "#LabelRing2CCW", "<span foreground=\"" INACTIVE_COLOR "\" >2nd Ring Counter Clockwise</span>", WACOM_BUTTON_POSITION_RIGHT);
190 		print_label (cairo_context, tablet, "#LabelRing2CW", "<span foreground=\"" INACTIVE_COLOR "\" >2nd Ring Clockwise</span>", WACOM_BUTTON_POSITION_RIGHT);
191 	}
192 	/* Touch strips */
193 	if (libwacom_get_num_strips(tablet->device) > 0) {
194 		print_label (cairo_context, tablet, "#LabelStripUp", "<span foreground=\"" INACTIVE_COLOR "\" >Strip Up</span>", WACOM_BUTTON_POSITION_LEFT);
195 		print_label (cairo_context, tablet, "#LabelStripDown", "<span foreground=\"" INACTIVE_COLOR "\" >Strip Down</span>", WACOM_BUTTON_POSITION_LEFT);
196 	}
197 	if (libwacom_get_num_strips(tablet->device) > 1) {
198 		print_label (cairo_context, tablet, "#LabelStrip2Up", "<span foreground=\"" INACTIVE_COLOR "\" >2nd Strip Up</span>", WACOM_BUTTON_POSITION_RIGHT);
199 		print_label (cairo_context, tablet, "#LabelStrip2Down", "<span foreground=\"" INACTIVE_COLOR "\" >2nd Strip Down</span>", WACOM_BUTTON_POSITION_RIGHT);
200 	}
201 }
202 
203 static void
update_tablet(Tablet * tablet)204 update_tablet (Tablet *tablet)
205 {
206 	char        *width, *height;
207 	char         button;
208 	GError      *error;
209 	gchar       *file_data, *escaped_file_data, *data;
210 	gsize        file_len;
211 
212 	error = NULL;
213 
214 	if (tablet->handle)
215 		g_object_unref (tablet->handle);
216 
217 	if (!g_file_get_contents (libwacom_get_layout_filename (tablet->device), &file_data, &file_len, NULL))
218 		return;
219 	escaped_file_data = g_base64_encode ((guchar*) file_data, file_len);
220 
221 	width = g_strdup_printf ("%d", tablet->area.width);
222 	height = g_strdup_printf ("%d", tablet->area.height);
223 
224 	data = g_strconcat ("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n"
225 	                    "<svg version=\"1.1\"\n"
226 	                    "     xmlns=\"http://www.w3.org/2000/svg\"\n"
227 	                    "     xmlns:xi=\"http://www.w3.org/2001/XInclude\"\n"
228 	                    "     width=\"", width, "\"\n"
229 	                    "     height=\"", height, "\">\n"
230 	                    "  <style type=\"text/css\">\n",
231 	                    "    .Leader {\n"
232 	                    "      stroke-width: .5 !important;\n"
233 	                    "      stroke: ", INACTIVE_COLOR, ";\n"
234 	                    "      fill:    none !important;\n"
235 	                    "    }\n",
236 	                    "    .Button {\n"
237 	                    "      stroke-width: 0.25;\n"
238 	                    "      stroke: ", INACTIVE_COLOR, ";\n"
239 	                    "      fill:   ", INACTIVE_COLOR, ";\n"
240 	                    "    }\n",
241 	                    NULL);
242 	g_free (width);
243 	g_free (height);
244 
245 	for (button = 'A'; button < 'A' + tablet->num_buttons; button++) {
246 		gchar class[] = {button, '\0'};
247 		if (button == tablet->active_button) {
248 			data = g_strconcat (data,
249 			                    "    .", class, " {\n"
250 			                    "      stroke: ", ACTIVE_COLOR, " !important;\n"
251 			                    "      fill:   ", ACTIVE_COLOR, " !important;\n"
252 			                    "    }\n",
253 			                    NULL);
254 	    }
255 	}
256 
257 	data = g_strconcat (data,
258 	                    "    .Leader {\n"
259 	                    "      fill:    none !important;\n"
260 	                    "    }\n",
261 	                    "    .Label {\n"
262 	                    "      stroke: none      !important;\n"
263 	                    "      stroke-width: 0.1 !important;\n"
264 	                    "      opacity:   .0     !important;\n"
265 	                    "      font-size: .1     !important;\n"
266 	                    "      fill:   ", BACK_COLOR,    " !important;\n"
267 	                    "    }\n",
268 	                    "    .TouchStrip,.TouchRing {\n"
269 	                    "      stroke-width: 0.1   !important;\n"
270 	                    "      stroke: ", INACTIVE_COLOR," !important;\n"
271 	                    "      fill:   ", DARK_COLOR,    " !important;\n"
272 	                    "    }\n",
273 	                    "  </style>\n"
274 	                    "  <xi:include href=\"data:text/xml;base64,", escaped_file_data, "\"/>\n"
275 	                    "</svg>",
276 	                    NULL);
277 
278 	tablet->handle = rsvg_handle_new_from_data ((guint8 *) data, strlen(data), &error);
279 	g_free (data);
280 	g_free (escaped_file_data);
281 	g_free (file_data);
282 }
283 
284 static void
on_expose_cb(GtkWidget * widget,GdkEvent * event,Tablet * tablet)285 on_expose_cb (GtkWidget *widget, GdkEvent *event, Tablet *tablet)
286 {
287 	GtkAllocation  allocation;
288 	cairo_t       *cairo_context;
289 	float          scale;
290 	double         twidth, theight;
291 
292 	if (tablet->handle == NULL)
293 		update_tablet (tablet);
294 
295 	/* Create a Cairo for the widget */
296 	cairo_context = gdk_cairo_create (gtk_widget_get_window (widget));
297 	cairo_set_operator (cairo_context, CAIRO_OPERATOR_CLEAR);
298 	cairo_paint (cairo_context);
299 	cairo_set_operator (cairo_context, CAIRO_OPERATOR_OVER);
300 
301 	/* Scale to fit in window */
302 	gtk_widget_get_allocation(widget, &allocation);
303 	scale = MIN ((float) allocation.width / tablet->area.width,
304 	             (float) allocation.height / tablet->area.height);
305 	cairo_scale (cairo_context, scale, scale);
306 
307 	/* Center the result in window */
308 	twidth = (double) tablet->area.width;
309 	theight = (double) tablet->area.height;
310 	cairo_user_to_device_distance (cairo_context, &twidth, &theight);
311 	twidth = ((double) allocation.width - twidth) / 2.0;
312 	theight = ((double) allocation.height - theight) / 2.0;
313 	cairo_device_to_user_distance (cairo_context, &twidth, &theight);
314 	cairo_translate (cairo_context, twidth, theight);
315 
316 	/* And render the tablet layout */
317 	rsvg_handle_render_cairo (tablet->handle, cairo_context);
318 	print_button_labels (cairo_context, tablet);
319 	cairo_destroy (cairo_context);
320 }
321 
322 static gboolean
on_timer_cb(Tablet * tablet)323 on_timer_cb (Tablet *tablet)
324 {
325 	GtkAllocation allocation;
326 	int           num_buttons;
327 
328 	tablet->active_button++;
329 	num_buttons = libwacom_get_num_buttons (tablet->device);
330 	if (tablet->active_button >= 'A' + num_buttons)
331 		tablet->active_button = 'A';
332 	update_tablet (tablet);
333 	gtk_widget_get_allocation (GTK_WIDGET(tablet->widget), &allocation);
334 	gdk_window_invalidate_rect(gtk_widget_get_window (tablet->widget), &allocation, FALSE);
335 
336 	return TRUE;
337 }
338 
339 static gboolean
on_delete_cb(GtkWidget * widget,GdkEvent * event,Tablet * tablet)340 on_delete_cb (GtkWidget *widget, GdkEvent  *event, Tablet *tablet)
341 {
342 	gtk_main_quit ();
343 
344 	return TRUE;
345 }
346 
347 int
main(int argc,char ** argv)348 main (int argc, char **argv)
349 {
350 	GOptionContext      *context;
351 	RsvgHandle          *handle;
352 	RsvgDimensionData    dimensions;
353 	GError              *error;
354 	WacomDeviceDatabase *db;
355 	WacomDevice         *device;
356 	Tablet              *tablet;
357 	GdkWindow           *gdk_win;
358 	GdkColor             black;
359 	char                *tabletname;
360 	const char          *filename;
361 	GOptionEntry
362 		options[] = {
363 			{"tablet", 't', 0, G_OPTION_ARG_STRING, &tabletname, "Name of the tablet to show", "<string>"},
364 			{NULL}
365 		};
366 
367 	handle = NULL;
368 	error = NULL;
369 	tabletname = NULL;
370 
371 	context = g_option_context_new ("- libwacom tablet viewer");
372 	g_option_context_add_main_entries (context, options, NULL);
373 	g_option_context_add_group (context, gtk_get_option_group (TRUE));
374 	g_option_context_set_help_enabled (context, TRUE);
375 	g_option_context_parse (context, &argc, &argv, NULL);
376 	g_option_context_free (context);
377 
378 	gtk_init (&argc, &argv);
379 	if (tabletname == NULL) {
380 		g_warning ("No tablet name provided, exiting");
381 		return 1;
382 	}
383 	db = libwacom_database_new_for_path(TOPSRCDIR"/data");
384 	if (!db) {
385 		g_warning ("Failed to load libwacom database, exiting");
386 		return 1;
387 	}
388 	device = libwacom_new_from_name(db, tabletname, NULL);
389 	if (!device) {
390 		g_warning ("Device '%s' not found in libwacom database, exiting", tabletname);
391 		return 1;
392 	}
393 
394 	filename = libwacom_get_layout_filename(device);
395 	if (filename == NULL) {
396 		g_warning ("Device '%s' has no layout available, exiting", tabletname);
397 		return 1;
398 	}
399 	handle = rsvg_handle_new_from_file (filename, &error);
400 	if (error || handle == NULL)
401 		return 1;
402 	rsvg_handle_get_dimensions (handle, &dimensions);
403 	g_object_unref (handle);
404 
405 	tablet = g_new0 (Tablet, 1);
406 	tablet->device = device;
407 	tablet->area.width = dimensions.width;
408 	tablet->area.height = dimensions.height;
409 	tablet->handle = NULL;
410 	tablet->active_button = 'A';
411 	tablet->num_buttons = libwacom_get_num_buttons (device);
412 	tablet->widget = gtk_window_new (GTK_WINDOW_TOPLEVEL);
413 
414 	gtk_widget_set_app_paintable (tablet->widget, TRUE);
415 	gtk_widget_realize (tablet->widget);
416 	gdk_win = gtk_widget_get_window (tablet->widget);
417 	gdk_color_parse (BACK_COLOR, &black);
418 	gdk_window_set_background (gdk_win, &black);
419 	gtk_window_set_default_size (GTK_WINDOW (tablet->widget), 800, 600);
420 	gtk_window_set_title (GTK_WINDOW (tablet->widget), libwacom_get_name (device));
421 
422 	g_signal_connect (tablet->widget, "expose-event", G_CALLBACK(on_expose_cb), tablet);
423 	g_signal_connect (tablet->widget, "delete-event", G_CALLBACK(on_delete_cb), tablet);
424 	tablet->timeout = g_timeout_add(750 /* ms */, (GSourceFunc) on_timer_cb, tablet);
425 
426 	gtk_widget_show (tablet->widget);
427 
428 	gtk_main();
429 
430 	libwacom_destroy(device);
431 	libwacom_database_destroy(db);
432 
433 	return 0;
434 }
435