Add a description (for GUI tooltip) to LXHotkeyAttr.
[lxde/lxhotkey.git] / plugins / openbox.c
1 /*
2 * Copyright (C) 2016 Andriy Grytsenko <andrej@rep.kiev.ua>
3 *
4 * This file is a part of LXHotkey project.
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, write to the Free Software Foundation,
18 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19 */
20
21 #ifdef HAVE_CONFIG_H
22 # include "config.h"
23 #endif
24
25 #include "lxhotkey.h"
26
27 #include <X11/Xlib.h>
28 #include <X11/Xatom.h>
29
30 #include <libfm/fm-extra.h>
31 #include <glib/gi18n.h>
32
33 #include <fnmatch.h>
34
35 #define LXKEYS_OB_ERROR lxhotkey_ob_error_quark()
36 static GQuark lxhotkey_ob_error_quark(void)
37 {
38 static GQuark q = 0;
39
40 if G_UNLIKELY(q == 0)
41 q = g_quark_from_static_string("lxhotkey-ob-error");
42
43 return q;
44 }
45 enum LXHotkeyObError {
46 LXKEYS_FILE_ERROR,
47 LXKEYS_PARSE_ERROR
48 };
49
50
51 /* simple functions to manage LXHotkeyAttr data type */
52 static inline LXHotkeyAttr *lxhotkey_attr_new(void)
53 {
54 return g_slice_new0(LXHotkeyAttr);
55 }
56
57 #define free_options(acts) g_list_free_full(acts, (GDestroyNotify)lkxeys_attr_free)
58
59 static void lkxeys_attr_free(LXHotkeyAttr *data)
60 {
61 g_free(data->name);
62 g_list_free_full(data->values, g_free);
63 free_options(data->subopts);
64 g_slice_free(LXHotkeyAttr, data);
65 }
66
67 static void lkxeys_action_free(LXHotkeyGlobal *data)
68 {
69 free_options(data->actions);
70 g_free(data->accel1);
71 g_free(data->accel2);
72 g_free(data);
73 }
74
75 static void lkxeys_app_free(LXHotkeyApp *data)
76 {
77 g_free(data->exec);
78 free_options(data->options);
79 g_free(data->accel1);
80 g_free(data->accel2);
81 g_free(data);
82 }
83
84 /* recursively compare two lists of LXHotkeyAttr */
85 static gboolean options_equal(GList *opts1, GList *opts2)
86 {
87 while (opts1 && opts2) {
88 LXHotkeyAttr *attr1 = opts1->data, *attr2 = opts2->data;
89
90 if (g_strcmp0(attr1->name, attr2->name) != 0)
91 return FALSE;
92 if (attr1->values && attr2->values) {
93 if (g_strcmp0(attr1->values->data, attr2->values->data) != 0)
94 return FALSE;
95 } else if (attr1->values || attr2->values)
96 return FALSE;
97 if (!options_equal(attr1->subopts, attr2->subopts))
98 return FALSE;
99 opts1 = opts1->next;
100 opts2 = opts2->next;
101 }
102 return (opts1 == NULL && opts2 == NULL);
103 }
104
105 /* convert from OB format (A-Return) into GDK format (<Alt>Return) */
106 static gchar *obkey_to_key(const gchar *obkey)
107 {
108 GString *str = g_string_sized_new(16);
109
110 while (*obkey) {
111 if (obkey[1] == '-')
112 switch(obkey[0]) {
113 case 'S':
114 g_string_append(str, "<Shift>");
115 break;
116 case 'C':
117 g_string_append(str, "<Control>");
118 break;
119 case 'A':
120 g_string_append(str, "<Alt>");
121 break;
122 case 'W':
123 g_string_append(str, "<Super>");
124 break;
125 case 'M':
126 g_string_append(str, "<Meta>");
127 break;
128 case 'H':
129 g_string_append(str, "<Hyper>");
130 break;
131 default:
132 goto _add_rest;
133 }
134 else
135 _add_rest:
136 break;
137 obkey += 2;
138 }
139 g_string_append(str, obkey);
140 return g_string_free(str, FALSE);
141 }
142
143 /* convert from GDK format (<Alt>Return) into OB format (A-Return) */
144 static gchar *key_to_obkey(const gchar *key)
145 {
146 GString *str = g_string_sized_new(16);
147 gboolean in_lt = FALSE;
148
149 while (*key) {
150 if (in_lt) {
151 if (*key++ == '>')
152 in_lt = FALSE;
153 } else if (*key == '<') {
154 key++;
155 in_lt = TRUE;
156 if (strncmp(key, "Shift", 5) == 0) {
157 g_string_append(str, "S-");
158 key += 5;
159 } else if (strncmp(key, "Contr", 5) == 0 ||
160 strncmp(key, "Ctr", 3) == 0) {
161 g_string_append(str, "C-");
162 key += 3;
163 } else if (strncmp(key, "Alt", 3) == 0) {
164 g_string_append(str, "A-");
165 key += 3;
166 } else if (strncmp(key, "Super", 5) == 0) {
167 g_string_append(str, "W-");
168 key += 5;
169 } else if (strncmp(key, "Meta", 4) == 0) {
170 g_string_append(str, "M-");
171 key += 4;
172 } else if (strncmp(key, "Hyper", 5) == 0) {
173 g_string_append(str, "H-");
174 key += 5;
175 }
176 } else
177 g_string_append_c(str, *key++);
178 }
179 return g_string_free(str, FALSE);
180 }
181
182
183 static gboolean restart_openbox(GError **error)
184 {
185 Display *dpy = XOpenDisplay(NULL);
186 XEvent ce;
187 Status st;
188 gboolean ret = TRUE;
189
190 ce.xclient.type = ClientMessage;
191 ce.xclient.message_type = XInternAtom(dpy, "_OB_CONTROL", True);
192 ce.xclient.display = dpy;
193 ce.xclient.window = RootWindow(dpy, DefaultScreen(dpy));
194 ce.xclient.format = 32;
195 ce.xclient.data.l[0] = 1; /* reconfigure */
196 ce.xclient.data.l[1] = 0;
197 ce.xclient.data.l[2] = 0;
198 ce.xclient.data.l[3] = 0;
199 ce.xclient.data.l[4] = 0;
200 if (ce.xclient.message_type == None) {
201 g_set_error_literal(error, LXKEYS_OB_ERROR, LXKEYS_FILE_ERROR,
202 _("Failed to reconfigure Openbox."));
203 ret = FALSE;
204 } else
205 st = XSendEvent(dpy, ce.xclient.window, False,
206 SubstructureNotifyMask | SubstructureRedirectMask, &ce);
207 // FIXME: inspect status!
208 XCloseDisplay(dpy);
209 return ret;
210 }
211
212
213 /*
214 * Actions/options list supported by Openbox.
215 *
216 * This array is a bit tricky since it does not contain GList pointers, but
217 * those pointers will be expanded on demand.
218 */
219
220 #define TO_BE_CONVERTED(a) (GList *)(a)
221 #define TO_BE_PREVIOUS TO_BE_CONVERTED(1) /* reuse GList */
222 #define BOOLEAN_VALUES TO_BE_CONVERTED(2) /* reuse GList */
223
224 static char * values_enabled[] = { N_("yes"), N_("no"), NULL };
225
226 static LXHotkeyAttr options_startupnotify[] = {
227 { N_("enabled"), BOOLEAN_VALUES, NULL, NULL, FALSE },
228 { N_("wmclass"), NULL, NULL, NULL, FALSE },
229 { N_("name"), NULL, NULL, NULL, FALSE },
230 { N_("icon"), NULL, NULL, NULL, FALSE },
231 { NULL }
232 };
233
234 static LXHotkeyAttr options_Execute[] = {
235 { N_("command"), NULL, NULL, NULL, FALSE },
236 { N_("prompt"), NULL, NULL, NULL, FALSE },
237 { N_("startupnotify"), NULL, TO_BE_CONVERTED(options_startupnotify), NULL, FALSE },
238 { NULL }
239 };
240
241 static char * values_x[] = { "#", "%", N_("center"), NULL };
242 static char * values_monitor[] = { N_("default"), N_("primary"), N_("mouse"),
243 N_("active"), N_("all"), "#", NULL };
244
245 static LXHotkeyAttr options_position[] = {
246 { "x", TO_BE_CONVERTED(values_x), NULL, NULL, FALSE },
247 { "y", TO_BE_PREVIOUS, NULL, NULL, FALSE },
248 { N_("monitor"), TO_BE_CONVERTED(values_monitor), NULL, NULL, FALSE },
249 { NULL }
250 };
251
252 static LXHotkeyAttr options_ShowMenu[] = {
253 { N_("menu"), NULL, NULL, NULL, FALSE },
254 { N_("position"), NULL, TO_BE_CONVERTED(options_position), NULL, FALSE },
255 { NULL }
256 };
257
258 static char * values_dialog[] = { N_("list"), N_("icons"), N_("none"), NULL };
259
260 static LXHotkeyAttr options_NextWindow[] = {
261 { N_("dialog"), TO_BE_CONVERTED(values_dialog), NULL, NULL, FALSE },
262 { N_("bar"), BOOLEAN_VALUES, NULL, NULL, FALSE },
263 { N_("raise"), BOOLEAN_VALUES, NULL, NULL, FALSE },
264 { N_("allDesktops"), BOOLEAN_VALUES, NULL, NULL, FALSE },
265 { N_("panels"), BOOLEAN_VALUES, NULL, NULL, FALSE },
266 { N_("desktop"), BOOLEAN_VALUES, NULL, NULL, FALSE },
267 { N_("linear"), BOOLEAN_VALUES, NULL, NULL, FALSE },
268 { N_("interactive"), BOOLEAN_VALUES, NULL, NULL, FALSE },
269 { N_("finalactions"), NULL, NULL, NULL, TRUE },
270 { NULL }
271 };
272
273 static char * values_direction[] = { N_("north"), N_("northeast"), N_("east"),
274 N_("southeast"), N_("south"), N_("southwest"),
275 N_("west"), N_("northwest"), NULL };
276
277 static LXHotkeyAttr options_DirectionalCycleWindows[] = {
278 { N_("direction"), TO_BE_CONVERTED(values_direction), NULL, NULL, FALSE },
279 { N_("dialog"), BOOLEAN_VALUES, NULL, NULL, FALSE },
280 { N_("bar"), BOOLEAN_VALUES, NULL, NULL, FALSE },
281 { N_("raise"), BOOLEAN_VALUES, NULL, NULL, FALSE },
282 { N_("panels"), BOOLEAN_VALUES, NULL, NULL, FALSE },
283 { N_("desktops"), BOOLEAN_VALUES, NULL, NULL, FALSE },
284 { N_("finalactions"), NULL, NULL, NULL, TRUE },
285 { NULL }
286 };
287
288 static char * values_to[] = { "#", N_("current"), N_("next"), N_("previous"),
289 N_("last"), N_("north"), N_("up"), N_("south"),
290 N_("down"), N_("west"), N_("left"), N_("east"),
291 N_("right"), NULL };
292
293 static LXHotkeyAttr options_GoToDesktop[] = {
294 { N_("to"), TO_BE_CONVERTED(values_to), NULL, NULL, FALSE },
295 { N_("wrap"), BOOLEAN_VALUES, NULL, NULL, FALSE },
296 { NULL }
297 };
298
299 static char * values_where[] = { N_("current"), N_("last"), NULL };
300
301 static LXHotkeyAttr options_AddDesktop[] = {
302 { N_("where"), TO_BE_CONVERTED(values_where), NULL, NULL, FALSE },
303 { NULL }
304 };
305
306 static LXHotkeyAttr options_Restart[] = {
307 { N_("command"), NULL, NULL, NULL, FALSE },
308 { NULL }
309 };
310
311 static LXHotkeyAttr options_Exit[] = {
312 { N_("prompt"), BOOLEAN_VALUES, NULL, NULL, FALSE },
313 { NULL }
314 };
315
316 static char * values_directionM[] = { N_("both"), N_("horizontal"), N_("vertical"), NULL };
317
318 static LXHotkeyAttr options_ToggleMaximize[] = {
319 { N_("direction"), TO_BE_CONVERTED(values_directionM), NULL, NULL, FALSE },
320 { NULL }
321 };
322
323 static char * values_toS[] = { "#", N_("current"), N_("next"), N_("previous"),
324 N_("last"), N_("north"), N_("up"), N_("south"),
325 N_("down"), N_("west"), N_("left"), N_("east"),
326 N_("right"), NULL };
327
328 static LXHotkeyAttr options_SendToDesktop[] = {
329 { N_("to"), TO_BE_CONVERTED(values_toS), NULL, NULL, FALSE },
330 { N_("wrap"), BOOLEAN_VALUES, NULL, NULL, FALSE },
331 { N_("follow"), BOOLEAN_VALUES, NULL, NULL, FALSE },
332 { NULL }
333 };
334
335 static char * values_edge[] = { N_("top"), N_("left"), N_("right"), N_("bottom"),
336 N_("topleft"), N_("topright"), N_("bottomleft"),
337 N_("bottomright"), NULL };
338
339 static LXHotkeyAttr options_Resize[] = {
340 { N_("edge"), TO_BE_CONVERTED(values_edge), NULL, NULL, FALSE },
341 { NULL }
342 };
343
344 static char * values_xM[] = { "#", N_("current"), N_("center"), NULL };
345 static char * values_width[] = { "#", "%", N_("current"), NULL };
346 static char * values_monitorM[] = { "#", N_("current"), N_("all"), N_("next"), N_("prev"), NULL };
347
348 static LXHotkeyAttr options_MoveResizeTo[] = {
349 { "x", TO_BE_CONVERTED(values_xM), NULL, NULL, FALSE },
350 { "y", TO_BE_PREVIOUS, NULL, NULL, FALSE },
351 { N_("width"), TO_BE_CONVERTED(values_width), NULL, NULL, FALSE },
352 { N_("height"), TO_BE_PREVIOUS, NULL, NULL, FALSE },
353 { N_("monitor"), TO_BE_CONVERTED(values_monitorM), NULL, NULL, FALSE },
354 { NULL }
355 };
356
357 static char * values_xR[] = { "#", NULL };
358
359 static LXHotkeyAttr options_MoveRelative[] = {
360 { "x", TO_BE_CONVERTED(values_xR), NULL, NULL, FALSE },
361 { "y", TO_BE_PREVIOUS, NULL, NULL, FALSE },
362 { NULL }
363 };
364
365 static char * values_xRR[] = { "#", NULL };
366
367 static LXHotkeyAttr options_ResizeRelative[] = {
368 { N_("left"), TO_BE_CONVERTED(values_xRR), NULL, NULL, FALSE },
369 { N_("right"), TO_BE_PREVIOUS, NULL, NULL, FALSE },
370 { N_("top"), TO_BE_PREVIOUS, NULL, NULL, FALSE },
371 { N_("bottom"), TO_BE_PREVIOUS, NULL, NULL, FALSE },
372 { NULL }
373 };
374
375 static char * values_directionE[] = { N_("north"), N_("south"), N_("west"), N_("east"), NULL };
376
377 static LXHotkeyAttr options_MoveToEdge[] = {
378 { N_("direction"), TO_BE_CONVERTED(values_directionE), NULL, NULL, FALSE },
379 { NULL }
380 };
381
382 static char * values_layer[] = { N_("top"), N_("normal"), N_("bottom"), NULL };
383
384 static LXHotkeyAttr options_SendToLayer[] = {
385 { N_("layer"), TO_BE_CONVERTED(values_layer), NULL, NULL, FALSE },
386 { NULL }
387 };
388
389 static LXHotkeyAttr list_actions[] = {
390 /* global actions */
391 { N_("Execute"), NULL, TO_BE_CONVERTED(options_Execute), NULL, FALSE },
392 { N_("ShowMenu"), NULL, TO_BE_CONVERTED(options_ShowMenu), NULL, FALSE },
393 { N_("NextWindow"), NULL, TO_BE_CONVERTED(options_NextWindow), NULL, FALSE },
394 { N_("PreviousWindow"), NULL, TO_BE_PREVIOUS, NULL, FALSE },
395 { N_("DirectionalCycleWindows"), NULL, TO_BE_CONVERTED(options_DirectionalCycleWindows), NULL, FALSE },
396 { N_("DirectionalTargetWindow"), NULL, TO_BE_PREVIOUS, NULL, FALSE },
397 { N_("GoToDesktop"), NULL, TO_BE_CONVERTED(options_GoToDesktop), NULL, FALSE },
398 { N_("AddDesktop"), NULL, TO_BE_CONVERTED(options_AddDesktop), NULL, FALSE },
399 { N_("RemoveDesktop"), NULL, TO_BE_PREVIOUS, NULL, FALSE },
400 { N_("ToggleDockAutohide"), NULL, NULL, NULL, FALSE },
401 { N_("Reconfigure"), NULL, NULL, NULL, FALSE },
402 { N_("Restart"), NULL, TO_BE_CONVERTED(options_Restart), NULL, FALSE },
403 { N_("Exit"), NULL, TO_BE_CONVERTED(options_Exit), NULL, FALSE },
404 /* windows actions */
405 { N_("Focus"), NULL, NULL, NULL, FALSE },
406 { N_("Raise"), NULL, NULL, NULL, FALSE },
407 { N_("Lower"), NULL, NULL, NULL, FALSE },
408 { N_("RaiseLower"), NULL, NULL, NULL, FALSE },
409 { N_("Unfocus"), NULL, NULL, NULL, FALSE },
410 { N_("FocusToBottom"), NULL, NULL, NULL, FALSE },
411 { N_("Iconify"), NULL, NULL, NULL, FALSE },
412 { N_("Close"), NULL, NULL, NULL, FALSE },
413 { N_("ToggleShade"), NULL, NULL, NULL, FALSE },
414 { N_("Shade"), NULL, NULL, NULL, FALSE },
415 { N_("Unshade"), NULL, NULL, NULL, FALSE },
416 { N_("ToggleOmnipresent"), NULL, NULL, NULL, FALSE },
417 { N_("ToggleMaximize"), NULL, TO_BE_CONVERTED(options_ToggleMaximize), NULL, FALSE },
418 { N_("Maximize"), NULL, TO_BE_PREVIOUS, NULL, FALSE },
419 { N_("Unmaximize"), NULL, TO_BE_PREVIOUS, NULL, FALSE },
420 { N_("ToggleFullscreen"), NULL, NULL, NULL, FALSE },
421 { N_("ToggleDecorations"), NULL, NULL, NULL, FALSE },
422 { N_("Decorate"), NULL, NULL, NULL, FALSE },
423 { N_("Undecorate"), NULL, NULL, NULL, FALSE },
424 { N_("SendToDesktop"), NULL, TO_BE_CONVERTED(options_SendToDesktop), NULL, FALSE },
425 { N_("Move"), NULL, NULL, NULL, FALSE },
426 { N_("Resize"), NULL, TO_BE_CONVERTED(options_Resize), NULL, FALSE },
427 { N_("MoveResizeTo"), NULL, TO_BE_CONVERTED(options_MoveResizeTo), NULL, FALSE },
428 { N_("MoveRelative"), NULL, TO_BE_CONVERTED(options_MoveRelative), NULL, FALSE },
429 { N_("ResizeRelative"), NULL, TO_BE_CONVERTED(options_ResizeRelative), NULL, FALSE },
430 { N_("MoveToEdge"), NULL, TO_BE_CONVERTED(options_MoveToEdge), NULL, FALSE },
431 { N_("GrowToEdge"), NULL, TO_BE_PREVIOUS, NULL, FALSE },
432 { N_("ShrinkToEdge"), NULL, TO_BE_PREVIOUS, NULL, FALSE },
433 { N_("GrowToFill"), NULL, NULL, NULL, FALSE },
434 { N_("ToggleAlwaysOnTop"), NULL, NULL, NULL, FALSE },
435 { N_("ToggleAlwaysOnBottom"), NULL, NULL, NULL, FALSE },
436 { N_("SendToLayer"), NULL, TO_BE_CONVERTED(options_SendToLayer), NULL, FALSE },
437 // FIXME: support for If/ForEach/Stop ?
438 { NULL }
439 };
440
441 static GList *boolean_values = NULL;
442 static GList *available_wm_actions = NULL;
443 static GList *available_app_options = NULL;
444
445 static GList *convert_values(gpointer data)
446 {
447 char ** array;
448 GList *list = NULL;
449
450 for (array = data; array[0] != NULL; array++) {
451 g_debug("creating GList for string '%s'", array[0]);
452 list = g_list_prepend(list, array[0]);
453 }
454 return g_list_reverse(list);
455 }
456
457 static GList *convert_options(gpointer data)
458 {
459 LXHotkeyAttr *array, *last = NULL;
460 GList *list = NULL;
461
462 for (array = data; array->name != NULL; array++) {
463 list = g_list_prepend(list, array);
464 if (last && array->values == TO_BE_PREVIOUS)
465 array->values = last->values;
466 else if (array->values == BOOLEAN_VALUES) {
467 if (boolean_values == NULL)
468 boolean_values = convert_values(values_enabled);
469 array->values = boolean_values;
470 } else if (array->values != NULL)
471 array->values = convert_values(array->values);
472 if (last && array->subopts == TO_BE_PREVIOUS)
473 array->subopts = last->subopts;
474 else if (array->subopts != NULL) {
475 if (array->subopts == TO_BE_CONVERTED(options_Execute))
476 array->subopts = available_app_options = convert_options(array->subopts);
477 else
478 array->subopts = convert_options(array->subopts);
479 }
480 last = array;
481 }
482 return g_list_reverse(list);
483 }
484
485
486 typedef struct {
487 FmXmlFileItem *parent; /* R/O */
488 GList *list; /* contains LXHotkeyAttr items for finished actions */
489 } ObActionsList;
490
491 typedef struct {
492 char *path;
493 FmXmlFile *xml;
494 FmXmlFileItem *keyboard; /* the <keyboard> section */
495 GList *actions; /* no-exec actions, in reverse order */
496 GList *execs; /* exec-only actions, in reverse order */
497 GList *stack; /* only for build - elements are ObActionsList */
498 GList *added_tags; /* only for edit */
499 } ObXmlFile;
500
501 static FmXmlFileTag ObXmlFile_keyboard; /* section that we work on */
502 static FmXmlFileTag ObXmlFile_keybind; /* subsection, for each binding */
503 static FmXmlFileTag ObXmlFile_action; /* may be multiple for a binding */
504 static FmXmlFileTag ObXmlFile_command; /* for <action name="Execute"> */
505 static FmXmlFileTag ObXmlFile_execute; /* obsolete alias for command */
506
507 static gboolean tag_handler_keyboard(FmXmlFileItem *item, GList *children,
508 char * const *attribute_names,
509 char * const *attribute_values,
510 guint n_attributes, gint line, gint pos,
511 GError **error, gpointer user_data)
512 {
513 ObXmlFile *cfg = user_data;
514
515 if (cfg->keyboard) {
516 /* FIXME: merge duplicate section? */
517 g_set_error_literal(error, LXKEYS_OB_ERROR, LXKEYS_PARSE_ERROR,
518 _("Duplicate <keyboard> section in the rc.xml file."));
519 return FALSE;
520 }
521 cfg->keyboard = item;
522 return TRUE;
523 }
524
525 static gboolean tag_handler_keybind(FmXmlFileItem *item, GList *children,
526 char * const *attribute_names,
527 char * const *attribute_values,
528 guint n_attributes, gint line, gint pos,
529 GError **error, gpointer user_data)
530 {
531 ObXmlFile *cfg = user_data;
532 ObActionsList *oblist;
533 GList *actions, *l;
534 gchar *key;
535 const char *exec_line = NULL;
536 LXHotkeyAttr *action;
537 LXHotkeyApp *app = NULL;
538 LXHotkeyGlobal *act;
539 guint i;
540
541 if (!cfg->stack || cfg->stack->next) { /* corruption! */
542 g_set_error_literal(error, LXKEYS_OB_ERROR, LXKEYS_PARSE_ERROR,
543 _("Internal error."));
544 return FALSE;
545 }
546 oblist = cfg->stack->data;
547 if (oblist->parent != item) { /* corruption! */
548 g_set_error_literal(error, LXKEYS_OB_ERROR, LXKEYS_PARSE_ERROR,
549 _("Internal error."));
550 return FALSE;
551 }
552 /* just remove top stack item, all actions are already there */
553 actions = oblist->list;
554 g_free(oblist);
555 g_list_free(cfg->stack);
556 cfg->stack = NULL;
557 action = actions->data;
558 /* decide where to put: execs or actions */
559 if (children && !children->next && /* exactly one child which is an action */
560 fm_xml_file_item_get_tag(children->data) == ObXmlFile_action) {
561 if (strcmp(action->name, "Execute") == 0) { /* and action is Execute */
562 FmXmlFileItem *exec = fm_xml_file_item_find_child(children->data,
563 ObXmlFile_command);
564 if (!exec)
565 exec = fm_xml_file_item_find_child(children->data, ObXmlFile_execute);
566 /* if exec is NULL then it's invalid action after all */
567 if (exec)
568 {
569 /* not empty exec line was verified in the handler */
570 exec_line = fm_xml_file_item_get_data(fm_xml_file_item_find_child(exec, FM_XML_FILE_TEXT), NULL);
571 for (l = cfg->execs; l; l = l->next)
572 /* if exec line is equal to one gathered already */
573 if (strcmp(((LXHotkeyApp *)l->data)->exec, exec_line) == 0 &&
574 /* and it has no secondary keybinding */
575 ((LXHotkeyApp *)l->data)->accel2 == NULL &&
576 /* and options are also equal */
577 options_equal(((LXHotkeyApp *)l->data)->options, action->subopts))
578 {
579 /* then just add this keybinding to found one */
580 app = (LXHotkeyApp *)l->data;
581 break;
582 }
583 }
584 }
585 }
586 /* find a "key" attribute and save its value as accel1 */
587 for (i = 0; i < n_attributes; i++)
588 if (g_strcmp0(attribute_names[i], "key") == 0)
589 break;
590 if (i == n_attributes) { /* no name in XML! */
591 g_set_error_literal(error, LXKEYS_OB_ERROR, LXKEYS_PARSE_ERROR,
592 _("rc.xml error: no key is set for a keybind."));
593 free_options(actions);
594 return FALSE;
595 } else
596 key = obkey_to_key(attribute_values[i]);
597 /* if that exec already exists then reuse it and set as accel2 */
598 if (app) {
599 app->accel2 = key;
600 app->data2 = item;
601 /* otherwise create LXHotkeyApp or LXHotkeyGlobal and add it to the list */
602 } else if (exec_line) {
603 app = g_new0(LXHotkeyApp, 1);
604 app->accel1 = key;
605 app->exec = g_strdup(exec_line);
606 app->data1 = item;
607 app->options = action->subopts;
608 /* remove exec line from options, it should be in XML but not in LXHotkeyApp */
609 for (l = app->options; l; ) {
610 LXHotkeyAttr *opt = l->data;
611 l = l->next;
612 if (strcmp(opt->name, "command") == 0 || strcmp(opt->name, "execute") == 0) {
613 app->options = g_list_remove(app->options, opt);
614 lkxeys_attr_free(opt);
615 }
616 }
617 action->subopts = NULL;
618 cfg->execs = g_list_prepend(cfg->execs, app);
619 } else {
620 for (l = cfg->actions; l; l = l->next)
621 /* if the same actions list was gathered already */
622 if (options_equal(((LXHotkeyGlobal *)l->data)->actions, actions)
623 /* and it has no secondary keybinding */
624 && ((LXHotkeyGlobal *)l->data)->accel2 == NULL)
625 break;
626 if (l == NULL) {
627 act = g_new0(LXHotkeyGlobal, 1);
628 act->accel1 = key;
629 act->data1 = item;
630 act->actions = actions;
631 actions = NULL;
632 cfg->actions = g_list_prepend(cfg->actions, act);
633 } else { /* actions exist in list so reuse it adding a second keybinding */
634 act->accel2 = key;
635 act->data2 = item;
636 }
637 }
638 free_options(actions);
639 return TRUE;
640 }
641
642 /* collect all children of FmXmlFileItem into LXHotkeyAttr list
643 removing from stack if found there
644 since actions cannot be mixed with options, just
645 ignore anything not collected into any ObActionsList */
646 static GList *resolve_item(GList **stack, GList *children, GList **value,
647 GError **error)
648 {
649 GList *child, *l, *items = NULL;
650 ObActionsList *act; /* stack item */
651 FmXmlFileItem *item; /* child item */
652 LXHotkeyAttr *data;
653
654 for (child = children; child; child = child->next) {
655 item = child->data;
656 if (fm_xml_file_item_get_tag(item) == FM_XML_FILE_TEXT) { /* value! */
657 *value = g_list_prepend(*value,
658 g_strdup(fm_xml_file_item_get_data(item, NULL)));
659 continue;
660 }
661 if (fm_xml_file_item_get_tag(item) == ObXmlFile_action) { /* stray action? */
662 g_set_error_literal(error, LXKEYS_OB_ERROR, LXKEYS_PARSE_ERROR,
663 _("Invalid rc.xml: action with a sub-action."));
664 free_options(items);
665 return NULL;
666 }
667 data = lxhotkey_attr_new();
668 data->name = g_strdup(fm_xml_file_item_get_tag_name(item));
669 /* find if item is in stack, then remove from there and use ready list */
670 for (l = *stack; l; l = l->next) {
671 act = l->data;
672 if (act->parent == item) {
673 *stack = g_list_delete_link(*stack, l);
674 data->subopts = act->list;
675 data->has_actions = TRUE;
676 g_free(act); /* release the ObActionsList */
677 break;
678 }
679 }
680 /* otherwise collect children by recursive call */
681 if (l == NULL) {
682 GError *err = NULL;
683
684 l = fm_xml_file_item_get_children(item);
685 data->subopts = resolve_item(stack, l, &data->values, &err);
686 g_list_free(l);
687 if (err) {
688 g_propagate_error(error, err);
689 free_options(items);
690 lkxeys_attr_free(data);
691 return NULL;
692 }
693 }
694 /* add the item to the list */
695 items = g_list_prepend(items, data);
696 }
697 return g_list_reverse(items);
698 }
699
700 static gboolean is_on_stack(GList *stack, FmXmlFileItem *item)
701 {
702 while (stack) {
703 if (((ObActionsList *)stack->data)->parent == item)
704 return TRUE;
705 stack = stack->next;
706 }
707 return FALSE;
708 }
709
710 /* push new item on the stack, return top item */
711 static ObActionsList *put_on_stack_top(ObXmlFile *cfg, FmXmlFileItem *parent)
712 {
713 ObActionsList *oblist = g_new0(ObActionsList, 1);
714
715 oblist->parent = parent;
716 cfg->stack = g_list_prepend(cfg->stack, oblist);
717 return oblist;
718 }
719
720 /* (parent/children) stack (changes)
721 <k><a1><f><a2/> = (f/-) f (+f)
722 +<a3><x><a4/> => (x/-) f x:4 (+x)
723 +<a5/> => (x/-) f x:45 ()
724 +</x></a3> => (f/x:45) f:3-x:45 (-x)
725 +</f></a1> => (k/f:3) k:1-f:3 (-f +k)
726 +<a6><h><a7><t><a8/> => (t/-) k:1 t:8 (+t)
727 +</t></a7> => (h/t:8) k:1 h:7-t:8 (-t +h)
728 +</h></a6> => (k/h:7) k:16-h:7 (-h)
729 +<a9><b><c><d><a0><e><g><az/> => (g/-) k:16 g:z (+g)
730 +</g></e></a0> => (d/e-g:z) k:16 d:0-e-g:z (-g +d)
731 +</d></c></b></a9> => (k/b-c-d:0) k:169-b-c-d:0 (-d)
732 +<a3><l><m><ay/> => (m/-) k:169 m:y (+m)
733 +</m><n><a2><o/></a2> => (n/o) k:169 m:y n:2-o (+n)
734 +</n></l></a3> => (k/l-{m:y|n:2}) k:1693-l-{m:y|n:2} (-m -n)
735 +</k> => (-/1693) - (-k)
736 */
737
738 static gboolean tag_handler_action(FmXmlFileItem *item, GList *children,
739 char * const *attribute_names,
740 char * const *attribute_values,
741 guint n_attributes, gint line, gint pos,
742 GError **error, gpointer user_data)
743 {
744 /* if parent is on top of stack then it's another action of the same parent */
745 /* if parent exists deeper on stack after resolving then it's curruption */
746 /* if parent doesn't exist on stack but some of children is then that child
747 is finished, replace with this parent on stack after resolving children */
748 /* if parent doesn't exist on stack neither any child is then it got
749 deeper so just add it on stack */
750 ObXmlFile *cfg = user_data;
751 LXHotkeyAttr *data;
752 ObActionsList *oblist;
753 FmXmlFileItem *parent;
754 GError *err = NULL;
755 guint i;
756
757 /* create a LXHotkeyAttr */
758 data = lxhotkey_attr_new();
759 //data->has_actions = FALSE; /* action can have only options, not sub-actions! */
760 /* resolve all children of this action, clearing from stack */
761 data->subopts = resolve_item(&cfg->stack, children, &data->values, &err);
762 if (err) {
763 g_propagate_error(error, err);
764 lkxeys_attr_free(data);
765 return FALSE;
766 }
767 /* find a "name" attribute and set it as name */
768 for (i = 0; i < n_attributes; i++)
769 if (g_strcmp0(attribute_names[i], "name") == 0)
770 break;
771 if (i == n_attributes) { /* no name in XML! */
772 g_set_error_literal(error, LXKEYS_OB_ERROR, LXKEYS_PARSE_ERROR,
773 _("rc.xml error: no name is set for action."));
774 lkxeys_attr_free(data);
775 return FALSE;
776 } else
777 data->name = g_strdup(attribute_values[i]);
778 /* add this action to the parent's list */
779 parent = fm_xml_file_item_get_parent(item);
780 if (!is_on_stack(cfg->stack, parent))
781 oblist = put_on_stack_top(cfg, parent);
782 else if ((oblist = cfg->stack->data)->parent != parent) { /* corruption */
783 g_set_error_literal(error, LXKEYS_OB_ERROR, LXKEYS_PARSE_ERROR,
784 _("Internal error."));
785 lkxeys_attr_free(data);
786 return FALSE;
787 }
788 oblist->list = g_list_append(oblist->list, data);
789 return TRUE;
790 }
791
792 static gboolean tag_handler_command(FmXmlFileItem *item, GList *children,
793 char * const *attribute_names,
794 char * const *attribute_values,
795 guint n_attributes, gint line, gint pos,
796 GError **error, gpointer user_data)
797 {
798 FmXmlFileItem *text = fm_xml_file_item_find_child(item, FM_XML_FILE_TEXT);
799
800 if (text == NULL) {
801 /* check if value is not empty */
802 g_set_error(error, LXKEYS_OB_ERROR, LXKEYS_PARSE_ERROR,
803 _("rc.xml error: empty tag <%s> is prohibited."),
804 fm_xml_file_item_get_tag_name(item));
805 return FALSE;
806 }
807 return TRUE;
808 }
809
810
811 static void obcfg_free(gpointer config)
812 {
813 ObXmlFile *cfg = (ObXmlFile *)config;
814
815 g_free(cfg->path);
816 g_object_unref(cfg->xml);
817 g_list_free_full(cfg->actions, (GDestroyNotify)lkxeys_action_free);
818 g_list_free_full(cfg->execs, (GDestroyNotify)lkxeys_app_free);
819 while (cfg->stack != NULL) {
820 free_options(((ObActionsList *)cfg->stack->data)->list);
821 g_free(cfg->stack->data);
822 cfg->stack = g_list_delete_link(cfg->stack, cfg->stack);
823 }
824 g_list_free(cfg->added_tags);
825 g_free(cfg);
826 }
827
828 static gpointer obcfg_load(gpointer config, GError **error)
829 {
830 ObXmlFile *cfg = (ObXmlFile *)config;
831 gchar *contents = NULL;
832 GError *err = NULL;
833 gsize len;
834
835 if (config) {
836 /* just discard any changes if any exist */
837 FmXmlFile *old_xml = cfg->xml;
838
839 cfg->xml = fm_xml_file_new(old_xml);
840 g_object_unref(old_xml);
841 g_list_free_full(cfg->actions, (GDestroyNotify)lkxeys_action_free);
842 g_list_free_full(cfg->execs, (GDestroyNotify)lkxeys_app_free);
843 cfg->actions = NULL;
844 cfg->execs = NULL;
845 cfg->keyboard = NULL;
846 } else {
847 const char *session;
848
849 /* prepare data */
850 cfg = g_new0(ObXmlFile, 1);
851 cfg->xml = fm_xml_file_new(NULL);
852 /* register handlers */
853 ObXmlFile_keyboard = fm_xml_file_set_handler(cfg->xml, "keyboard",
854 &tag_handler_keyboard, FALSE, NULL);
855 ObXmlFile_keybind = fm_xml_file_set_handler(cfg->xml, "keybind",
856 &tag_handler_keybind, FALSE, NULL);
857 ObXmlFile_action = fm_xml_file_set_handler(cfg->xml, "action",
858 &tag_handler_action, FALSE, NULL);
859 ObXmlFile_command = fm_xml_file_set_handler(cfg->xml, "command",
860 &tag_handler_command, FALSE, NULL);
861 ObXmlFile_execute = fm_xml_file_set_handler(cfg->xml, "execute",
862 &tag_handler_command, FALSE, NULL);
863 /* let try to detect rc.xml file currently in use:
864 with Lubuntu it's lubuntu-rc.xml, with lxde session it's lxde-rc.xml */
865 session = g_getenv("DESKTOP_SESSION");
866 if (session == NULL)
867 session = g_getenv("GDMSESSION");
868 if (session == NULL)
869 session = g_getenv("XDG_CURRENT_DESKTOP");
870 if (g_strcmp0(session, "Lubuntu") == 0)
871 cfg->path = g_build_filename(g_get_user_config_dir(), "openbox",
872 "lubuntu-rc.xml", NULL);
873 else if (g_strcmp0(session, "LXDE") == 0)
874 cfg->path = g_build_filename(g_get_user_config_dir(), "openbox",
875 "lxde-rc.xml", NULL);
876 else
877 cfg->path = g_build_filename(g_get_user_config_dir(), "openbox",
878 "rc.xml", NULL);
879 }
880
881 /* try to load ~/.config/openbox/$xml */
882 if (!g_file_get_contents(cfg->path, &contents, &len, NULL)) {
883 /* if it does not exist then try to load $XDG_SYSTEM_CONFDIR/openbox/rc.xml */
884 const gchar * const *dirs;
885 char *path = NULL;
886
887 for (dirs = g_get_system_config_dirs(); dirs[0]; dirs++) {
888 path = g_build_filename(dirs[0], "openbox", "rc.xml", NULL);
889 if (g_file_get_contents(path, &contents, &len, NULL))
890 break;
891 g_free(path);
892 path = NULL;
893 }
894 if (path == NULL) { /* failed to load */
895 g_set_error_literal(error, LXKEYS_OB_ERROR, LXKEYS_FILE_ERROR,
896 _("Could not find the rc.xml file anywhere."));
897 obcfg_free(cfg);
898 return NULL;
899 }
900 g_free(path);
901 }
902 /* parse the found rc.xml file */
903 if (!fm_xml_file_parse_data(cfg->xml, contents, len, &err, cfg)
904 || fm_xml_file_finish_parse(cfg->xml, &err) == NULL) {
905 g_propagate_error(error, err);
906 g_free(contents);
907 obcfg_free(cfg);
908 return NULL;
909 }
910 g_free(contents);
911 return cfg;
912 }
913
914 static gboolean obcfg_save(gpointer config, GError **error)
915 {
916 ObXmlFile *cfg = (ObXmlFile *)config;
917 char *contents;
918 gsize len;
919 gboolean ret = FALSE;
920
921 /* save as ~/.config/openbox/$xml */
922 contents = fm_xml_file_to_data(cfg->xml, &len, error);
923 if (contents) {
924 /* workaround on libfm-extra bug on save data without DTD */
925 if (contents[0] == '\n')
926 ret = g_file_set_contents(cfg->path, contents+1, len-1, error);
927 else
928 ret = g_file_set_contents(cfg->path, contents, len, error);
929 g_free(contents);
930 }
931 return ret;
932 }
933
934 static GList *obcfg_get_wm_keys(gpointer config, const char *mask, GError **error)
935 {
936 ObXmlFile *cfg = (ObXmlFile *)config;
937 GList *list = NULL, *l;
938 LXHotkeyGlobal *data;
939
940 if (cfg == NULL)
941 g_set_error_literal(error, LXKEYS_OB_ERROR, LXKEYS_FILE_ERROR,
942 _("No WM configuration is available."));
943 else for (l = cfg->actions; l; l = l->next) {
944 data = l->data;
945 if (mask == NULL || fnmatch(mask, data->accel1, 0) == 0
946 || (data->accel2 != NULL && fnmatch(mask, data->accel2, 0) == 0))
947 list = g_list_prepend(list, data);
948 }
949 return list;
950 }
951
952 static gboolean tag_null_handler(FmXmlFileItem *item, GList *children,
953 char * const *attribute_names,
954 char * const *attribute_values,
955 guint n_attributes, gint line, gint pos,
956 GError **error, gpointer user_data)
957 {
958 return TRUE;
959 }
960
961 /* if opts==NULL then don't copy any LXHotkeyAttr below it */
962 static FmXmlFileItem *make_new_xml_item(ObXmlFile *cfg, LXHotkeyAttr *opt,
963 GList **opts, gboolean is_action)
964 {
965 FmXmlFileItem *item, *sub;
966 GList *l;
967 LXHotkeyAttr *act = NULL;
968
969 if (is_action) {
970 item = fm_xml_file_item_new(ObXmlFile_action);
971 fm_xml_file_item_set_attribute(item, "name", opt->name);
972 } else {
973 FmXmlFileTag tag = FM_XML_FILE_TAG_NOT_HANDLED;
974
975 /* find a tag in list by opt->name */
976 for (l = cfg->added_tags; l; l = l->next)
977 if (g_strcmp0(fm_xml_file_get_tag_name(cfg->xml, GPOINTER_TO_UINT(l->data)),
978 opt->name) == 0)
979 break;
980 if (l == NULL) {
981 /* if not found then add to list */
982 tag = fm_xml_file_set_handler(cfg->xml, opt->name, &tag_null_handler, FALSE, NULL);
983 cfg->added_tags = g_list_prepend(cfg->added_tags, GUINT_TO_POINTER(tag));
984 } else
985 tag = GPOINTER_TO_UINT(l->data);
986 item = fm_xml_file_item_new(tag);
987 if (opt->values)
988 fm_xml_file_item_append_text(item, opt->values->data, -1, FALSE);
989 }
990 if (opts != NULL) {
991 /* make a copy if requested */
992 act = lxhotkey_attr_new();
993 act->name = g_strdup(opt->name);
994 if (opt->values)
995 act->values = g_list_prepend(NULL, g_strdup(opt->values->data));
996 act->has_actions = opt->has_actions;
997 *opts = g_list_append(*opts, act);
998 }
999 for (l = opt->subopts; l; l = l->next) {
1000 sub = make_new_xml_item(cfg, l->data, act ? &act->subopts : NULL,
1001 opt->has_actions);
1002 fm_xml_file_item_append_child(item, sub);
1003 }
1004 return item;
1005 }
1006
1007 /* if opts==NULL then don't make any LXHotkeyAttr below it */
1008 static FmXmlFileItem *make_new_xml_binding(ObXmlFile *cfg, GList *actions,
1009 const gchar *accel, GList **opts,
1010 const gchar *exec)
1011 {
1012 FmXmlFileItem *binding = fm_xml_file_item_new(ObXmlFile_keybind);
1013 FmXmlFileItem *item;
1014 char *obkey = key_to_obkey(accel);
1015
1016 fm_xml_file_item_set_attribute(binding, "key", obkey);
1017 g_free(obkey);
1018 fm_xml_file_item_append_child(cfg->keyboard, binding);
1019 if (exec) {
1020 /* make <action name='Execute'><command>exec</command>..opts..</action>
1021 instead of ..<acts>.. */
1022 item = fm_xml_file_item_new(ObXmlFile_action);
1023 fm_xml_file_item_set_attribute(item, "name", "Execute");
1024 fm_xml_file_item_append_child(binding, item);
1025 binding = item;
1026 item = fm_xml_file_item_new(ObXmlFile_command);
1027 fm_xml_file_item_append_text(item, exec, -1, FALSE);
1028 fm_xml_file_item_append_child(binding, item);
1029 }
1030 for (; actions; actions = actions->next) {
1031 item = make_new_xml_item(cfg, actions->data, opts, (exec == NULL));
1032 fm_xml_file_item_append_child(binding, item);
1033 }
1034 return binding;
1035 }
1036
1037 static inline void replace_key(FmXmlFileItem *item, const char *key, char **kptr)
1038 {
1039 char *obkey = key_to_obkey(key);
1040
1041 fm_xml_file_item_set_attribute(item, "key", obkey);
1042 g_free(obkey);
1043 g_free(*kptr);
1044 *kptr = g_strdup(key);
1045 }
1046
1047 static gboolean obcfg_set_wm_key(gpointer config, LXHotkeyGlobal *data, GError **error)
1048 {
1049 ObXmlFile *cfg = (ObXmlFile *)config;
1050 GList *l;
1051 LXHotkeyGlobal *act;
1052
1053 if (cfg == NULL) {
1054 g_set_error_literal(error, LXKEYS_OB_ERROR, LXKEYS_FILE_ERROR,
1055 _("No WM configuration is available."));
1056 return FALSE;
1057 } else if (data->actions == NULL) {
1058 g_set_error_literal(error, LXKEYS_OB_ERROR, LXKEYS_PARSE_ERROR,
1059 _("Keybinding should activate at least one action."));
1060 return FALSE;
1061 }
1062 /* find if those keys are already bound elsewhere */
1063 for (l = cfg->actions; l; l = l->next) {
1064 if (data->accel1) {
1065 if (strcmp(data->accel1, ((LXHotkeyGlobal *)l->data)->accel1) == 0 ||
1066 g_strcmp0(data->accel1, ((LXHotkeyGlobal *)l->data)->accel2) == 0)
1067 goto _accel1_bound;
1068 }
1069 if (data->accel2) {
1070 if (g_strcmp0(data->accel2, ((LXHotkeyGlobal *)l->data)->accel1) == 0 ||
1071 g_strcmp0(data->accel2, ((LXHotkeyGlobal *)l->data)->accel2) == 0)
1072 goto _accel2_bound;
1073 }
1074 }
1075 for (l = cfg->execs; l; l = l->next) {
1076 if (data->accel1) {
1077 if (strcmp(data->accel1, ((LXHotkeyApp *)l->data)->accel1) == 0 ||
1078 g_strcmp0(data->accel1, ((LXHotkeyApp *)l->data)->accel2) == 0) {
1079 _accel1_bound:
1080 g_set_error(error, LXKEYS_OB_ERROR, LXKEYS_FILE_ERROR,
1081 _("Hotkey '%s' is already bound to an action."),
1082 data->accel1);
1083 return FALSE;
1084 }
1085 }
1086 if (data->accel2) {
1087 if (g_strcmp0(data->accel2, ((LXHotkeyApp *)l->data)->accel1) == 0 ||
1088 g_strcmp0(data->accel2, ((LXHotkeyApp *)l->data)->accel2) == 0) {
1089 _accel2_bound:
1090 g_set_error(error, LXKEYS_OB_ERROR, LXKEYS_FILE_ERROR,
1091 _("Hotkey '%s' is already bound to an action."),
1092 data->accel2);
1093 return FALSE;
1094 }
1095 }
1096 }
1097 /* find if that action(s) is present */
1098 for (l = cfg->actions; l; l = l->next)
1099 if (options_equal((act = l->data)->actions, data->actions))
1100 break;
1101 /* if found then either change keys or remove the keybinding */
1102 if (l != NULL) {
1103 if (data->accel1 == NULL) {
1104 /* removal requested */
1105 if (act->data1)
1106 fm_xml_file_item_destroy(act->data1);
1107 if (act->data2)
1108 fm_xml_file_item_destroy(act->data2);
1109 lkxeys_action_free(act);
1110 cfg->actions = g_list_delete_link(cfg->actions, l);
1111 } else {
1112 if (data->accel2 == NULL) {
1113 /* new data contains only one binding */
1114 if (g_strcmp0(act->accel1, data->accel1) == 0) {
1115 /* accel1 not changed, just clear accel2 */
1116 if (act->data2)
1117 fm_xml_file_item_destroy(act->data2);
1118 g_free(act->accel2);
1119 act->accel2 = NULL;
1120 } else if (g_strcmp0(act->accel2, data->accel1) == 0) {
1121 /* accel1 was removed */
1122 if (act->data1)
1123 fm_xml_file_item_destroy(act->data1);
1124 g_free(act->accel1);
1125 act->accel1 = act->accel2;
1126 act->accel2 = NULL;
1127 } else {
1128 /* full change */
1129 replace_key(act->data1, data->accel1, &act->accel1);
1130 if (act->data2)
1131 fm_xml_file_item_destroy(act->data2);
1132 g_free(act->accel2);
1133 act->accel2 = NULL;
1134 }
1135 } else if (act->accel2 == NULL) {
1136 /* new data has two bindings and old data has 1 */
1137 if (g_strcmp0(act->accel1, data->accel1) == 0) {
1138 /* add data->accel2 */
1139 act->data2 = make_new_xml_binding(cfg, data->actions, data->accel2, NULL, NULL);
1140 act->accel2 = g_strdup(data->accel2);
1141 } else if (g_strcmp0(act->accel1, data->accel2) == 0) {
1142 /* add data->accel1 */
1143 act->data2 = make_new_xml_binding(cfg, data->actions, data->accel1, NULL, NULL);
1144 act->accel2 = g_strdup(data->accel1);
1145 } else {
1146 /* replace key act->accel1 and add data->accel2 */
1147 replace_key(act->data1, data->accel1, &act->accel1);
1148 act->data2 = make_new_xml_binding(cfg, data->actions, data->accel2, NULL, NULL);
1149 act->accel2 = g_strdup(data->accel2);
1150 }
1151 } else {
1152 /* both keys are present in old and new data */
1153 if (g_strcmp0(act->accel1, data->accel1) == 0) {
1154 if (g_strcmp0(act->accel2, data->accel2) != 0)
1155 /* just accel2 is changed */
1156 replace_key(act->data2, data->accel2, &act->accel2);
1157 /* else nothing changed */
1158 } else if (g_strcmp0(act->accel1, data->accel2) == 0) {
1159 if (g_strcmp0(act->accel2, data->accel1) != 0)
1160 /* replace accel2 with data->accel1 */
1161 replace_key(act->data2, data->accel1, &act->accel2);
1162 /* else keys just swapped */
1163 } else if (g_strcmp0(act->accel2, data->accel2) == 0) {
1164 /* just accel1 is changed */
1165 replace_key(act->data1, data->accel1, &act->accel1);
1166 } else if (g_strcmp0(act->accel2, data->accel1) == 0) {
1167 /* replace accel1 with data->accel2 */
1168 replace_key(act->data1, data->accel2, &act->accel1);
1169 } else {
1170 /* both keys changed */
1171 replace_key(act->data1, data->accel1, &act->accel1);
1172 replace_key(act->data2, data->accel2, &act->accel2);
1173 }
1174 }
1175 }
1176 /* if not found then add a new keybinding */
1177 } else if (data->accel1) {
1178 act = g_new0(LXHotkeyGlobal, 1);
1179 act->data1 = make_new_xml_binding(cfg, data->actions, data->accel1, &act->actions, NULL);
1180 act->accel1 = g_strdup(data->accel1);
1181 /* do the same for accel2 if requested */
1182 if (data->accel2) {
1183 act->data2 = make_new_xml_binding(cfg, data->actions, data->accel2, NULL, NULL);
1184 act->accel2 = g_strdup(data->accel2);
1185 }
1186 cfg->actions = g_list_prepend(cfg->actions, act);
1187 }
1188 return restart_openbox(error);
1189 }
1190
1191 static GList *obcfg_get_app_keys(gpointer config, const char *mask, GError **error)
1192 {
1193 ObXmlFile *cfg = (ObXmlFile *)config;
1194 GList *list = NULL, *l;
1195 LXHotkeyApp *data;
1196
1197 if (cfg == NULL)
1198 g_set_error_literal(error, LXKEYS_OB_ERROR, LXKEYS_FILE_ERROR,
1199 _("No WM configuration is available."));
1200 else for (l = cfg->execs; l; l = l->next) {
1201 data = l->data;
1202 if (mask == NULL || fnmatch(mask, data->accel1, 0) == 0
1203 || (data->accel2 != NULL && fnmatch(mask, data->accel2, 0) == 0))
1204 list = g_list_prepend(list, data);
1205 }
1206 return list;
1207 }
1208
1209 static gboolean obcfg_set_app_key(gpointer config, LXHotkeyApp *data, GError **error)
1210 {
1211 ObXmlFile *cfg = (ObXmlFile *)config;
1212 GList *l;
1213 LXHotkeyApp *app;
1214
1215 if (cfg == NULL) {
1216 g_set_error_literal(error, LXKEYS_OB_ERROR, LXKEYS_FILE_ERROR,
1217 _("No WM configuration is available."));
1218 return FALSE;
1219 } else if (data->exec == NULL || data->exec[0] == '\0') {
1220 g_set_error_literal(error, LXKEYS_OB_ERROR, LXKEYS_PARSE_ERROR,
1221 _("The exec line cannot be empty."));
1222 return FALSE;
1223 }
1224 /* find if those keys are already bound elsewhere */
1225 for (l = cfg->actions; l; l = l->next) {
1226 if (data->accel1) {
1227 if (strcmp(data->accel1, ((LXHotkeyGlobal *)l->data)->accel1) == 0 ||
1228 g_strcmp0(data->accel1, ((LXHotkeyGlobal *)l->data)->accel2) == 0)
1229 goto _accel1_bound;
1230 }
1231 if (data->accel2) {
1232 if (g_strcmp0(data->accel2, ((LXHotkeyGlobal *)l->data)->accel1) == 0 ||
1233 g_strcmp0(data->accel2, ((LXHotkeyGlobal *)l->data)->accel2) == 0)
1234 goto _accel2_bound;
1235 }
1236 }
1237 for (l = cfg->execs; l; l = l->next) {
1238 if (data->accel1) {
1239 if (strcmp(data->accel1, ((LXHotkeyApp *)l->data)->accel1) == 0 ||
1240 g_strcmp0(data->accel1, ((LXHotkeyApp *)l->data)->accel2) == 0) {
1241 _accel1_bound:
1242 g_set_error(error, LXKEYS_OB_ERROR, LXKEYS_FILE_ERROR,
1243 _("Hotkey '%s' is already bound to an action."),
1244 data->accel1);
1245 return FALSE;
1246 }
1247 }
1248 if (data->accel2) {
1249 if (g_strcmp0(data->accel2, ((LXHotkeyApp *)l->data)->accel1) == 0 ||
1250 g_strcmp0(data->accel2, ((LXHotkeyApp *)l->data)->accel2) == 0) {
1251 _accel2_bound:
1252 g_set_error(error, LXKEYS_OB_ERROR, LXKEYS_FILE_ERROR,
1253 _("Hotkey '%s' is already bound to an action."),
1254 data->accel2);
1255 return FALSE;
1256 }
1257 }
1258 }
1259 /* find if that action(s) is present */
1260 for (l = cfg->execs; l; l = l->next)
1261 if (g_strcmp0((app = l->data)->exec, data->exec) == 0
1262 && options_equal(app->options, data->options))
1263 break;
1264 /* if found then either change keys or remove the keybinding */
1265 if (l != NULL) {
1266 if (data->accel1 == NULL) {
1267 /* removal requested */
1268 if (app->data1)
1269 fm_xml_file_item_destroy(app->data1);
1270 if (app->data2)
1271 fm_xml_file_item_destroy(app->data2);
1272 lkxeys_app_free(app);
1273 cfg->execs = g_list_delete_link(cfg->execs, l);
1274 } else {
1275 if (data->accel2 == NULL) {
1276 /* new data contains only one binding */
1277 if (g_strcmp0(app->accel1, data->accel1) == 0) {
1278 /* accel1 not changed, just clear accel2 */
1279 if (app->data2)
1280 fm_xml_file_item_destroy(app->data2);
1281 g_free(app->accel2);
1282 app->accel2 = NULL;
1283 } else if (g_strcmp0(app->accel2, data->accel1) == 0) {
1284 /* accel1 was removed */
1285 if (app->data1)
1286 fm_xml_file_item_destroy(app->data1);
1287 g_free(app->accel1);
1288 app->accel1 = app->accel2;
1289 app->accel2 = NULL;
1290 } else {
1291 /* full change */
1292 replace_key(app->data1, data->accel1, &app->accel1);
1293 if (app->data2)
1294 fm_xml_file_item_destroy(app->data2);
1295 g_free(app->accel2);
1296 app->accel2 = NULL;
1297 }
1298 } else if (app->accel2 == NULL) {
1299 /* new data has two bindings and old data has 1 */
1300 if (g_strcmp0(app->accel1, data->accel1) == 0) {
1301 /* add data->accel2 */
1302 app->data2 = make_new_xml_binding(cfg, data->options, data->accel2, NULL, data->exec);
1303 app->accel2 = g_strdup(data->accel2);
1304 } else if (g_strcmp0(app->accel1, data->accel2) == 0) {
1305 /* add data->accel1 */
1306 app->data2 = make_new_xml_binding(cfg, data->options, data->accel1, NULL, data->exec);
1307 app->accel2 = g_strdup(data->accel1);
1308 } else {
1309 /* replace key app->accel1 and add data->accel2 */
1310 replace_key(app->data1, data->accel1, &app->accel1);
1311 app->data2 = make_new_xml_binding(cfg, data->options, data->accel2, NULL, data->exec);
1312 app->accel2 = g_strdup(data->accel2);
1313 }
1314 } else {
1315 /* both keys are present in old and new data */
1316 if (g_strcmp0(app->accel1, data->accel1) == 0) {
1317 if (g_strcmp0(app->accel2, data->accel2) != 0)
1318 /* just accel2 is changed */
1319 replace_key(app->data2, data->accel2, &app->accel2);
1320 /* else nothing changed */
1321 } else if (g_strcmp0(app->accel1, data->accel2) == 0) {
1322 if (g_strcmp0(app->accel2, data->accel1) != 0)
1323 /* replace accel2 with data->accel1 */
1324 replace_key(app->data2, data->accel1, &app->accel2);
1325 /* else keys just swapped */
1326 } else if (g_strcmp0(app->accel2, data->accel2) == 0) {
1327 /* just accel1 is changed */
1328 replace_key(app->data1, data->accel1, &app->accel1);
1329 } else if (g_strcmp0(app->accel2, data->accel1) == 0) {
1330 /* replace accel1 with data->accel2 */
1331 replace_key(app->data1, data->accel2, &app->accel1);
1332 } else {
1333 /* both keys changed */
1334 replace_key(app->data1, data->accel1, &app->accel1);
1335 replace_key(app->data2, data->accel2, &app->accel2);
1336 }
1337 }
1338 }
1339 /* if not found then add a new keybinding */
1340 } else if (data->accel1) {
1341 app = g_new0(LXHotkeyApp, 1);
1342 app->exec = g_strdup(data->exec);
1343 app->data1 = make_new_xml_binding(cfg, data->options, data->accel1, &app->options, data->exec);
1344 app->accel1 = g_strdup(data->accel1);
1345 /* do the same for accel2 if requested */
1346 if (data->accel2) {
1347 app->data2 = make_new_xml_binding(cfg, data->options, data->accel2, NULL, data->exec);
1348 app->accel2 = g_strdup(data->accel2);
1349 }
1350 cfg->execs = g_list_prepend(cfg->execs, app);
1351 }
1352 return restart_openbox(error);
1353 }
1354
1355 static GList *obcfg_get_wm_actions(gpointer config, GError **error)
1356 {
1357 if (!available_wm_actions)
1358 available_wm_actions = convert_options(list_actions);
1359 return available_wm_actions;
1360 }
1361
1362
1363 static GList *obcfg_get_app_options(gpointer config, GError **error)
1364 {
1365 if (!available_wm_actions)
1366 available_wm_actions = convert_options(list_actions);
1367 return available_app_options;
1368 }
1369
1370
1371 FM_DEFINE_MODULE(lxhotkey, Openbox)
1372
1373 LXHotkeyPluginInit fm_module_init_lxhotkey = {
1374 .load = obcfg_load,
1375 .save = obcfg_save,
1376 .free = obcfg_free,
1377 .get_wm_keys = obcfg_get_wm_keys,
1378 .set_wm_key = obcfg_set_wm_key,
1379 .get_wm_actions = obcfg_get_wm_actions,
1380 .get_app_keys = obcfg_get_app_keys,
1381 .set_app_key = obcfg_set_app_key,
1382 .get_app_options = obcfg_get_app_options
1383 };