1 /** @file
2 * @brief Extended input devices dialog
3 */
4 /* Authors:
5 * Nicklas Lindgren <nili@lysator.liu.se>
6 * Johan Engelen <goejendaagh@zonnet.nl>
7 *
8 * Copyright (C) 2005-2006 Authors
9 *
10 * Released under GNU GPL, read the file 'COPYING' for more information
11 */
13 #ifdef HAVE_CONFIG_H
14 # include <config.h>
15 #endif
17 #include <gtk/gtksignal.h>
18 #include <gtk/gtkinputdialog.h>
19 #include <glibmm/ustring.h>
20 #include <list>
21 #include <set>
23 #include "macros.h"
24 #include "verbs.h"
25 #include "inkscape.h"
26 #include "interface.h"
27 #include "xml/repr.h"
29 #include "dialogs/dialog-events.h"
30 #include "preferences.h"
32 #define MIN_ONSCREEN_DISTANCE 50
34 static GtkWidget *dlg = NULL;
35 static win_data wd;
37 // impossible original values to make sure they are read from prefs
38 static gint x = -1000, y = -1000, w = 0, h = 0;
39 static Glib::ustring const prefs_path = "/dialogs/input/";
41 #define noTEST_WITH_GOOD_TABLET 1
42 #define noTEST_WITH_BAD_TABLET 1
44 #if defined(TEST_WITH_GOOD_TABLET) || defined(TEST_WITH_BAD_TABLET)
45 static int testDeviceCount = 0;
46 static GdkDevice* testDevices = 0;
48 // Defined at the end of the file to keep debugging out of the way.
49 static void initTestDevices();
50 #endif
52 static std::list<GdkDevice *> getInputDevices()
53 {
54 std::list<GdkDevice*> devices;
56 #if defined(TEST_WITH_GOOD_TABLET) || defined(TEST_WITH_BAD_TABLET)
57 initTestDevices();
58 for (int i = 0; i < testDeviceCount; i++) {
59 devices.push_back(&testDevices[i]);
60 }
61 #else
62 for (GList *ptr = gdk_devices_list(); ptr; ptr = ptr->next) {
63 GdkDevice *device = static_cast<GdkDevice *>(ptr->data);
64 devices.push_back(device);
65 }
66 #endif
68 return devices;
69 }
71 // wrap these GDK calls to be able to intercept for testing.
73 static bool setDeviceMode( GdkDevice *device, GdkInputMode mode )
74 {
75 #if defined(TEST_WITH_GOOD_TABLET) || defined(TEST_WITH_BAD_TABLET)
76 (void)device;
77 (void)mode;
78 bool retVal = true; // Can't let the Gdk call be called with bad data
79 #else
80 bool retVal = gdk_device_set_mode(device, mode);
81 #endif
82 return retVal;
83 }
85 static void setDeviceAxisUse( GdkDevice *device, guint index, GdkAxisUse use )
86 {
87 #if defined(TEST_WITH_GOOD_TABLET) && !defined(TEST_WITH_BAD_TABLET)
88 (void)device;
89 (void)index;
90 (void)use;
91 #else
92 gdk_device_set_axis_use(device, index, use);
93 #endif
94 }
96 static void setDeviceKey( GdkDevice* device, guint index, guint keyval, GdkModifierType modifiers )
97 {
98 #if defined(TEST_WITH_GOOD_TABLET) && !defined(TEST_WITH_BAD_TABLET)
99 (void)device;
100 (void)index;
101 (void)keyval;
102 (void)modifiers;
103 #else
104 gdk_device_set_key(device, index, keyval, modifiers);
105 #endif
106 }
109 static void
110 sp_input_dialog_destroy (GtkObject */*object*/, gpointer /*data*/)
111 {
112 sp_signal_disconnect_by_data (INKSCAPE, dlg);
113 wd.win = dlg = NULL;
114 wd.stop = 0;
115 }
117 static gboolean
118 sp_input_dialog_delete (GtkObject */*object*/, GdkEvent */*event*/, gpointer /*data*/)
119 {
120 Inkscape::Preferences *prefs = Inkscape::Preferences::get();
121 gtk_window_get_position ((GtkWindow *) dlg, &x, &y);
122 gtk_window_get_size ((GtkWindow *) dlg, &w, &h);
124 if (x<0) x=0;
125 if (y<0) y=0;
127 prefs->setInt(prefs_path + "x", x);
128 prefs->setInt(prefs_path + "y", y);
129 prefs->setInt(prefs_path + "w", w);
130 prefs->setInt(prefs_path + "h", h);
132 return FALSE; // which means, go ahead and destroy it
134 }
136 static gchar const *axis_use_strings[GDK_AXIS_LAST] = {
137 "ignore", "x", "y", "pressure", "xtilt", "ytilt", "wheel"
138 };
140 static const int RUNAWAY_MAX = 1000;
142 static Glib::ustring getBaseDeviceName(GdkInputSource source)
143 {
144 Glib::ustring name;
145 switch (source) {
146 case GDK_SOURCE_MOUSE:
147 name ="pointer";
148 break;
149 case GDK_SOURCE_PEN:
150 name ="pen";
151 break;
152 case GDK_SOURCE_ERASER:
153 name ="eraser";
154 break;
155 case GDK_SOURCE_CURSOR:
156 name ="cursor";
157 break;
158 default:
159 name = "tablet";
160 }
161 return name;
162 }
164 static Glib::ustring createSanitizedPath(GdkDevice* device, std::set<Glib::ustring> &seenPaths)
165 {
166 // LP #334800: tablet device names on Windows sometimes contain funny junk like
167 // \x03, \xf2, etc. Moreover this junk changes between runs.
168 // If the tablet name contains unprintable or non-ASCII characters,
169 // we use some default name.
170 // This might break if someone has two tablets with broken names, but it's
171 // not possible to do anything 100% correct then.
172 bool broken = false;
174 if (!device->name || (*(device->name) == 0)) {
175 broken = true;
176 } else {
177 for (gchar const *s = device->name; *s; ++s) {
178 if ((*s < 0x20) || (*s >= 0x7f)) {
179 broken = true;
180 break;
181 }
182 }
183 }
185 Glib::ustring device_path;
186 if (broken) {
187 Glib::ustring base = Glib::ustring("/devices/") + getBaseDeviceName(device->source);
188 int num = 1;
189 device_path = base;
190 while ((seenPaths.find(device_path) != seenPaths.end()) && (num < RUNAWAY_MAX)) {
191 device_path = Glib::ustring::compose("%1%2", base, ++num);
192 }
193 } else {
194 device_path += Glib::ustring("/devices/") + device->name;
195 }
197 seenPaths.insert(device_path);
199 return device_path;
200 }
202 void sp_input_load_from_preferences(void)
203 {
204 Inkscape::Preferences *prefs = Inkscape::Preferences::get();
206 std::list<GdkDevice *> devices = getInputDevices();
207 std::set<Glib::ustring> seenPaths;
208 for (std::list<GdkDevice *>::iterator it = devices.begin(); it != devices.end(); ++it) {
209 GdkDevice *device = *it;
211 // g_message(" s:%d m:%d hc:%d a:%d k:%d [%s]", device->source, device->mode, device->has_cursor, device->num_axes, device->num_keys, device->name);
212 // for (int i = 0; i < device->num_axes; i++) {
213 // GdkDeviceAxis &axis = device->axes[i];
214 // g_message(" axis[%d] u:%d min:%f max:%f", i, axis.use, axis.min, axis.max);
215 // }
217 Glib::ustring device_path = createSanitizedPath(device, seenPaths);
218 // if (device_path != (Glib::ustring("/devices/") + device->name)) {
219 // g_message(" re-name [%s]", device_path.c_str());
220 // }
222 Glib::ustring device_mode = prefs->getString(device_path + "/mode");
224 GdkInputMode mode = GDK_MODE_DISABLED;
225 if (device_mode == "screen") {
226 mode = GDK_MODE_SCREEN;
227 } else if (device_mode == "window") {
228 mode = GDK_MODE_WINDOW;
229 }
231 if (device->mode != mode) {
232 setDeviceMode(device, mode);
233 }
235 Glib::ustring::size_type pos0, pos1;
236 GdkAxisUse axis_use;
238 //temp_ptr = repr->attribute("axes");
239 Glib::ustring const axes_str = prefs->getString(device_path + "/axes");
240 pos0 = pos1 = 0;
241 for (gint i=0; i < device->num_axes; i++) {
242 pos1 = axes_str.find(';', pos0);
243 if (pos1 == Glib::ustring::npos) {
244 break; // Too few axis specifications
245 }
247 axis_use = GDK_AXIS_IGNORE;
248 for (gint j=0; j < GDK_AXIS_LAST; j++) {
249 if (!strcmp(axes_str.substr(pos0, pos1-pos0).c_str(), axis_use_strings[j])) {
250 axis_use = static_cast<GdkAxisUse>(j);
251 break;
252 }
253 }
254 setDeviceAxisUse(device, i, axis_use);
255 pos0 = pos1 + 1;
256 }
258 guint keyval;
259 GdkModifierType modifier;
261 Glib::ustring const keys_str = prefs->getString(device_path + "/keys");
262 pos0 = pos1 = 0;
263 for (gint i=0; i < device->num_keys; i++) {
264 pos1 = keys_str.find(';', pos0);
265 if (pos1 == Glib::ustring::npos) {
266 break; // Too few key specifications
267 }
269 gtk_accelerator_parse(keys_str.substr(pos0, pos1-pos0).c_str(), &keyval, &modifier);
270 setDeviceKey(device, i, keyval, modifier);
271 pos0 = pos1 + 1;
272 }
273 }
274 }
276 void sp_input_save_to_preferences(void)
277 {
278 Inkscape::Preferences *prefs = Inkscape::Preferences::get();
280 std::list<GdkDevice *> devices = getInputDevices();
281 std::set<Glib::ustring> seenPaths;
282 for (std::list<GdkDevice *>::iterator it = devices.begin(); it != devices.end(); ++it) {
283 GdkDevice *device = *it;
285 Glib::ustring device_path = createSanitizedPath(device, seenPaths);
287 switch (device->mode) {
288 default:
289 case GDK_MODE_DISABLED: {
290 prefs->setString(device_path + "/mode", "disabled");
291 break;
292 }
293 case GDK_MODE_SCREEN: {
294 prefs->setString(device_path + "/mode", "screen");
295 break;
296 }
297 case GDK_MODE_WINDOW: {
298 prefs->setString(device_path + "/mode", "window");
299 break;
300 }
301 }
303 Glib::ustring temp_attribute = "";
304 for (gint i=0; i < device->num_axes; i++) {
305 temp_attribute += axis_use_strings[device->axes[i].use];
306 temp_attribute += ";";
307 }
308 prefs->setString(device_path + "/axes", temp_attribute);
310 temp_attribute = "";
311 for (gint i=0; i < device->num_keys; i++) {
312 temp_attribute += gtk_accelerator_name(device->keys[i].keyval, device->keys[i].modifiers);
313 temp_attribute += ";";
314 }
315 prefs->setString(device_path + "/keys", temp_attribute);
316 }
317 }
319 static void
320 sp_input_save_button (GtkObject */*object*/, gpointer /*data*/)
321 {
322 sp_input_save_to_preferences();
323 }
325 void
326 sp_input_dialog (void)
327 {
328 if (dlg == NULL) {
329 Inkscape::Preferences *prefs = Inkscape::Preferences::get();
331 gchar title[500];
332 sp_ui_dialog_title_string (Inkscape::Verb::get(SP_VERB_DIALOG_INPUT), title);
334 dlg = gtk_input_dialog_new();
336 if (x == -1000 || y == -1000) {
337 x = prefs->getInt(prefs_path + "x", -1000);
338 y = prefs->getInt(prefs_path + "y", -1000);
339 }
341 if (w ==0 || h == 0) {
342 w = prefs->getInt(prefs_path + "w", 0);
343 h = prefs->getInt(prefs_path + "h", 0);
344 }
346 // if (x<0) x=0;
347 // if (y<0) y=0;
349 if (w && h) {
350 gtk_window_resize ((GtkWindow *) dlg, w, h);
351 }
352 if (x >= 0 && y >= 0 && (x < (gdk_screen_width()-MIN_ONSCREEN_DISTANCE)) && (y < (gdk_screen_height()-MIN_ONSCREEN_DISTANCE))) {
353 gtk_window_move ((GtkWindow *) dlg, x, y);
354 } else {
355 gtk_window_set_position(GTK_WINDOW(dlg), GTK_WIN_POS_CENTER);
356 }
359 sp_transientize (dlg);
360 wd.win = dlg;
361 wd.stop = 0;
363 g_signal_connect ( G_OBJECT (INKSCAPE), "activate_desktop", G_CALLBACK (sp_transientize_callback), &wd);
364 gtk_signal_connect ( GTK_OBJECT (dlg), "event", GTK_SIGNAL_FUNC (sp_dialog_event_handler), dlg);
365 gtk_signal_connect ( GTK_OBJECT (dlg), "destroy", G_CALLBACK (sp_input_dialog_destroy), dlg);
366 gtk_signal_connect ( GTK_OBJECT (dlg), "delete_event", G_CALLBACK (sp_input_dialog_delete), dlg);
367 g_signal_connect ( G_OBJECT (INKSCAPE), "shut_down", G_CALLBACK (sp_input_dialog_delete), dlg);
368 g_signal_connect ( G_OBJECT (INKSCAPE), "dialogs_hide", G_CALLBACK (sp_dialog_hide), dlg);
369 g_signal_connect ( G_OBJECT (INKSCAPE), "dialogs_unhide", G_CALLBACK (sp_dialog_unhide), dlg);
371 // Dialog-specific stuff
372 gtk_signal_connect_object (GTK_OBJECT(GTK_INPUT_DIALOG(dlg)->close_button),
373 "clicked",
374 (GtkSignalFunc)gtk_widget_destroy,
375 GTK_OBJECT(dlg));
376 gtk_signal_connect (GTK_OBJECT(GTK_INPUT_DIALOG(dlg)->save_button),
377 "clicked",
378 (GtkSignalFunc)sp_input_save_button, NULL);
379 }
381 gtk_window_present ((GtkWindow *) dlg);
382 }
384 // /////////////////////////////////
385 // For debugging:
386 // /////////////////////////////////
389 #if defined(TEST_WITH_GOOD_TABLET)
390 static void initTestDevices()
391 {
392 static bool init = false;
393 if (!init) {
394 static GdkDevice devs[5] = {};
395 int i = 0; // use variable instead of constant to allow devices to be moved around, commented out, etc.
397 {
398 // Laptop trackpad
399 devs[i].name = g_strdup("pointer");
400 devs[i].source = GDK_SOURCE_MOUSE;
401 devs[i].mode = GDK_MODE_DISABLED;
402 devs[i].has_cursor = 0;
403 static GdkDeviceAxis tmp[] = {{GDK_AXIS_X, 0, 0},
404 {GDK_AXIS_Y, 0, 0}};
405 devs[i].num_axes = G_N_ELEMENTS(tmp);
406 devs[i].axes = tmp;
407 devs[i].num_keys = 0;
408 devs[i].keys = 0;
409 i++;
410 }
412 {
413 // Tablet stylus
414 devs[i].name = g_strdup("pen");
415 devs[i].source = GDK_SOURCE_PEN;
416 devs[i].mode = GDK_MODE_DISABLED;
417 devs[i].has_cursor = 0;
418 static GdkDeviceAxis tmp[] = {{GDK_AXIS_X, 0, 0},
419 {GDK_AXIS_Y, 0, 0},
420 {GDK_AXIS_PRESSURE, 0, 1},
421 {GDK_AXIS_XTILT, -1, 1},
422 {GDK_AXIS_YTILT, -1, 1}};
423 devs[i].num_axes = G_N_ELEMENTS(tmp);
424 devs[i].axes = tmp;
425 devs[i].num_keys = 0;
426 devs[i].keys = 0;
427 i++;
428 }
430 {
431 // Puck
432 devs[i].name = g_strdup("cursor");
433 devs[i].source = GDK_SOURCE_CURSOR;
434 devs[i].mode = GDK_MODE_DISABLED;
435 devs[i].has_cursor = 0;
436 static GdkDeviceAxis tmp[] = {{GDK_AXIS_X, 0, 0},
437 {GDK_AXIS_Y, 0, 0},
438 {GDK_AXIS_PRESSURE, 0, 1},
439 {GDK_AXIS_XTILT, -1, 1},
440 {GDK_AXIS_YTILT, -1, 1}};
441 devs[i].num_axes = G_N_ELEMENTS(tmp);
442 devs[i].axes = tmp;
443 devs[i].num_keys = 0;
444 devs[i].keys = 0;
445 i++;
446 }
448 {
449 // Back of tablet stylus
450 devs[i].name = g_strdup("eraser");
451 devs[i].source = GDK_SOURCE_ERASER;
452 devs[i].mode = GDK_MODE_DISABLED;
453 devs[i].has_cursor = 0;
454 static GdkDeviceAxis tmp[] = {{GDK_AXIS_X, 0, 0},
455 {GDK_AXIS_Y, 0, 0},
456 {GDK_AXIS_PRESSURE, 0, 1},
457 {GDK_AXIS_XTILT, -1, 1},
458 {GDK_AXIS_YTILT, -1, 1}};
459 devs[i].num_axes = G_N_ELEMENTS(tmp);
460 devs[i].axes = tmp;
461 devs[i].num_keys = 0;
462 devs[i].keys = 0;
463 i++;
464 }
466 {
467 // Main (composit) mouse device
468 devs[i].name = g_strdup("Core Pointer");
469 devs[i].source = GDK_SOURCE_MOUSE;
470 devs[i].mode = GDK_MODE_SCREEN;
471 devs[i].has_cursor = 1;
472 static GdkDeviceAxis tmp[] = {{GDK_AXIS_X, 0, 0},
473 {GDK_AXIS_Y, 0, 0}};
474 devs[i].num_axes = G_N_ELEMENTS(tmp);
475 devs[i].axes = tmp;
476 devs[i].num_keys = 0;
477 devs[i].keys = 0;
478 i++;
479 }
481 testDeviceCount = i;
482 testDevices = devs;
483 init = true;
484 }
485 }
486 #elif defined(TEST_WITH_BAD_TABLET)
488 /**
489 * Uses the current time in seconds to change a name to be unique from one
490 * run of the program to the next.
491 */
492 void perturbName(gchar *str)
493 {
494 if (str) {
495 GTimeVal when = {0,0};
496 g_get_current_time(&when);
497 gchar *tmp = g_strdup_printf("%ld", when.tv_sec);
499 size_t partLen = strlen(tmp);
500 size_t len = strlen(str);
501 if (len > (partLen + 4)) {
502 size_t pos = (len - partLen) / 2;
503 for (size_t i = 0; i < partLen; i++) {
504 str[pos + i] = tmp[i];
505 }
506 }
507 g_free(tmp);
508 }
509 }
511 static void initTestDevices()
512 {
513 static bool init = false;
514 if (!init) {
515 static GdkDevice devs[5] = {};
516 int i = 0; // use variable instead of constant to allow devices to be moved around, commented out, etc.
518 {
519 // Main (composit) mouse device
520 devs[i].name = g_strdup("Core Pointer");
521 devs[i].source = GDK_SOURCE_MOUSE;
522 devs[i].mode = GDK_MODE_SCREEN;
523 devs[i].has_cursor = 1;
524 static GdkDeviceAxis tmp[] = {{GDK_AXIS_X, 0, 0},
525 {GDK_AXIS_Y, 0, 0}};
526 devs[i].num_axes = G_N_ELEMENTS(tmp);
527 devs[i].axes = tmp;
528 devs[i].num_keys = 0;
529 devs[i].keys = 0;
530 i++;
531 }
533 {
534 // Back of tablet stylus
535 devs[i].name = g_strdup("\346\205\227\347\221\254\347\201\257\345\220\240\346\211\241\346\225\254t\303\265\006 \347\211\220\347\215\245\347\225\263\346\225\262\345\214\240\347\245\264\347\225\254s\357\227\230#\354\234\274C\356\232\210\307\255\350\271\214\310\201\350\222\200\310\201\356\202\250\310\200\350\223\260\310\201\356\202\250\310\200");
536 perturbName(devs[i].name);
537 devs[i].source = GDK_SOURCE_ERASER;
538 devs[i].mode = GDK_MODE_DISABLED;
539 devs[i].has_cursor = 0;
540 static GdkDeviceAxis tmp[] = {{GDK_AXIS_X, 0, 0},
541 {GDK_AXIS_Y, 0, 0},
542 {GDK_AXIS_PRESSURE, 0, 1},
543 {GDK_AXIS_XTILT, -1, 1},
544 {GDK_AXIS_YTILT, -1, 1}};
545 devs[i].num_axes = G_N_ELEMENTS(tmp);
546 devs[i].axes = tmp;
547 devs[i].num_keys = 0;
548 devs[i].keys = 0;
549 i++;
550 }
552 {
553 // Tablet stylus
554 devs[i].name = g_strdup("\346\205\227\347\221\254\347\201\257\345\220\240\346\211\241\346\225\254t\303\265\006 \347\211\220\347\215\245\347\225\263\346\225\262\345\214\240\347\245\264\347\225\254s\357\227\230#\354\234\274C\341\221\230\307\255\343\277\214\310\202\343\230\200\310\202\331\270\310\202\343\231\260\310\202\331\270\310\202");
555 perturbName(devs[i].name);
556 devs[i].source = GDK_SOURCE_PEN;
557 devs[i].mode = GDK_MODE_DISABLED;
558 devs[i].has_cursor = 0;
559 static GdkDeviceAxis tmp[] = {{GDK_AXIS_X, 0, 0},
560 {GDK_AXIS_Y, 0, 0},
561 {GDK_AXIS_PRESSURE, 0, 1},
562 {GDK_AXIS_XTILT, -1, 1},
563 {GDK_AXIS_YTILT, -1, 1}};
564 devs[i].num_axes = G_N_ELEMENTS(tmp);
565 devs[i].axes = tmp;
566 devs[i].num_keys = 0;
567 devs[i].keys = 0;
568 i++;
569 }
571 {
572 // Tablet stylus
573 devs[i].name = g_strdup("\346\205\227\347\221\254\347\201\257\345\220\240\346\211\241\346\225\254t\303\265\006 \347\211\220\347\215\245\347\225\263\346\225\262\345\214\240\347\245\264\347\225\254s\357\227\230#\354\234\274C\341\221\230\307\255\343\277\214\310\202\343\230\200\310\202\331\270\310\202\343\231\260\310\202\331\270\310\202");
574 perturbName(devs[i].name);
575 devs[i].source = GDK_SOURCE_PEN;
576 devs[i].mode = GDK_MODE_DISABLED;
577 devs[i].has_cursor = 0;
578 static GdkDeviceAxis tmp[] = {{GDK_AXIS_X, 0, 0},
579 {GDK_AXIS_Y, 0, 0},
580 {GDK_AXIS_PRESSURE, 0, 1},
581 {GDK_AXIS_XTILT, -1, 1},
582 {GDK_AXIS_YTILT, -1, 1}};
583 devs[i].num_axes = G_N_ELEMENTS(tmp);
584 devs[i].axes = tmp;
585 devs[i].num_keys = 0;
586 devs[i].keys = 0;
587 i++;
588 }
590 testDeviceCount = i;
591 testDevices = devs;
592 init = true;
593 }
594 }
595 #endif
598 /*
599 Local Variables:
600 mode:c++
601 c-file-style:"stroustrup"
602 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
603 indent-tabs-mode:nil
604 fill-column:99
605 End:
606 */
607 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 :