First beta stage: Openbox plugin implemented, commandline works OK.
authorAndriy Grytsenko <andrej@rep.kiev.ua>
Thu, 10 Mar 2016 19:01:45 +0000 (21:01 +0200)
committerAndriy Grytsenko <andrej@rep.kiev.ua>
Thu, 10 Mar 2016 19:01:45 +0000 (21:01 +0200)
.gitignore
Makefile.am
configure.ac
lxkeys.pc.in [new file with mode: 0644]
plugins/Makefile.am
plugins/openbox.c
po/POTFILES.in
po/lxkeys.pot
src/Makefile.am
src/lxkeys.c
src/lxkeys.h

index 3a50a74..77caac1 100644 (file)
@@ -26,3 +26,5 @@ po/POTFILES
 *.lo
 *.la
 lxkeys
+*.pc
+*.tar.xz
index bbd3704..0c04fce 100644 (file)
@@ -2,6 +2,9 @@
 
 ACLOCAL_AMFLAGS= -I m4
 
-SUBDIRS = plugins src po
+SUBDIRS = src plugins po
 
-EXTRA_DIST = autogen.sh
+pkgconfigdir = $(libdir)/pkgconfig
+pkgconfig_DATA = lxkeys.pc
+
+EXTRA_DIST = autogen.sh lxkeys.pc.in
index 6d6276f..1885400 100644 (file)
@@ -60,6 +60,9 @@ PKG_CHECK_MODULES(PACKAGE, [$pkg_modules])
 AC_SUBST(PACKAGE_CFLAGS)
 AC_SUBST(PACKAGE_LIBS)
 
+dnl Test for libunistring for correct UTF-8 printf
+AC_CHECK_LIB(unistring, ulc_fprintf)
+
 dnl Fix invalid sysconfdir when --prefix=/usr
 if test `eval "echo $sysconfdir"` = /usr/etc
 then
@@ -69,6 +72,7 @@ fi
 dnl Finish all
 AC_CONFIG_FILES([
     Makefile
+    lxkeys.pc
     src/Makefile
     plugins/Makefile
     po/Makefile.in
diff --git a/lxkeys.pc.in b/lxkeys.pc.in
new file mode 100644 (file)
index 0000000..4d7b4f4
--- /dev/null
@@ -0,0 +1,12 @@
+prefix=@prefix@
+exec_prefix=@exec_prefix@
+pkglibdir=@libdir@/lxkeys
+includedir=@includedir@
+pluginsdir=${pkglibdir}
+
+Name: lxkeys
+Description: A lightweight global keyboard shortcuts configurator.
+Requires: libfm >= 1.2.0
+Version: @VERSION@
+Libs: 
+Cflags: -I${includedir}
index 0d91dde..927fd87 100644 (file)
@@ -8,7 +8,7 @@ AM_CPPFLAGS = \
 
 AM_LDFLAGS = \
        -module -avoid-version -shared -export-dynamic -no-undefined \
-       $(PACKAGE_LIBS)
+       -rpath $(libdir)/lxkeys $(PACKAGE_LIBS)
 
 ## modules list
 pkglibdir = $(libdir)/lxkeys
@@ -16,3 +16,11 @@ pkglib_LTLIBRARIES = ob.la
 
 ## Openbox module
 ob_la_SOURCES = openbox.c
+ob_la_LIBADD = -lfm-extra
+
+install-exec-hook:
+       rm -f $(DESTDIR)$(pkglibdir)/*.la
+
+PLUGINS_INSTALLED = $(pkglib_LTLIBRARIES:.la=.so)
+uninstall-hook:
+       cd $(DESTDIR)$(pkglibdir) && rm -f $(PLUGINS_INSTALLED) || true
index 2843233..680bdbc 100644 (file)
 
 #include "lxkeys.h"
 
+#include <X11/Xlib.h>
+#include <X11/Xatom.h>
+
 #include <libfm/fm-extra.h>
 #include <glib/gi18n.h>
 
+#include <fnmatch.h>
+
+#define LXKEYS_OB_ERROR lxkeys_ob_error_quark()
+static GQuark lxkeys_ob_error_quark(void)
+{
+    static GQuark q = 0;
+
+    if G_UNLIKELY(q == 0)
+        q = g_quark_from_static_string("lxkeys-ob-error");
+
+    return q;
+}
+enum LXKeysObError {
+    LXKEYS_FILE_ERROR,
+    LXKEYS_PARSE_ERROR
+};
+
+
+/* simple functions to manage LXKeysAttr data type */
+static inline LXKeysAttr *lxkeys_attr_new(void)
+{
+    return g_slice_new0(LXKeysAttr);
+}
+
+#define free_options(acts) g_list_free_full(acts, (GDestroyNotify)lkxeys_attr_free)
+
+static void lkxeys_attr_free(LXKeysAttr *data)
+{
+    g_free(data->name);
+    g_list_free_full(data->values, g_free);
+    free_options(data->subopts);
+    g_slice_free(LXKeysAttr, data);
+}
+
+static void lkxeys_action_free(LXKeysGlobal *data)
+{
+    free_options(data->actions);
+    g_free(data->accel1);
+    g_free(data->accel2);
+    g_free(data);
+}
+
+static void lkxeys_app_free(LXKeysApp *data)
+{
+    g_free(data->exec);
+    free_options(data->options);
+    g_free(data->accel1);
+    g_free(data->accel2);
+    g_free(data);
+}
+
+/* recursively compare two lists of LXKeysAttr */
+static gboolean options_equal(GList *opts1, GList *opts2)
+{
+    while (opts1 && opts2) {
+        LXKeysAttr *attr1 = opts1->data, *attr2 = opts2->data;
+
+        if (g_strcmp0(attr1->name, attr2->name) != 0)
+            return FALSE;
+        if (attr1->values && attr2->values) {
+            if (g_strcmp0(attr1->values->data, attr2->values->data) != 0)
+                return FALSE;
+        } else if (attr1->values || attr2->values)
+            return FALSE;
+        if (!options_equal(attr1->subopts, attr2->subopts))
+            return FALSE;
+        opts1 = opts1->next;
+        opts2 = opts2->next;
+    }
+    return (opts1 == NULL && opts2 == NULL);
+}
+
+/* convert from OB format (A-Return) into GDK format (<Alt>Return) */
+static gchar *obkey_to_key(const gchar *obkey)
+{
+    // FIXME: TODO
+    return g_strdup(obkey);
+}
+
+/* convert from GDK format (<Alt>Return) into OB format (A-Return) */
+static gchar *key_to_obkey(const gchar *key)
+{
+    // FIXME: TODO
+    return g_strdup(key);
+}
+
+
+static gboolean restart_openbox(GError **error)
+{
+    Display *dpy = XOpenDisplay(NULL);
+    XEvent ce;
+    Status st;
+    gboolean ret = TRUE;
+
+    ce.xclient.type = ClientMessage;
+    ce.xclient.message_type = XInternAtom(dpy, "_OB_CONTROL", True);
+    ce.xclient.display = dpy;
+    ce.xclient.window = RootWindow(dpy, DefaultScreen(dpy));
+    ce.xclient.format = 32;
+    ce.xclient.data.l[0] = 1; /* reconfigure */
+    ce.xclient.data.l[1] = 0;
+    ce.xclient.data.l[2] = 0;
+    ce.xclient.data.l[3] = 0;
+    ce.xclient.data.l[4] = 0;
+    if (ce.xclient.message_type == None) {
+        g_set_error_literal(error, LXKEYS_OB_ERROR, LXKEYS_FILE_ERROR,
+                            _("Failed to reconfigure Openbox."));
+        ret = FALSE;
+    } else
+        st = XSendEvent(dpy, ce.xclient.window, False,
+                        SubstructureNotifyMask | SubstructureRedirectMask, &ce);
+        // FIXME: inspect status!
+    XCloseDisplay(dpy);
+    return ret;
+}
+
+
+/*
+ * Actions/options list supported by Openbox.
+ *
+ * This array is a bit tricky since it does not contain GList pointers, but
+ * those pointers will be expanded on demand.
+ */
+
+#define TO_BE_CONVERTED(a) (GList *)(a)
+#define TO_BE_PREVIOUS TO_BE_CONVERTED(1) /* reuse GList */
+#define BOOLEAN_VALUES TO_BE_CONVERTED(2) /* reuse GList */
+
+static char * values_enabled[] = { N_("yes"), N_("no"), NULL };
+
+static LXKeysAttr options_startupnotify[] = {
+    { N_("enabled"), BOOLEAN_VALUES, NULL, FALSE },
+    { N_("wmclass"), NULL, NULL, FALSE },
+    { N_("name"), NULL, NULL, FALSE },
+    { N_("icon"), NULL, NULL, FALSE },
+    { NULL }
+};
+
+static LXKeysAttr options_Execute[] = {
+    { N_("command"), NULL, NULL, FALSE },
+    { N_("prompt"), NULL, NULL, FALSE },
+    { N_("startupnotify"), NULL, TO_BE_CONVERTED(options_startupnotify), FALSE },
+    { NULL }
+};
+
+static char * values_x[] = { "#", "%", N_("center"), NULL };
+static char * values_monitor[] = { N_("default"), N_("primary"), N_("mouse"),
+                                   N_("active"), N_("all"), "#", NULL };
+
+static LXKeysAttr options_position[] = {
+    { "x", TO_BE_CONVERTED(values_x), NULL, FALSE },
+    { "y", TO_BE_PREVIOUS, NULL, FALSE },
+    { N_("monitor"), TO_BE_CONVERTED(values_monitor), NULL, FALSE },
+    { NULL }
+};
+
+static LXKeysAttr options_ShowMenu[] = {
+    { N_("menu"), NULL, NULL, FALSE },
+    { N_("position"), NULL, TO_BE_CONVERTED(options_position), FALSE },
+    { NULL }
+};
+
+static char * values_dialog[] = { N_("list"), N_("icons"), N_("none"), NULL };
+
+static LXKeysAttr options_NextWindow[] = {
+    { N_("dialog"), TO_BE_CONVERTED(values_dialog), NULL, FALSE },
+    { N_("bar"), BOOLEAN_VALUES, NULL, FALSE },
+    { N_("raise"), BOOLEAN_VALUES, NULL, FALSE },
+    { N_("allDesktops"), BOOLEAN_VALUES, NULL, FALSE },
+    { N_("panels"), BOOLEAN_VALUES, NULL, FALSE },
+    { N_("desktop"), BOOLEAN_VALUES, NULL, FALSE },
+    { N_("linear"), BOOLEAN_VALUES, NULL, FALSE },
+    { N_("interactive"), BOOLEAN_VALUES, NULL, FALSE },
+    { N_("finalactions"), NULL, NULL, TRUE },
+    { NULL }
+};
+
+static char * values_direction[] = { N_("north"), N_("northeast"), N_("east"),
+                                     N_("southeast"), N_("south"), N_("southwest"),
+                                     N_("west"), N_("northwest"), NULL };
+
+static LXKeysAttr options_DirectionalCycleWindows[] = {
+    { N_("direction"), TO_BE_CONVERTED(values_direction), NULL, FALSE },
+    { N_("dialog"), BOOLEAN_VALUES, NULL, FALSE },
+    { N_("bar"), BOOLEAN_VALUES, NULL, FALSE },
+    { N_("raise"), BOOLEAN_VALUES, NULL, FALSE },
+    { N_("panels"), BOOLEAN_VALUES, NULL, FALSE },
+    { N_("desktops"), BOOLEAN_VALUES, NULL, FALSE },
+    { N_("finalactions"), NULL, NULL, TRUE },
+    { NULL }
+};
+
+static char * values_to[] = { "#",  N_("current"), N_("next"), N_("previous"),
+                              N_("last"), N_("north"), N_("up"), N_("south"),
+                              N_("down"), N_("west"), N_("left"), N_("east"),
+                              N_("right"), NULL };
+
+static LXKeysAttr options_GoToDesktop[] = {
+    { N_("to"), TO_BE_CONVERTED(values_to), NULL, FALSE },
+    { N_("wrap"), BOOLEAN_VALUES, NULL, FALSE },
+    { NULL }
+};
+
+static char * values_where[] = { N_("current"), N_("last"), NULL };
+
+static LXKeysAttr options_AddDesktop[] = {
+    { N_("where"), TO_BE_CONVERTED(values_where), NULL, FALSE },
+    { NULL }
+};
+
+static LXKeysAttr options_Restart[] = {
+    { N_("command"), NULL, NULL, FALSE },
+    { NULL }
+};
+
+static LXKeysAttr options_Exit[] = {
+    { N_("prompt"), BOOLEAN_VALUES, NULL, FALSE },
+    { NULL }
+};
+
+static char * values_directionM[] = { N_("both"), N_("horizontal"), N_("vertical"), NULL };
+
+static LXKeysAttr options_ToggleMaximize[] = {
+    { N_("direction"), TO_BE_CONVERTED(values_directionM), NULL, FALSE },
+    { NULL }
+};
+
+static char * values_toS[] = { "#", N_("current"), N_("next"), N_("previous"),
+                               N_("last"), N_("north"), N_("up"), N_("south"),
+                               N_("down"), N_("west"), N_("left"), N_("east"),
+                               N_("right"), NULL };
+
+static LXKeysAttr options_SendToDesktop[] = {
+    { N_("to"), TO_BE_CONVERTED(values_toS), NULL, FALSE },
+    { N_("wrap"), BOOLEAN_VALUES, NULL, FALSE },
+    { N_("follow"), BOOLEAN_VALUES, NULL, FALSE },
+    { NULL }
+};
+
+static char * values_edge[] = { N_("top"), N_("left"), N_("right"), N_("bottom"),
+                                N_("topleft"), N_("topright"), N_("bottomleft"),
+                                N_("bottomright"), NULL };
+
+static LXKeysAttr options_Resize[] = {
+    { N_("edge"), TO_BE_CONVERTED(values_edge), NULL, FALSE },
+    { NULL }
+};
+
+static char * values_xM[] = { "#", N_("current"), N_("center"), NULL };
+static char * values_width[] = { "#", "%", N_("current"), NULL };
+static char * values_monitorM[] = { "#", N_("current"), N_("all"), N_("next"), N_("prev"), NULL };
+
+static LXKeysAttr options_MoveResizeTo[] = {
+    { "x", TO_BE_CONVERTED(values_xM), NULL, FALSE },
+    { "y", TO_BE_PREVIOUS, NULL, FALSE },
+    { N_("width"), TO_BE_CONVERTED(values_width), NULL, FALSE },
+    { N_("height"), TO_BE_PREVIOUS, NULL, FALSE },
+    { N_("monitor"), TO_BE_CONVERTED(values_monitorM), NULL, FALSE },
+    { NULL }
+};
+
+static char * values_xR[] = { "#", NULL };
+
+static LXKeysAttr options_MoveRelative[] = {
+    { "x", TO_BE_CONVERTED(values_xR), NULL, FALSE },
+    { "y", TO_BE_PREVIOUS, NULL, FALSE },
+    { NULL }
+};
+
+static char * values_xRR[] = { "#", NULL };
+
+static LXKeysAttr options_ResizeRelative[] = {
+    { N_("left"), TO_BE_CONVERTED(values_xRR), NULL, FALSE },
+    { N_("right"), TO_BE_PREVIOUS, NULL, FALSE },
+    { N_("top"), TO_BE_PREVIOUS, NULL, FALSE },
+    { N_("bottom"), TO_BE_PREVIOUS, NULL, FALSE },
+    { NULL }
+};
+
+static char * values_directionE[] = { N_("north"), N_("south"), N_("west"), N_("east"), NULL };
+
+static LXKeysAttr options_MoveToEdge[] = {
+    { N_("direction"), TO_BE_CONVERTED(values_directionE), NULL, FALSE },
+    { NULL }
+};
+
+static char * values_layer[] = { N_("top"), N_("normal"), N_("bottom"), NULL };
+
+static LXKeysAttr options_SendToLayer[] = {
+    { N_("layer"), TO_BE_CONVERTED(values_layer), NULL, FALSE },
+    { NULL }
+};
+
+static LXKeysAttr list_actions[] = {
+    /* global actions */
+    { N_("Execute"), NULL, TO_BE_CONVERTED(options_Execute), FALSE },
+    { N_("ShowMenu"), NULL, TO_BE_CONVERTED(options_ShowMenu), FALSE },
+    { N_("NextWindow"), NULL, TO_BE_CONVERTED(options_NextWindow), FALSE },
+    { N_("PreviousWindow"), NULL, TO_BE_PREVIOUS, FALSE },
+    { N_("DirectionalCycleWindows"), NULL, TO_BE_CONVERTED(options_DirectionalCycleWindows), FALSE },
+    { N_("DirectionalTargetWindow"), NULL, TO_BE_PREVIOUS, FALSE },
+    { N_("GoToDesktop"), NULL, TO_BE_CONVERTED(options_GoToDesktop), FALSE },
+    { N_("AddDesktop"), NULL, TO_BE_CONVERTED(options_AddDesktop), FALSE },
+    { N_("RemoveDesktop"), NULL, TO_BE_PREVIOUS, FALSE },
+    { N_("ToggleDockAutohide"), NULL, NULL, FALSE },
+    { N_("Reconfigure"), NULL, NULL, FALSE },
+    { N_("Restart"), NULL, TO_BE_CONVERTED(options_Restart), FALSE },
+    { N_("Exit"), NULL, TO_BE_CONVERTED(options_Exit), FALSE },
+    /* windows actions */
+    { N_("Focus"), NULL, NULL, FALSE },
+    { N_("Raise"), NULL, NULL, FALSE },
+    { N_("Lower"), NULL, NULL, FALSE },
+    { N_("RaiseLower"), NULL, NULL, FALSE },
+    { N_("Unfocus"), NULL, NULL, FALSE },
+    { N_("FocusToBottom"), NULL, NULL, FALSE },
+    { N_("Iconify"), NULL, NULL, FALSE },
+    { N_("Close"), NULL, NULL, FALSE },
+    { N_("ToggleShade"), NULL, NULL, FALSE },
+    { N_("Shade"), NULL, NULL, FALSE },
+    { N_("Unshade"), NULL, NULL, FALSE },
+    { N_("ToggleOmnipresent"), NULL, NULL, FALSE },
+    { N_("ToggleMaximize"), NULL, TO_BE_CONVERTED(options_ToggleMaximize), FALSE },
+    { N_("Maximize"), NULL, TO_BE_PREVIOUS, FALSE },
+    { N_("Unmaximize"), NULL, TO_BE_PREVIOUS, FALSE },
+    { N_("ToggleFullscreen"), NULL, NULL, FALSE },
+    { N_("ToggleDecorations"), NULL, NULL, FALSE },
+    { N_("Decorate"), NULL, NULL, FALSE },
+    { N_("Undecorate"), NULL, NULL, FALSE },
+    { N_("SendToDesktop"), NULL, TO_BE_CONVERTED(options_SendToDesktop), FALSE },
+    { N_("Move"), NULL, NULL, FALSE },
+    { N_("Resize"), NULL, TO_BE_CONVERTED(options_Resize), FALSE },
+    { N_("MoveResizeTo"), NULL, TO_BE_CONVERTED(options_MoveResizeTo), FALSE },
+    { N_("MoveRelative"), NULL, TO_BE_CONVERTED(options_MoveRelative), FALSE },
+    { N_("ResizeRelative"), NULL, TO_BE_CONVERTED(options_ResizeRelative), FALSE },
+    { N_("MoveToEdge"), NULL, TO_BE_CONVERTED(options_MoveToEdge), FALSE },
+    { N_("GrowToEdge"), NULL, TO_BE_PREVIOUS, FALSE },
+    { N_("ShrinkToEdge"), NULL, TO_BE_PREVIOUS, FALSE },
+    { N_("GrowToFill"), NULL, NULL, FALSE },
+    { N_("ToggleAlwaysOnTop"), NULL, NULL, FALSE },
+    { N_("ToggleAlwaysOnBottom"), NULL, NULL, FALSE },
+    { N_("SendToLayer"), NULL, TO_BE_CONVERTED(options_SendToLayer), FALSE },
+    // FIXME: support for If/ForEach/Stop ?
+    { NULL }
+};
+
+static GList *boolean_values = NULL;
+static GList *available_wm_actions = NULL;
+static GList *available_app_options = NULL;
+
+static GList *convert_values(gpointer data)
+{
+    char ** array;
+    GList *list = NULL;
+
+    for (array = data; array[0] != NULL; array++) {
+        g_debug("creating GList for string '%s'", array[0]);
+        list = g_list_prepend(list, array[0]);
+    }
+    return g_list_reverse(list);
+}
+
+static GList *convert_options(gpointer data)
+{
+    LXKeysAttr *array, *last = NULL;
+    GList *list = NULL;
+
+    for (array = data; array->name != NULL; array++) {
+        list = g_list_prepend(list, array);
+        if (last && array->values == TO_BE_PREVIOUS)
+            array->values = last->values;
+        else if (array->values == BOOLEAN_VALUES) {
+            if (boolean_values == NULL)
+                boolean_values = convert_values(values_enabled);
+            array->values = boolean_values;
+        } else if (array->values != NULL)
+            array->values = convert_values(array->values);
+        if (last && array->subopts == TO_BE_PREVIOUS)
+            array->subopts = last->subopts;
+        else if (array->subopts != NULL) {
+            if (array->subopts == TO_BE_CONVERTED(options_Execute))
+                array->subopts = available_app_options = convert_options(array->subopts);
+            else
+                array->subopts = convert_options(array->subopts);
+        }
+        last = array;
+    }
+    return g_list_reverse(list);
+}
+
+
+typedef struct {
+    FmXmlFileItem *parent; /* R/O */
+    GList *list; /* contains LXKeysAttr items for finished actions */
+} ObActionsList;
+
 typedef struct {
     char *path;
     FmXmlFile *xml;
-    GList *actions; /* no-exec actions */
-    GList *execs; /* exec-only actions */
-    //GList *available_wm_actions;
-    //GList *available_app_actions;
+    FmXmlFileItem *keyboard; /* the <keyboard> section */
+    GList *actions; /* no-exec actions, in reverse order */
+    GList *execs; /* exec-only actions, in reverse order */
+    GList *stack; /* only for build - elements are ObActionsList */
+    GList *added_tags; /* only for edit */
 } ObXmlFile;
 
 static FmXmlFileTag ObXmlFile_keyboard; /* section that we work on */
@@ -48,7 +447,15 @@ static gboolean tag_handler_keyboard(FmXmlFileItem *item, GList *children,
                                      guint n_attributes, gint line, gint pos,
                                      GError **error, gpointer user_data)
 {
-    /* anything to do? */
+    ObXmlFile *cfg = user_data;
+
+    if (cfg->keyboard) {
+        /* FIXME: merge duplicate section? */
+        g_set_error_literal(error, LXKEYS_OB_ERROR, LXKEYS_PARSE_ERROR,
+                            _("Duplicate <keyboard> section in the rc.xml file."));
+        return FALSE;
+    }
+    cfg->keyboard = item;
     return TRUE;
 }
 
@@ -58,17 +465,265 @@ static gboolean tag_handler_keybind(FmXmlFileItem *item, GList *children,
                                     guint n_attributes, gint line, gint pos,
                                     GError **error, gpointer user_data)
 {
+    ObXmlFile *cfg = user_data;
+    ObActionsList *oblist;
+    GList *actions, *l;
+    gchar *key;
+    const char *exec_line = NULL;
+    LXKeysAttr *action;
+    LXKeysApp *app = NULL;
+    LXKeysGlobal *act;
+    guint i;
+
+    if (!cfg->stack || cfg->stack->next) { /* corruption! */
+        g_set_error_literal(error, LXKEYS_OB_ERROR, LXKEYS_PARSE_ERROR,
+                            _("Internal error."));
+        return FALSE;
+    }
+    oblist = cfg->stack->data;
+    if (oblist->parent != item) { /* corruption! */
+        g_set_error_literal(error, LXKEYS_OB_ERROR, LXKEYS_PARSE_ERROR,
+                            _("Internal error."));
+        return FALSE;
+    }
+    /* just remove top stack item, all actions are already there */
+    actions = oblist->list;
+    g_free(oblist);
+    g_list_free(cfg->stack);
+    cfg->stack = NULL;
+    action = actions->data;
     /* decide where to put: execs or actions */
-    /* create LXKeysApp or LXKeysGlobal */
+    if (children && !children->next && /* exactly one child which is an action */
+        fm_xml_file_item_get_tag(children->data) == ObXmlFile_action) {
+        if (strcmp(action->name, "Execute") == 0) { /* and action is Execute */
+            FmXmlFileItem *exec = fm_xml_file_item_find_child(children->data,
+                                                              ObXmlFile_command);
+            if (!exec)
+                exec = fm_xml_file_item_find_child(children->data, ObXmlFile_execute);
+            /* if exec is NULL then it's invalid action after all */
+            if (exec)
+            {
+                /* not empty exec line was verified in the handler */
+                exec_line = fm_xml_file_item_get_data(fm_xml_file_item_find_child(exec, FM_XML_FILE_TEXT), NULL);
+                for (l = cfg->execs; l; l = l->next)
+                    /* if exec line is equal to one gathered already */
+                    if (strcmp(((LXKeysApp *)l->data)->exec, exec_line) == 0 &&
+                        /* and it has no secondary keybinding */
+                        ((LXKeysApp *)l->data)->accel2 == NULL &&
+                        /* and options are also equal */
+                        options_equal(((LXKeysApp *)l->data)->options, action->subopts))
+                    {
+                        /* then just add this keybinding to found one */
+                        app = (LXKeysApp *)l->data;
+                        break;
+                    }
+            }
+        }
+    }
+    /* find a "key" attribute and save its value as accel1 */
+    for (i = 0; i < n_attributes; i++)
+        if (g_strcmp0(attribute_names[i], "key") == 0)
+            break;
+    if (i == n_attributes) { /* no name in XML! */
+        g_set_error_literal(error, LXKEYS_OB_ERROR, LXKEYS_PARSE_ERROR,
+                            _("rc.xml error: no key is set for a keybind."));
+        free_options(actions);
+        return FALSE;
+    } else
+        key = obkey_to_key(attribute_values[i]);
+    /* if that exec already exists then reuse it and set as accel2 */
+    if (app) {
+        app->accel2 = key;
+        app->data2 = item;
+    /* otherwise create LXKeysApp or LXKeysGlobal and add it to the list */
+    } else if (exec_line) {
+        app = g_new0(LXKeysApp, 1);
+        app->accel1 = key;
+        app->exec = g_strdup(exec_line);
+        app->data1 = item;
+        app->options = action->subopts;
+        /* remove exec line from options, it should be in XML but not in LXKeysApp */
+        for (l = app->options; l; ) {
+            LXKeysAttr *opt = l->data;
+            l = l->next;
+            if (strcmp(opt->name, "command") == 0 || strcmp(opt->name, "execute") == 0) {
+                app->options = g_list_remove(app->options, opt);
+                lkxeys_attr_free(opt);
+            }
+        }
+        action->subopts = NULL;
+        cfg->execs = g_list_prepend(cfg->execs, app);
+    } else {
+        for (l = cfg->actions; l; l = l->next)
+            /* if the same actions list was gathered already */
+            if (options_equal(((LXKeysGlobal *)l->data)->actions, actions)
+                /* and it has no secondary keybinding */
+                && ((LXKeysGlobal *)l->data)->accel2 == NULL)
+                break;
+        if (l == NULL) {
+            act = g_new0(LXKeysGlobal, 1);
+            act->accel1 = key;
+            act->data1 = item;
+            act->actions = actions;
+            actions = NULL;
+            cfg->actions = g_list_prepend(cfg->actions, act);
+        } else { /* actions exist in list so reuse it adding a second keybinding */
+            act->accel2 = key;
+            act->data2 = item;
+        }
+    }
+    free_options(actions);
+    return TRUE;
+}
+
+/* collect all children of FmXmlFileItem into LXKeysAttr list
+   removing from stack if found there
+   since actions cannot be mixed with options, just
+   ignore anything not collected into any ObActionsList */
+static GList *resolve_item(GList **stack, GList *children, GList **value,
+                           GError **error)
+{
+    GList *child, *l, *items = NULL;
+    ObActionsList *act; /* stack item */
+    FmXmlFileItem *item; /* child item */
+    LXKeysAttr *data;
+
+    for (child = children; child; child = child->next) {
+        item = child->data;
+        if (fm_xml_file_item_get_tag(item) == FM_XML_FILE_TEXT) { /* value! */
+            *value = g_list_prepend(*value,
+                                    g_strdup(fm_xml_file_item_get_data(item, NULL)));
+            continue;
+        }
+        if (fm_xml_file_item_get_tag(item) == ObXmlFile_action) { /* stray action? */
+            g_set_error_literal(error, LXKEYS_OB_ERROR, LXKEYS_PARSE_ERROR,
+                                _("Invalid rc.xml: action with a sub-action."));
+            free_options(items);
+            return NULL;
+        }
+        data = lxkeys_attr_new();
+        data->name = g_strdup(fm_xml_file_item_get_tag_name(item));
+        /* find if item is in stack, then remove from there and use ready list */
+        for (l = *stack; l; l = l->next) {
+            act = l->data;
+            if (act->parent == item) {
+                *stack = g_list_delete_link(*stack, l);
+                data->subopts = act->list;
+                data->has_actions = TRUE;
+                g_free(act); /* release the ObActionsList */
+                break;
+            }
+        }
+        /* otherwise collect children by recursive call */
+        if (l == NULL) {
+            GError *err = NULL;
+
+            l = fm_xml_file_item_get_children(item);
+            data->subopts = resolve_item(stack, l, &data->values, &err);
+            g_list_free(l);
+            if (err) {
+                g_propagate_error(error, err);
+                free_options(items);
+                lkxeys_attr_free(data);
+                return NULL;
+            }
+        }
+        /* add the item to the list */
+        items = g_list_prepend(items, data);
+    }
+    return g_list_reverse(items);
+}
+
+static gboolean is_on_stack(GList *stack, FmXmlFileItem *item)
+{
+    while (stack) {
+        if (((ObActionsList *)stack->data)->parent == item)
+            return TRUE;
+        stack = stack->next;
+    }
+    return FALSE;
+}
+
+/* push new item on the stack, return top item */
+static ObActionsList *put_on_stack_top(ObXmlFile *cfg, FmXmlFileItem *parent)
+{
+    ObActionsList *oblist = g_new0(ObActionsList, 1);
+
+    oblist->parent = parent;
+    cfg->stack = g_list_prepend(cfg->stack, oblist);
+    return oblist;
 }
 
+/* (parent/children) stack (changes)
+<k><a1><f><a2/> = (f/-) f (+f)
+             +<a3><x><a4/> => (x/-) f x:4 (+x)
+             +<a5/> => (x/-) f x:45 ()
+             +</x></a3> => (f/x:45) f:3-x:45 (-x)
+             +</f></a1> => (k/f:3) k:1-f:3 (-f +k)
+             +<a6><h><a7><t><a8/> => (t/-) k:1 t:8 (+t)
+             +</t></a7> => (h/t:8) k:1 h:7-t:8 (-t +h)
+             +</h></a6> => (k/h:7) k:16-h:7 (-h)
+             +<a9><b><c><d><a0><e><g><az/> => (g/-) k:16 g:z (+g)
+             +</g></e></a0> => (d/e-g:z) k:16 d:0-e-g:z (-g +d)
+             +</d></c></b></a9> => (k/b-c-d:0) k:169-b-c-d:0 (-d)
+             +<a3><l><m><ay/> => (m/-) k:169 m:y (+m)
+             +</m><n><a2><o/></a2> => (n/o) k:169 m:y n:2-o (+n)
+             +</n></l></a3> => (k/l-{m:y|n:2}) k:1693-l-{m:y|n:2} (-m -n)
+             +</k> => (-/1693) - (-k)
+*/
+
 static gboolean tag_handler_action(FmXmlFileItem *item, GList *children,
                                    char * const *attribute_names,
                                    char * const *attribute_values,
                                    guint n_attributes, gint line, gint pos,
                                    GError **error, gpointer user_data)
 {
-    
+    /* if parent is on top of stack then it's another action of the same parent */
+    /* if parent exists deeper on stack after resolving then it's curruption */
+    /* if parent doesn't exist on stack but some of children is then that child
+       is finished, replace with this parent on stack after resolving children */
+    /* if parent doesn't exist on stack neither any child is then it got
+       deeper so just add it on stack */
+    ObXmlFile *cfg = user_data;
+    LXKeysAttr *data;
+    ObActionsList *oblist;
+    FmXmlFileItem *parent;
+    GError *err = NULL;
+    guint i;
+
+    /* create a LXKeysAttr */
+    data = lxkeys_attr_new();
+    //data->has_actions = FALSE; /* action can have only options, not sub-actions! */
+    /* resolve all children of this action, clearing from stack */
+    data->subopts = resolve_item(&cfg->stack, children, &data->values, &err);
+    if (err) {
+        g_propagate_error(error, err);
+        lkxeys_attr_free(data);
+        return FALSE;
+    }
+    /* find a "name" attribute and set it as name */
+    for (i = 0; i < n_attributes; i++)
+        if (g_strcmp0(attribute_names[i], "name") == 0)
+            break;
+    if (i == n_attributes) { /* no name in XML! */
+        g_set_error_literal(error, LXKEYS_OB_ERROR, LXKEYS_PARSE_ERROR,
+                            _("rc.xml error: no name is set for action."));
+        lkxeys_attr_free(data);
+        return FALSE;
+    } else
+        data->name = g_strdup(attribute_values[i]);
+    /* add this action to the parent's list */
+    parent = fm_xml_file_item_get_parent(item);
+    if (!is_on_stack(cfg->stack, parent))
+        oblist = put_on_stack_top(cfg, parent);
+    else if ((oblist = cfg->stack->data)->parent != parent) { /* corruption */
+        g_set_error_literal(error, LXKEYS_OB_ERROR, LXKEYS_PARSE_ERROR,
+                            _("Internal error."));
+        lkxeys_attr_free(data);
+        return FALSE;
+    }
+    oblist->list = g_list_append(oblist->list, data);
+    return TRUE;
 }
 
 static gboolean tag_handler_command(FmXmlFileItem *item, GList *children,
@@ -77,11 +732,38 @@ static gboolean tag_handler_command(FmXmlFileItem *item, GList *children,
                                     guint n_attributes, gint line, gint pos,
                                     GError **error, gpointer user_data)
 {
+    FmXmlFileItem *text = fm_xml_file_item_find_child(item, FM_XML_FILE_TEXT);
+
+    if (text == NULL) {
+        /* check if value is not empty */
+        g_set_error(error, LXKEYS_OB_ERROR, LXKEYS_PARSE_ERROR,
+                    _("rc.xml error: empty tag <%s> is prohibited."),
+                    fm_xml_file_item_get_tag_name(item));
+        return FALSE;
+    }
+    return TRUE;
+}
+
+
+static void obcfg_free(gpointer config)
+{
+    ObXmlFile *cfg = (ObXmlFile *)config;
+
+    g_free(cfg->path);
+    g_object_unref(cfg->xml);
+    g_list_free_full(cfg->actions, (GDestroyNotify)lkxeys_action_free);
+    g_list_free_full(cfg->execs, (GDestroyNotify)lkxeys_app_free);
+    // FIXME: free cfg->stack !
+    g_list_free(cfg->added_tags);
+    g_free(cfg);
 }
 
 static gpointer obcfg_load(gpointer config, GError **error)
 {
     ObXmlFile *cfg = (ObXmlFile *)config;
+    gchar *contents = NULL;
+    GError *err = NULL;
+    gsize len;
 
     if (config) {
         /* just discard any changes if any exist */
@@ -89,7 +771,14 @@ static gpointer obcfg_load(gpointer config, GError **error)
 
         cfg->xml = fm_xml_file_new(old_xml);
         g_object_unref(old_xml);
+        g_list_free_full(cfg->actions, (GDestroyNotify)lkxeys_action_free);
+        g_list_free_full(cfg->execs, (GDestroyNotify)lkxeys_app_free);
+        cfg->actions = NULL;
+        cfg->execs = NULL;
+        cfg->keyboard = NULL;
     } else {
+        const char *session;
+
         /* prepare data */
         cfg = g_new0(ObXmlFile, 1);
         cfg->xml = fm_xml_file_new(NULL);
@@ -104,43 +793,514 @@ static gpointer obcfg_load(gpointer config, GError **error)
                                                      &tag_handler_command, FALSE, NULL);
         ObXmlFile_execute = fm_xml_file_set_handler(cfg->xml, "execute",
                                                      &tag_handler_command, FALSE, NULL);
-        
         /* let try to detect rc.xml file currently in use:
            with Lubuntu it's lubuntu-rc.xml, with lxde session it's lxde-rc.xml */
-        
+        session = g_getenv("DESKTOP_SESSION");
+        if (session == NULL)
+            session = g_getenv("GDMSESSION");
+        if (session == NULL)
+            session = g_getenv("XDG_CURRENT_DESKTOP");
+        if (g_strcmp0(session, "Lubuntu") == 0)
+            cfg->path = g_build_filename(g_get_user_config_dir(), "openbox",
+                                         "lubuntu-rc.xml", NULL);
+        else if (g_strcmp0(session, "LXDE") == 0)
+            cfg->path = g_build_filename(g_get_user_config_dir(), "openbox",
+                                         "lxde-rc.xml", NULL);
+        else
+            cfg->path = g_build_filename(g_get_user_config_dir(), "openbox",
+                                         "rc.xml", NULL);
     }
+
     /* try to load ~/.config/openbox/$xml */
-    
-    /* if it does not exist then try to load $XDG_SYSTEM_CONFDIR/openbox/rc.xml */
-    
+    if (!g_file_get_contents(cfg->path, &contents, &len, NULL)) {
+        /* if it does not exist then try to load $XDG_SYSTEM_CONFDIR/openbox/rc.xml */
+        const gchar * const *dirs;
+        char *path = NULL;
 
+        for (dirs = g_get_system_config_dirs(); dirs[0]; dirs++) {
+            path = g_build_filename(dirs[0], "openbox", "rc.xml", NULL);
+            if (g_file_get_contents(path, &contents, &len, NULL))
+                break;
+            g_free(path);
+            path = NULL;
+        }
+        if (path == NULL) { /* failed to load */
+            g_set_error_literal(error, LXKEYS_OB_ERROR, LXKEYS_FILE_ERROR,
+                                _("Could not find the rc.xml file anywhere."));
+            obcfg_free(cfg);
+            return NULL;
+        }
+        g_free(path);
+    }
+    /* parse the found rc.xml file */
+    if (!fm_xml_file_parse_data(cfg->xml, contents, len, &err, cfg)
+        || fm_xml_file_finish_parse(cfg->xml, &err) == NULL) {
+        g_propagate_error(error, err);
+        g_free(contents);
+        obcfg_free(cfg);
+        return NULL;
+    }
+    g_free(contents);
     return cfg;
 }
 
 static gboolean obcfg_save(gpointer config, GError **error)
 {
     ObXmlFile *cfg = (ObXmlFile *)config;
+    char *contents;
+    gsize len;
+    gboolean ret = FALSE;
 
     /* save as ~/.config/openbox/$xml */
+    contents = fm_xml_file_to_data(cfg->xml, &len, error);
+    if (contents) {
+        /* workaround on libfm-extra bug on save data without DTD */
+        if (contents[0] == '\n')
+            ret = g_file_set_contents(cfg->path, contents+1, len-1, error);
+        else
+            ret = g_file_set_contents(cfg->path, contents, len, error);
+        g_free(contents);
+    }
+    return ret;
 }
 
-static void obcfg_free(gpointer config)
+static GList *obcfg_get_wm_keys(gpointer config, const char *mask, GError **error)
 {
     ObXmlFile *cfg = (ObXmlFile *)config;
+    GList *list = NULL, *l;
+    LXKeysGlobal *data;
 
-    g_free(cfg->path);
-    g_object_unref(cfg->xml);
-    g_free(cfg);
+    if (cfg == NULL)
+        g_set_error_literal(error, LXKEYS_OB_ERROR, LXKEYS_FILE_ERROR,
+                            _("No WM configuration is available."));
+    else for (l = cfg->actions; l; l = l->next) {
+        data = l->data;
+        if (mask == NULL || fnmatch(mask, data->accel1, 0) == 0
+            || (data->accel2 != NULL && fnmatch(mask, data->accel2, 0) == 0))
+            list = g_list_prepend(list, data);
+    }
+    return list;
 }
 
-static GList *obcfg_get_wm_keys(gpointer config, const char *mask, GError **error)
+static gboolean tag_null_handler(FmXmlFileItem *item, GList *children,
+                                 char * const *attribute_names,
+                                 char * const *attribute_values,
+                                 guint n_attributes, gint line, gint pos,
+                                 GError **error, gpointer user_data)
+{
+    return TRUE;
+}
+
+/* if opts==NULL then don't copy any LXKeysAttr below it */
+static FmXmlFileItem *make_new_xml_item(ObXmlFile *cfg, LXKeysAttr *opt,
+                                        GList **opts, gboolean is_action)
 {
+    FmXmlFileItem *item, *sub;
+    GList *l;
+    LXKeysAttr *act = NULL;
+
+    if (is_action) {
+        item = fm_xml_file_item_new(ObXmlFile_action);
+        fm_xml_file_item_set_attribute(item, "name", opt->name);
+    } else {
+        FmXmlFileTag tag = FM_XML_FILE_TAG_NOT_HANDLED;
+
+        /* find a tag in list by opt->name */
+        for (l = cfg->added_tags; l; l = l->next)
+            if (g_strcmp0(fm_xml_file_get_tag_name(cfg->xml, GPOINTER_TO_UINT(l->data)),
+                          opt->name) == 0)
+                break;
+        if (l == NULL) {
+            /* if not found then add to list */
+            tag = fm_xml_file_set_handler(cfg->xml, opt->name, &tag_null_handler, FALSE, NULL);
+            cfg->added_tags = g_list_prepend(cfg->added_tags, GUINT_TO_POINTER(tag));
+        } else
+            tag = GPOINTER_TO_UINT(l->data);
+        item = fm_xml_file_item_new(tag);
+        if (opt->values)
+            fm_xml_file_item_append_text(item, opt->values->data, -1, FALSE);
+    }
+    if (opts != NULL) {
+        /* make a copy if requested */
+        act = g_new0(LXKeysAttr, 1);
+        act->name = g_strdup(opt->name);
+        if (opt->values)
+            act->values = g_list_prepend(NULL, g_strdup(opt->values->data));
+        act->has_actions = opt->has_actions;
+        *opts = g_list_append(*opts, act);
+    }
+    for (l = opt->subopts; l; l = l->next) {
+        sub = make_new_xml_item(cfg, l->data, act ? &act->subopts : NULL,
+                                opt->has_actions);
+        fm_xml_file_item_append_child(item, sub);
+    }
+    return item;
+}
+
+/* if opts==NULL then don't make any LXKeysAttr below it */
+static FmXmlFileItem *make_new_xml_binding(ObXmlFile *cfg, GList *actions,
+                                           const gchar *accel, GList **opts,
+                                           const gchar *exec)
+{
+    FmXmlFileItem *binding = fm_xml_file_item_new(ObXmlFile_keybind);
+    FmXmlFileItem *item;
+    char *obkey = key_to_obkey(accel);
+
+    fm_xml_file_item_set_attribute(binding, "key", obkey);
+    g_free(obkey);
+    fm_xml_file_item_append_child(cfg->keyboard, binding);
+    if (exec) {
+        /* make <action name='Execute'><command>exec</command>..opts..</action>
+           instead of ..<acts>.. */
+        item = fm_xml_file_item_new(ObXmlFile_action);
+        fm_xml_file_item_set_attribute(item, "name", "Execute");
+        fm_xml_file_item_append_child(binding, item);
+        binding = item;
+        item = fm_xml_file_item_new(ObXmlFile_command);
+        fm_xml_file_item_append_text(item, exec, -1, FALSE);
+        fm_xml_file_item_append_child(binding, item);
+    }
+    for (; actions; actions = actions->next) {
+        item = make_new_xml_item(cfg, actions->data, opts, (exec == NULL));
+        fm_xml_file_item_append_child(binding, item);
+    }
+    return binding;
+}
+
+static inline void replace_key(FmXmlFileItem *item, const char *key, char **kptr)
+{
+    char *obkey = key_to_obkey(key);
+
+    fm_xml_file_item_set_attribute(item, "key", obkey);
+    g_free(obkey);
+    g_free(*kptr);
+    *kptr = g_strdup(key);
 }
 
 static gboolean obcfg_set_wm_key(gpointer config, LXKeysGlobal *data, GError **error)
 {
+    ObXmlFile *cfg = (ObXmlFile *)config;
+    GList *l;
+    LXKeysGlobal *act;
+
+    if (cfg == NULL) {
+        g_set_error_literal(error, LXKEYS_OB_ERROR, LXKEYS_FILE_ERROR,
+                            _("No WM configuration is available."));
+        return FALSE;
+    } else if (data->actions == NULL) {
+        g_set_error_literal(error, LXKEYS_OB_ERROR, LXKEYS_PARSE_ERROR,
+                            _("Keybinding should activate at least one action."));
+        return FALSE;
+    }
+    /* find if those keys are already bound elsewhere */
+    for (l = cfg->actions; l; l = l->next) {
+        if (data->accel1) {
+            if (strcmp(data->accel1, ((LXKeysGlobal *)l->data)->accel1) == 0 ||
+                g_strcmp0(data->accel1, ((LXKeysGlobal *)l->data)->accel2) == 0)
+                goto _accel1_bound;
+        }
+        if (data->accel2) {
+            if (g_strcmp0(data->accel2, ((LXKeysGlobal *)l->data)->accel1) == 0 ||
+                g_strcmp0(data->accel2, ((LXKeysGlobal *)l->data)->accel2) == 0)
+                goto _accel2_bound;
+        }
+    }
+    for (l = cfg->execs; l; l = l->next) {
+        if (data->accel1) {
+            if (strcmp(data->accel1, ((LXKeysApp *)l->data)->accel1) == 0 ||
+                g_strcmp0(data->accel1, ((LXKeysApp *)l->data)->accel2) == 0) {
+_accel1_bound:
+                g_set_error(error, LXKEYS_OB_ERROR, LXKEYS_FILE_ERROR,
+                            _("Hotkey '%s' is already bound to an action."),
+                            data->accel1);
+                return FALSE;
+            }
+        }
+        if (data->accel2) {
+            if (g_strcmp0(data->accel2, ((LXKeysApp *)l->data)->accel1) == 0 ||
+                g_strcmp0(data->accel2, ((LXKeysApp *)l->data)->accel2) == 0) {
+_accel2_bound:
+                g_set_error(error, LXKEYS_OB_ERROR, LXKEYS_FILE_ERROR,
+                            _("Hotkey '%s' is already bound to an action."),
+                            data->accel2);
+                return FALSE;
+            }
+        }
+    }
+    /* find if that action(s) is present */
+    for (l = cfg->actions; l; l = l->next)
+        if (options_equal((act = l->data)->actions, data->actions))
+            break;
+    /* if found then either change keys or remove the keybinding */
+    if (l != NULL) {
+        if (data->accel1 == NULL) {
+            /* removal requested */
+            if (act->data1)
+                fm_xml_file_item_destroy(act->data1);
+            if (act->data2)
+                fm_xml_file_item_destroy(act->data2);
+            lkxeys_action_free(act);
+            cfg->actions = g_list_delete_link(cfg->actions, l);
+        } else {
+            if (data->accel2 == NULL) {
+                /* new data contains only one binding */
+                if (g_strcmp0(act->accel1, data->accel1) == 0) {
+                    /* accel1 not changed, just clear accel2 */
+                    if (act->data2)
+                        fm_xml_file_item_destroy(act->data2);
+                    g_free(act->accel2);
+                    act->accel2 = NULL;
+                } else if (g_strcmp0(act->accel2, data->accel1) == 0) {
+                    /* accel1 was removed */
+                    if (act->data1)
+                        fm_xml_file_item_destroy(act->data1);
+                    g_free(act->accel1);
+                    act->accel1 = act->accel2;
+                    act->accel2 = NULL;
+                } else {
+                    /* full change */
+                    replace_key(act->data1, data->accel1, &act->accel1);
+                    if (act->data2)
+                        fm_xml_file_item_destroy(act->data2);
+                    g_free(act->accel2);
+                    act->accel2 = NULL;
+                }
+            } else if (act->accel2 == NULL) {
+                /* new data has two bindings and old data has 1 */
+                if (g_strcmp0(act->accel1, data->accel1) == 0) {
+                    /* add data->accel2 */
+                    act->data2 = make_new_xml_binding(cfg, data->actions, data->accel2, NULL, NULL);
+                    act->accel2 = g_strdup(data->accel2);
+                } else if (g_strcmp0(act->accel1, data->accel2) == 0) {
+                    /* add data->accel1 */
+                    act->data2 = make_new_xml_binding(cfg, data->actions, data->accel1, NULL, NULL);
+                    act->accel2 = g_strdup(data->accel1);
+                } else {
+                    /* replace key act->accel1 and add data->accel2 */
+                    replace_key(act->data1, data->accel1, &act->accel1);
+                    act->data2 = make_new_xml_binding(cfg, data->actions, data->accel2, NULL, NULL);
+                    act->accel2 = g_strdup(data->accel2);
+                }
+            } else {
+                /* both keys are present in old and new data */
+                if (g_strcmp0(act->accel1, data->accel1) == 0) {
+                    if (g_strcmp0(act->accel2, data->accel2) != 0)
+                        /* just accel2 is changed */
+                        replace_key(act->data2, data->accel2, &act->accel2);
+                    /* else nothing changed */
+                } else if (g_strcmp0(act->accel1, data->accel2) == 0) {
+                    if (g_strcmp0(act->accel2, data->accel1) != 0)
+                        /* replace accel2 with data->accel1 */
+                        replace_key(act->data2, data->accel1, &act->accel2);
+                    /* else keys just swapped */
+                } else if (g_strcmp0(act->accel2, data->accel2) == 0) {
+                    /* just accel1 is changed */
+                    replace_key(act->data1, data->accel1, &act->accel1);
+                } else if (g_strcmp0(act->accel2, data->accel1) == 0) {
+                    /* replace accel1 with data->accel2 */
+                    replace_key(act->data1, data->accel2, &act->accel1);
+                } else {
+                    /* both keys changed */
+                    replace_key(act->data1, data->accel1, &act->accel1);
+                    replace_key(act->data2, data->accel2, &act->accel2);
+                }
+            }
+        }
+    /* if not found then add a new keybinding */
+    } else if (data->accel1) {
+        act = g_new0(LXKeysGlobal, 1);
+        act->data1 = make_new_xml_binding(cfg, data->actions, data->accel1, &act->actions, NULL);
+        act->accel1 = g_strdup(data->accel1);
+        /* do the same for accel2 if requested */
+        if (data->accel2) {
+            act->data2 = make_new_xml_binding(cfg, data->actions, data->accel2, NULL, NULL);
+            act->accel2 = g_strdup(data->accel2);
+        }
+        cfg->actions = g_list_prepend(cfg->actions, act);
+    }
+    return restart_openbox(error);
+}
+
+static GList *obcfg_get_app_keys(gpointer config, const char *mask, GError **error)
+{
+    ObXmlFile *cfg = (ObXmlFile *)config;
+    GList *list = NULL, *l;
+    LXKeysApp *data;
+
+    if (cfg == NULL)
+        g_set_error_literal(error, LXKEYS_OB_ERROR, LXKEYS_FILE_ERROR,
+                            _("No WM configuration is available."));
+    else for (l = cfg->execs; l; l = l->next) {
+        data = l->data;
+        if (mask == NULL || fnmatch(mask, data->accel1, 0) == 0
+            || (data->accel2 != NULL && fnmatch(mask, data->accel2, 0) == 0))
+            list = g_list_prepend(list, data);
+    }
+    return list;
+}
+
+static gboolean obcfg_set_app_key(gpointer config, LXKeysApp *data, GError **error)
+{
+    ObXmlFile *cfg = (ObXmlFile *)config;
+    GList *l;
+    LXKeysApp *app;
+
+    if (cfg == NULL) {
+        g_set_error_literal(error, LXKEYS_OB_ERROR, LXKEYS_FILE_ERROR,
+                            _("No WM configuration is available."));
+        return FALSE;
+    } else if (data->exec == NULL || data->exec[0] == '\0') {
+        g_set_error_literal(error, LXKEYS_OB_ERROR, LXKEYS_PARSE_ERROR,
+                            _("The exec line cannot be empty."));
+        return FALSE;
+    }
+    /* find if those keys are already bound elsewhere */
+    for (l = cfg->actions; l; l = l->next) {
+        if (data->accel1) {
+            if (strcmp(data->accel1, ((LXKeysGlobal *)l->data)->accel1) == 0 ||
+                g_strcmp0(data->accel1, ((LXKeysGlobal *)l->data)->accel2) == 0)
+                goto _accel1_bound;
+        }
+        if (data->accel2) {
+            if (g_strcmp0(data->accel2, ((LXKeysGlobal *)l->data)->accel1) == 0 ||
+                g_strcmp0(data->accel2, ((LXKeysGlobal *)l->data)->accel2) == 0)
+                goto _accel2_bound;
+        }
+    }
+    for (l = cfg->execs; l; l = l->next) {
+        if (data->accel1) {
+            if (strcmp(data->accel1, ((LXKeysApp *)l->data)->accel1) == 0 ||
+                g_strcmp0(data->accel1, ((LXKeysApp *)l->data)->accel2) == 0) {
+_accel1_bound:
+                g_set_error(error, LXKEYS_OB_ERROR, LXKEYS_FILE_ERROR,
+                            _("Hotkey '%s' is already bound to an action."),
+                            data->accel1);
+                return FALSE;
+            }
+        }
+        if (data->accel2) {
+            if (g_strcmp0(data->accel2, ((LXKeysApp *)l->data)->accel1) == 0 ||
+                g_strcmp0(data->accel2, ((LXKeysApp *)l->data)->accel2) == 0) {
+_accel2_bound:
+                g_set_error(error, LXKEYS_OB_ERROR, LXKEYS_FILE_ERROR,
+                            _("Hotkey '%s' is already bound to an action."),
+                            data->accel2);
+                return FALSE;
+            }
+        }
+    }
+    /* find if that action(s) is present */
+    for (l = cfg->execs; l; l = l->next)
+        if (g_strcmp0((app = l->data)->exec, data->exec) == 0
+            && options_equal(app->options, data->options))
+            break;
+    /* if found then either change keys or remove the keybinding */
+    if (l != NULL) {
+        if (data->accel1 == NULL) {
+            /* removal requested */
+            if (app->data1)
+                fm_xml_file_item_destroy(app->data1);
+            if (app->data2)
+                fm_xml_file_item_destroy(app->data2);
+            lkxeys_app_free(app);
+            cfg->execs = g_list_delete_link(cfg->execs, l);
+        } else {
+            if (data->accel2 == NULL) {
+                /* new data contains only one binding */
+                if (g_strcmp0(app->accel1, data->accel1) == 0) {
+                    /* accel1 not changed, just clear accel2 */
+                    if (app->data2)
+                        fm_xml_file_item_destroy(app->data2);
+                    g_free(app->accel2);
+                    app->accel2 = NULL;
+                } else if (g_strcmp0(app->accel2, data->accel1) == 0) {
+                    /* accel1 was removed */
+                    if (app->data1)
+                        fm_xml_file_item_destroy(app->data1);
+                    g_free(app->accel1);
+                    app->accel1 = app->accel2;
+                    app->accel2 = NULL;
+                } else {
+                    /* full change */
+                    replace_key(app->data1, data->accel1, &app->accel1);
+                    if (app->data2)
+                        fm_xml_file_item_destroy(app->data2);
+                    g_free(app->accel2);
+                    app->accel2 = NULL;
+                }
+            } else if (app->accel2 == NULL) {
+                /* new data has two bindings and old data has 1 */
+                if (g_strcmp0(app->accel1, data->accel1) == 0) {
+                    /* add data->accel2 */
+                    app->data2 = make_new_xml_binding(cfg, data->options, data->accel2, NULL, data->exec);
+                    app->accel2 = g_strdup(data->accel2);
+                } else if (g_strcmp0(app->accel1, data->accel2) == 0) {
+                    /* add data->accel1 */
+                    app->data2 = make_new_xml_binding(cfg, data->options, data->accel1, NULL, data->exec);
+                    app->accel2 = g_strdup(data->accel1);
+                } else {
+                    /* replace key app->accel1 and add data->accel2 */
+                    replace_key(app->data1, data->accel1, &app->accel1);
+                    app->data2 = make_new_xml_binding(cfg, data->options, data->accel2, NULL, data->exec);
+                    app->accel2 = g_strdup(data->accel2);
+                }
+            } else {
+                /* both keys are present in old and new data */
+                if (g_strcmp0(app->accel1, data->accel1) == 0) {
+                    if (g_strcmp0(app->accel2, data->accel2) != 0)
+                        /* just accel2 is changed */
+                        replace_key(app->data2, data->accel2, &app->accel2);
+                    /* else nothing changed */
+                } else if (g_strcmp0(app->accel1, data->accel2) == 0) {
+                    if (g_strcmp0(app->accel2, data->accel1) != 0)
+                        /* replace accel2 with data->accel1 */
+                        replace_key(app->data2, data->accel1, &app->accel2);
+                    /* else keys just swapped */
+                } else if (g_strcmp0(app->accel2, data->accel2) == 0) {
+                    /* just accel1 is changed */
+                    replace_key(app->data1, data->accel1, &app->accel1);
+                } else if (g_strcmp0(app->accel2, data->accel1) == 0) {
+                    /* replace accel1 with data->accel2 */
+                    replace_key(app->data1, data->accel2, &app->accel1);
+                } else {
+                    /* both keys changed */
+                    replace_key(app->data1, data->accel1, &app->accel1);
+                    replace_key(app->data2, data->accel2, &app->accel2);
+                }
+            }
+        }
+    /* if not found then add a new keybinding */
+    } else if (data->accel1) {
+        app = g_new0(LXKeysApp, 1);
+        app->exec = g_strdup(data->exec);
+        app->data1 = make_new_xml_binding(cfg, data->options, data->accel1, &app->options, data->exec);
+        app->accel1 = g_strdup(data->accel1);
+        /* do the same for accel2 if requested */
+        if (data->accel2) {
+            app->data2 = make_new_xml_binding(cfg, data->options, data->accel2, NULL, data->exec);
+            app->accel2 = g_strdup(data->accel2);
+        }
+        cfg->execs = g_list_prepend(cfg->execs, app);
+    }
+    return restart_openbox(error);
 }
 
+static GList *obcfg_get_wm_actions(gpointer config, GError **error)
+{
+    if (!available_wm_actions)
+        available_wm_actions = convert_options(list_actions);
+    return available_wm_actions;
+}
+
+
+static GList *obcfg_get_app_options(gpointer config, GError **error)
+{
+    if (!available_wm_actions)
+        available_wm_actions = convert_options(list_actions);
+    return available_app_options;
+}
+
+
 FM_DEFINE_MODULE(lxkeys, Openbox)
 
 LXKeysPluginInit fm_module_init_lxkeys = {
@@ -148,8 +1308,9 @@ LXKeysPluginInit fm_module_init_lxkeys = {
     .save = obcfg_save,
     .free = obcfg_free,
     .get_wm_keys = obcfg_get_wm_keys,
-    .set_wm_key = obcfg_set_wm_key
-    // .get_wm_actions = obcfg_get_wm_actions,
-    // .get_app_keys = obcfg_get_app_keys,
-    // .set_app_key = obcfg_set_app_key
+    .set_wm_key = obcfg_set_wm_key,
+    .get_wm_actions = obcfg_get_wm_actions,
+    .get_app_keys = obcfg_get_app_keys,
+    .set_app_key = obcfg_set_app_key,
+    .get_app_options = obcfg_get_app_options
 };
index 96bcec0..964e903 100644 (file)
@@ -1 +1,2 @@
 src/lxkeys.c
+plugins/openbox.c
index 0afd9ce..e8ef407 100644 (file)
@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-03-06 22:10+0200\n"
+"POT-Creation-Date: 2016-03-10 20:52+0200\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -17,109 +17,625 @@ msgstr ""
 "Content-Type: text/plain; charset=CHARSET\n"
 "Content-Transfer-Encoding: 8bit\n"
 
-#: ../src/lxkeys.c:198
+#: ../src/lxkeys.c:228
 #, c-format
 msgid "Usage: %s global [<action>]      - show keys bound to action(s)\n"
 msgstr ""
 
-#: ../src/lxkeys.c:199
+#: ../src/lxkeys.c:229
 #, c-format
 msgid "       %s global <action> <key>  - bind a key to the action\n"
 msgstr ""
 
-#: ../src/lxkeys.c:200
+#: ../src/lxkeys.c:230
 #, c-format
 msgid "       %s app [<exec>]           - show keys bound to exec line\n"
 msgstr ""
 
-#: ../src/lxkeys.c:201
+#: ../src/lxkeys.c:231
 #, c-format
 msgid "       %s app <exec> <key>       - bind a key to some exec line\n"
 msgstr ""
 
-#: ../src/lxkeys.c:202
+#: ../src/lxkeys.c:232
 #, c-format
 msgid "       %s app <exec> --          - unbind all keys from exec line\n"
 msgstr ""
 
-#: ../src/lxkeys.c:203
+#: ../src/lxkeys.c:233
 #, c-format
 msgid "       %s show <key>             - show the action bound to a key\n"
 msgstr ""
 
-#: ../src/lxkeys.c:314
+#: ../src/lxkeys.c:234
+#, c-format
+msgid "       %s --gui=<type>           - start with GUI\n"
+msgstr ""
+
+#: ../src/lxkeys.c:329
 msgid "empty option name."
 msgstr ""
 
-#: ../src/lxkeys.c:317
+#: ../src/lxkeys.c:332
 msgid "empty action name."
 msgstr ""
 
-#: ../src/lxkeys.c:347
+#: ../src/lxkeys.c:364
 #, c-format
 msgid "no matching option '%s' found for action '%s'."
 msgstr ""
 
-#: ../src/lxkeys.c:351
+#: ../src/lxkeys.c:368
 #, c-format
-msgid "no mathing action '%s' found for the WM configuration."
+msgid "action '%s' isn't supported by WM %s."
 msgstr ""
 
-#: ../src/lxkeys.c:364
+#: ../src/lxkeys.c:385
 #, c-format
 msgid "value '%s' is not supported for option '%s'."
 msgstr ""
 
-#: ../src/lxkeys.c:368
+#: ../src/lxkeys.c:389
 #, c-format
 msgid "value '%s' is not supported for action '%s'."
 msgstr ""
 
-#: ../src/lxkeys.c:381
+#: ../src/lxkeys.c:402
 #, c-format
 msgid "action '%s' does not support options."
 msgstr ""
 
-#: ../src/lxkeys.c:445
+#: ../src/lxkeys.c:466
 #, c-format
 msgid "LXKeys: sorry, cannot configure keys remotely.\n"
 msgstr ""
 
-#: ../src/lxkeys.c:472
+#: ../src/lxkeys.c:493
 #, c-format
 msgid "Window manager %s isn't supported now, sorry."
 msgstr ""
 
-#: ../src/lxkeys.c:479
+#: ../src/lxkeys.c:500
 msgid "Problems loading configuration: "
 msgstr ""
 
-#: ../src/lxkeys.c:488
+#: ../src/lxkeys.c:509
 #, c-format
 msgid "GUI type %s currently isn't supported."
 msgstr ""
 
 #. invalid request
-#: ../src/lxkeys.c:504 ../src/lxkeys.c:578
+#: ../src/lxkeys.c:526 ../src/lxkeys.c:602
 msgid "Invalid request: "
 msgstr ""
 
-#: ../src/lxkeys.c:514 ../src/lxkeys.c:589
+#: ../src/lxkeys.c:536 ../src/lxkeys.c:614
 msgid "Problems saving configuration: "
 msgstr ""
 
-#: ../src/lxkeys.c:531
+#: ../src/lxkeys.c:553
 msgid "ACTION(s)"
 msgstr ""
 
-#: ../src/lxkeys.c:531 ../src/lxkeys.c:606
+#: ../src/lxkeys.c:553 ../src/lxkeys.c:631
 msgid "KEY(s)"
 msgstr ""
 
-#: ../src/lxkeys.c:606
+#: ../src/lxkeys.c:631
 msgid "EXEC"
 msgstr ""
 
-#: ../src/lxkeys.c:626
+#: ../src/lxkeys.c:652
 msgid "Requested operation isn't supported."
 msgstr ""
+
+#: ../plugins/openbox.c:139
+msgid "Failed to reconfigure Openbox."
+msgstr ""
+
+#. reuse GList
+#. reuse GList
+#: ../plugins/openbox.c:161
+msgid "yes"
+msgstr ""
+
+#: ../plugins/openbox.c:161
+msgid "no"
+msgstr ""
+
+#: ../plugins/openbox.c:164
+msgid "enabled"
+msgstr ""
+
+#: ../plugins/openbox.c:165
+msgid "wmclass"
+msgstr ""
+
+#: ../plugins/openbox.c:166
+msgid "name"
+msgstr ""
+
+#: ../plugins/openbox.c:167
+msgid "icon"
+msgstr ""
+
+#: ../plugins/openbox.c:172 ../plugins/openbox.c:244
+msgid "command"
+msgstr ""
+
+#: ../plugins/openbox.c:173 ../plugins/openbox.c:249
+msgid "prompt"
+msgstr ""
+
+#: ../plugins/openbox.c:174
+msgid "startupnotify"
+msgstr ""
+
+#: ../plugins/openbox.c:178 ../plugins/openbox.c:281
+msgid "center"
+msgstr ""
+
+#: ../plugins/openbox.c:179
+msgid "default"
+msgstr ""
+
+#: ../plugins/openbox.c:179
+msgid "primary"
+msgstr ""
+
+#: ../plugins/openbox.c:179
+msgid "mouse"
+msgstr ""
+
+#: ../plugins/openbox.c:180
+msgid "active"
+msgstr ""
+
+#: ../plugins/openbox.c:180 ../plugins/openbox.c:283
+msgid "all"
+msgstr ""
+
+#: ../plugins/openbox.c:185 ../plugins/openbox.c:290
+msgid "monitor"
+msgstr ""
+
+#: ../plugins/openbox.c:190
+msgid "menu"
+msgstr ""
+
+#: ../plugins/openbox.c:191
+msgid "position"
+msgstr ""
+
+#: ../plugins/openbox.c:195
+msgid "list"
+msgstr ""
+
+#: ../plugins/openbox.c:195
+msgid "icons"
+msgstr ""
+
+#: ../plugins/openbox.c:195
+msgid "none"
+msgstr ""
+
+#: ../plugins/openbox.c:198 ../plugins/openbox.c:216
+msgid "dialog"
+msgstr ""
+
+#: ../plugins/openbox.c:199 ../plugins/openbox.c:217
+msgid "bar"
+msgstr ""
+
+#: ../plugins/openbox.c:200 ../plugins/openbox.c:218
+msgid "raise"
+msgstr ""
+
+#: ../plugins/openbox.c:201
+msgid "allDesktops"
+msgstr ""
+
+#: ../plugins/openbox.c:202 ../plugins/openbox.c:219
+msgid "panels"
+msgstr ""
+
+#: ../plugins/openbox.c:203
+msgid "desktop"
+msgstr ""
+
+#: ../plugins/openbox.c:204
+msgid "linear"
+msgstr ""
+
+#: ../plugins/openbox.c:205
+msgid "interactive"
+msgstr ""
+
+#: ../plugins/openbox.c:206 ../plugins/openbox.c:221
+msgid "finalactions"
+msgstr ""
+
+#: ../plugins/openbox.c:210 ../plugins/openbox.c:226 ../plugins/openbox.c:261
+#: ../plugins/openbox.c:312
+msgid "north"
+msgstr ""
+
+#: ../plugins/openbox.c:210
+msgid "northeast"
+msgstr ""
+
+#: ../plugins/openbox.c:210 ../plugins/openbox.c:227 ../plugins/openbox.c:262
+#: ../plugins/openbox.c:312
+msgid "east"
+msgstr ""
+
+#: ../plugins/openbox.c:211
+msgid "southeast"
+msgstr ""
+
+#: ../plugins/openbox.c:211 ../plugins/openbox.c:226 ../plugins/openbox.c:261
+#: ../plugins/openbox.c:312
+msgid "south"
+msgstr ""
+
+#: ../plugins/openbox.c:211
+msgid "southwest"
+msgstr ""
+
+#: ../plugins/openbox.c:212 ../plugins/openbox.c:227 ../plugins/openbox.c:262
+#: ../plugins/openbox.c:312
+msgid "west"
+msgstr ""
+
+#: ../plugins/openbox.c:212
+msgid "northwest"
+msgstr ""
+
+#: ../plugins/openbox.c:215 ../plugins/openbox.c:256 ../plugins/openbox.c:315
+msgid "direction"
+msgstr ""
+
+#: ../plugins/openbox.c:220
+msgid "desktops"
+msgstr ""
+
+#: ../plugins/openbox.c:225 ../plugins/openbox.c:236 ../plugins/openbox.c:260
+#: ../plugins/openbox.c:281 ../plugins/openbox.c:282 ../plugins/openbox.c:283
+msgid "current"
+msgstr ""
+
+#: ../plugins/openbox.c:225 ../plugins/openbox.c:260 ../plugins/openbox.c:283
+msgid "next"
+msgstr ""
+
+#: ../plugins/openbox.c:225 ../plugins/openbox.c:260
+msgid "previous"
+msgstr ""
+
+#: ../plugins/openbox.c:226 ../plugins/openbox.c:236 ../plugins/openbox.c:261
+msgid "last"
+msgstr ""
+
+#: ../plugins/openbox.c:226 ../plugins/openbox.c:261
+msgid "up"
+msgstr ""
+
+#: ../plugins/openbox.c:227 ../plugins/openbox.c:262
+msgid "down"
+msgstr ""
+
+#: ../plugins/openbox.c:227 ../plugins/openbox.c:262 ../plugins/openbox.c:272
+#: ../plugins/openbox.c:305
+msgid "left"
+msgstr ""
+
+#: ../plugins/openbox.c:228 ../plugins/openbox.c:263 ../plugins/openbox.c:272
+#: ../plugins/openbox.c:306
+msgid "right"
+msgstr ""
+
+#: ../plugins/openbox.c:231 ../plugins/openbox.c:266
+msgid "to"
+msgstr ""
+
+#: ../plugins/openbox.c:232 ../plugins/openbox.c:267
+msgid "wrap"
+msgstr ""
+
+#: ../plugins/openbox.c:239
+msgid "where"
+msgstr ""
+
+#: ../plugins/openbox.c:253
+msgid "both"
+msgstr ""
+
+#: ../plugins/openbox.c:253
+msgid "horizontal"
+msgstr ""
+
+#: ../plugins/openbox.c:253
+msgid "vertical"
+msgstr ""
+
+#: ../plugins/openbox.c:268
+msgid "follow"
+msgstr ""
+
+#: ../plugins/openbox.c:272 ../plugins/openbox.c:307 ../plugins/openbox.c:319
+msgid "top"
+msgstr ""
+
+#: ../plugins/openbox.c:272 ../plugins/openbox.c:308 ../plugins/openbox.c:319
+msgid "bottom"
+msgstr ""
+
+#: ../plugins/openbox.c:273
+msgid "topleft"
+msgstr ""
+
+#: ../plugins/openbox.c:273
+msgid "topright"
+msgstr ""
+
+#: ../plugins/openbox.c:273
+msgid "bottomleft"
+msgstr ""
+
+#: ../plugins/openbox.c:274
+msgid "bottomright"
+msgstr ""
+
+#: ../plugins/openbox.c:277
+msgid "edge"
+msgstr ""
+
+#: ../plugins/openbox.c:283
+msgid "prev"
+msgstr ""
+
+#: ../plugins/openbox.c:288
+msgid "width"
+msgstr ""
+
+#: ../plugins/openbox.c:289
+msgid "height"
+msgstr ""
+
+#: ../plugins/openbox.c:319
+msgid "normal"
+msgstr ""
+
+#: ../plugins/openbox.c:322
+msgid "layer"
+msgstr ""
+
+#. global actions
+#: ../plugins/openbox.c:328
+msgid "Execute"
+msgstr ""
+
+#: ../plugins/openbox.c:329
+msgid "ShowMenu"
+msgstr ""
+
+#: ../plugins/openbox.c:330
+msgid "NextWindow"
+msgstr ""
+
+#: ../plugins/openbox.c:331
+msgid "PreviousWindow"
+msgstr ""
+
+#: ../plugins/openbox.c:332
+msgid "DirectionalCycleWindows"
+msgstr ""
+
+#: ../plugins/openbox.c:333
+msgid "DirectionalTargetWindow"
+msgstr ""
+
+#: ../plugins/openbox.c:334
+msgid "GoToDesktop"
+msgstr ""
+
+#: ../plugins/openbox.c:335
+msgid "AddDesktop"
+msgstr ""
+
+#: ../plugins/openbox.c:336
+msgid "RemoveDesktop"
+msgstr ""
+
+#: ../plugins/openbox.c:337
+msgid "ToggleDockAutohide"
+msgstr ""
+
+#: ../plugins/openbox.c:338
+msgid "Reconfigure"
+msgstr ""
+
+#: ../plugins/openbox.c:339
+msgid "Restart"
+msgstr ""
+
+#: ../plugins/openbox.c:340
+msgid "Exit"
+msgstr ""
+
+#. windows actions
+#: ../plugins/openbox.c:342
+msgid "Focus"
+msgstr ""
+
+#: ../plugins/openbox.c:343
+msgid "Raise"
+msgstr ""
+
+#: ../plugins/openbox.c:344
+msgid "Lower"
+msgstr ""
+
+#: ../plugins/openbox.c:345
+msgid "RaiseLower"
+msgstr ""
+
+#: ../plugins/openbox.c:346
+msgid "Unfocus"
+msgstr ""
+
+#: ../plugins/openbox.c:347
+msgid "FocusToBottom"
+msgstr ""
+
+#: ../plugins/openbox.c:348
+msgid "Iconify"
+msgstr ""
+
+#: ../plugins/openbox.c:349
+msgid "Close"
+msgstr ""
+
+#: ../plugins/openbox.c:350
+msgid "ToggleShade"
+msgstr ""
+
+#: ../plugins/openbox.c:351
+msgid "Shade"
+msgstr ""
+
+#: ../plugins/openbox.c:352
+msgid "Unshade"
+msgstr ""
+
+#: ../plugins/openbox.c:353
+msgid "ToggleOmnipresent"
+msgstr ""
+
+#: ../plugins/openbox.c:354
+msgid "ToggleMaximize"
+msgstr ""
+
+#: ../plugins/openbox.c:355
+msgid "Maximize"
+msgstr ""
+
+#: ../plugins/openbox.c:356
+msgid "Unmaximize"
+msgstr ""
+
+#: ../plugins/openbox.c:357
+msgid "ToggleFullscreen"
+msgstr ""
+
+#: ../plugins/openbox.c:358
+msgid "ToggleDecorations"
+msgstr ""
+
+#: ../plugins/openbox.c:359
+msgid "Decorate"
+msgstr ""
+
+#: ../plugins/openbox.c:360
+msgid "Undecorate"
+msgstr ""
+
+#: ../plugins/openbox.c:361
+msgid "SendToDesktop"
+msgstr ""
+
+#: ../plugins/openbox.c:362
+msgid "Move"
+msgstr ""
+
+#: ../plugins/openbox.c:363
+msgid "Resize"
+msgstr ""
+
+#: ../plugins/openbox.c:364
+msgid "MoveResizeTo"
+msgstr ""
+
+#: ../plugins/openbox.c:365
+msgid "MoveRelative"
+msgstr ""
+
+#: ../plugins/openbox.c:366
+msgid "ResizeRelative"
+msgstr ""
+
+#: ../plugins/openbox.c:367
+msgid "MoveToEdge"
+msgstr ""
+
+#: ../plugins/openbox.c:368
+msgid "GrowToEdge"
+msgstr ""
+
+#: ../plugins/openbox.c:369
+msgid "ShrinkToEdge"
+msgstr ""
+
+#: ../plugins/openbox.c:370
+msgid "GrowToFill"
+msgstr ""
+
+#: ../plugins/openbox.c:371
+msgid "ToggleAlwaysOnTop"
+msgstr ""
+
+#: ../plugins/openbox.c:372
+msgid "ToggleAlwaysOnBottom"
+msgstr ""
+
+#: ../plugins/openbox.c:373
+msgid "SendToLayer"
+msgstr ""
+
+#: ../plugins/openbox.c:455
+msgid "Duplicate <keyboard> section in the rc.xml file."
+msgstr ""
+
+#: ../plugins/openbox.c:480 ../plugins/openbox.c:486 ../plugins/openbox.c:721
+msgid "Internal error."
+msgstr ""
+
+#: ../plugins/openbox.c:529
+msgid "rc.xml error: no key is set for a keybind."
+msgstr ""
+
+#: ../plugins/openbox.c:600
+msgid "Invalid rc.xml: action with a sub-action."
+msgstr ""
+
+#: ../plugins/openbox.c:710
+msgid "rc.xml error: no name is set for action."
+msgstr ""
+
+#: ../plugins/openbox.c:740
+#, c-format
+msgid "rc.xml error: empty tag <%s> is prohibited."
+msgstr ""
+
+#: ../plugins/openbox.c:829
+msgid "Could not find the rc.xml file anywhere."
+msgstr ""
+
+#: ../plugins/openbox.c:875 ../plugins/openbox.c:988 ../plugins/openbox.c:1132
+#: ../plugins/openbox.c:1150
+msgid "No WM configuration is available."
+msgstr ""
+
+#: ../plugins/openbox.c:992
+msgid "Keybinding should activate at least one action."
+msgstr ""
+
+#: ../plugins/openbox.c:1014 ../plugins/openbox.c:1024
+#: ../plugins/openbox.c:1176 ../plugins/openbox.c:1186
+#, c-format
+msgid "Hotkey '%s' is already bound to an action."
+msgstr ""
+
+#: ../plugins/openbox.c:1154
+msgid "The exec line cannot be empty."
+msgstr ""
index ac194ff..20381df 100644 (file)
@@ -11,3 +11,6 @@ lxkeys_CPPFLAGS = \
 lxkeys_SOURCES = lxkeys.c
 
 lxkeys_LDADD = $(PACKAGE_LIBS)
+
+lxkeys_includedir = $(includedir)/lxkeys
+lxkeys_include_HEADERS = lxkeys.h
index 0b6e4ef..17c3226 100644 (file)
 #include <X11/Xlib.h>
 #include <X11/Xatom.h>
 
-#define NONULL(a) (a) ? ((char *)a) : ""
+#include <stdlib.h>
+
+#ifdef HAVE_LIBUNISTRING
+# include <unistdio.h>
+# define ulc_printf(...) ulc_fprintf(stdout,__VA_ARGS__)
+#else
+# define ulc_printf printf
+#endif
+
+/* for errors */
+static GQuark LXKEYS_ERROR;
+typedef enum {
+    LXKEYS_BAD_ARGS, /* invalid commandline arguments */
+    LXKEYS_NOT_SUPPORTED /* operation not supported */
+} LXKeysError;
+
+/* simple functions to manage LXKeysAttr data type */
+static inline LXKeysAttr *lxkeys_attr_new(void)
+{
+    return g_slice_new0(LXKeysAttr);
+}
+
+#define free_actions(acts) g_list_free_full(acts, (GDestroyNotify)lkxeys_attr_free)
 
-GQuark LXKEYS_ERROR;
+static void lkxeys_attr_free(LXKeysAttr *data)
+{
+    g_free(data->name);
+    g_list_free_full(data->values, g_free);
+    free_actions(data->subopts);
+    g_slice_free(LXKeysAttr, data);
+}
+
+#define NONULL(a) (a) ? ((char *)a) : ""
 
 /* this function is taken from wmctrl utility */
 #define MAX_PROPERTY_VALUE_LEN 4096
@@ -201,25 +231,10 @@ static int _print_help(const char *cmd)
     printf(_("       %s app <exec> <key>       - bind a key to some exec line\n"), cmd);
     printf(_("       %s app <exec> --          - unbind all keys from exec line\n"), cmd);
     printf(_("       %s show <key>             - show the action bound to a key\n"), cmd);
+    printf(_("       %s --gui=<type>           - start with GUI\n"), cmd);
     return 0;
 }
 
-/* free a LXKeysAttr list */
-static void free_actions(gpointer act)
-{
-    GList *l;
-    LXKeysAttr *data;
-
-    while ((l = act)) {
-        act = l->next;
-        data = l->data;
-        g_list_free_1(l);
-        g_free(data->name);
-        g_list_free_full(data->values, g_free);
-        g_list_free_full(data->subopts, free_actions);
-    }
-}
-
 /* convert text line to LXKeysAttr list
    text may be formatted like this:
    startupnotify=yes:attr1=val1:attr2=val2&action=val
@@ -324,10 +339,12 @@ _failed:
 /* check if action list matches origin
    if origin==NULL then error is possibly set already */
 static gboolean validate_actions(const GList *act, const GList *origin,
-                                 const LXKeysAttr *action, GError **error)
+                                 const LXKeysAttr *action, gchar *wm_name,
+                                 GError **error)
 {
     const LXKeysAttr *data, *ordata;
     const GList *l, *olist;
+    char *endptr;
 
     if (!origin)
         return FALSE;
@@ -348,15 +365,19 @@ static gboolean validate_actions(const GList *act, const GList *origin,
                             data->name, action->name);
             else
                 g_set_error(error, LXKEYS_ERROR, LXKEYS_BAD_ARGS,
-                            _("no mathing action '%s' found for the WM configuration."),
-                            data->name);
+                            _("action '%s' isn't supported by WM %s."),
+                            data->name, wm_name);
             return FALSE;
         }
         /* if ordata->values isn't NULL and data->values isn't NULL
            then it must match, ordata->values==NULL means anything matches */
         if (data->values != NULL && ordata->values != NULL) {
             for (l = ordata->values; l; l = l->next)
-                if (g_strcmp0(data->values->data, l->data) == 0)
+                if (g_strcmp0(data->values->data, l->data) == 0 ||
+                    /* check for numeric value too */
+                    (strtol(data->values->data, &endptr, 10) < LONG_MAX &&
+                     ((g_strcmp0(l->data, "#") == 0 && endptr[0] == '\0') ||
+                      (g_strcmp0(l->data, "%") == 0 && endptr[0] == '%'))))
                     break;
             if (l == NULL) {
                 if (action)
@@ -374,13 +395,13 @@ static gboolean validate_actions(const GList *act, const GList *origin,
         if (!data->subopts) ;
         else if (ordata->has_actions) {
             /* test against origin actions list, not suboptions */
-            if (!validate_actions(data->subopts, origin, NULL, error))
+            if (!validate_actions(data->subopts, origin, NULL, wm_name, error))
                 return FALSE;
         } else if (!ordata->subopts) {
             g_set_error(error, LXKEYS_ERROR, LXKEYS_BAD_ARGS,
                         _("action '%s' does not support options."), data->name);
             return FALSE;
-        } else if (!validate_actions(data->subopts, origin, ordata, error))
+        } else if (!validate_actions(data->subopts, origin, ordata, wm_name, error))
             return FALSE;
         act = act->next;
     }
@@ -398,10 +419,10 @@ static void print_suboptions(GList *sub, int indent)
     while (sub) {
         LXKeysAttr *action = sub->data;
         if (action->values && action->values->data)
-            printf("%*s%s=%s\n", indent, "", action->name,
-                                  (char *)action->values->data);
+            printf("%*s%s=%s\n", indent, "", _(action->name),
+                                  _(action->values->data));
         else
-            printf("%*s%s\n", indent, "", action->name);
+            printf("%*s%s\n", indent, "", _(action->name));
         print_suboptions(action->subopts, indent);
         sub = sub->next;
     }
@@ -453,7 +474,7 @@ int main(int argc, char *argv[])
     if (do_gui)
         fm_module_register_lxkeys_gui();
 
-    LXKEYS_ERROR = g_quark_from_static_string("LXKEYS_ERROR");
+    LXKEYS_ERROR = g_quark_from_static_string("lxkeys-error");
 
     /* detect current WM and find a module for it */
     wm_name = get_wm_info();
@@ -494,13 +515,14 @@ int main(int argc, char *argv[])
         if (argc > 3) { /* set */
             LXKeysGlobal data;
 
-            if (plugin->t->get_wm_actions == NULL || plugin->t->set_wm_key == NULL)
+            if (plugin->t->set_wm_key == NULL)
                 goto _not_supported;
             /* parse and validate actions */
             data.actions = actions_from_str(argv[2], &error);
             if (error ||
-                !validate_actions(data.actions, plugin->t->get_wm_actions(config, &error),
-                                  NULL, &error)) { /* invalid request */
+                (plugin->t->get_wm_actions != NULL &&
+                 !validate_actions(data.actions, plugin->t->get_wm_actions(config, &error),
+                                   NULL, wm_name, &error))) { /* invalid request */
                 g_prefix_error(&error, _("Invalid request: "));
                 goto _exit;
             }
@@ -528,22 +550,23 @@ int main(int argc, char *argv[])
             if (argc > 2)
                 mask = argv[2]; /* mask given */
             keys = plugin->t->get_wm_keys(config, mask, NULL);
-            printf("%24s %s\n", _("ACTION(s)"), _("KEY(s)"));
+            ulc_printf(" %-24s %s\n", _("ACTION(s)"), _("KEY(s)"));
             for (key = keys; key; key = key->next) {
                 data = key->data;
                 for (act = data->actions; act; act = act->next)
                 {
                     action = act->data;
                     if (act != data->actions)
-                        printf("%s\n", action->name);
+                        printf("+ %s\n", _(action->name));
                     else if (data->accel2)
-                        printf("%24s %s %s\n", action->name, data->accel1,
-                                               data->accel2);
+                        ulc_printf("%-24s %s %s\n", _(action->name), data->accel1,
+                                                    data->accel2);
                     else
-                        printf("%24s %s\n", action->name, data->accel1);
+                        ulc_printf("%-24s %s\n", _(action->name), data->accel1);
                     print_suboptions(action->subopts, 0);
                 }
             }
+            g_list_free(keys);
         }
     } else if (strcmp(argv[1], "app") == 0) { /* lxkeys app ... */
         if (argc > 3) { /* set */
@@ -567,31 +590,33 @@ int main(int argc, char *argv[])
             } else {
                 data.accel1 = argv[3];
             }
+            g_list_free(keys);
             cmd = strchr(argv[2], '&');
             if (cmd) {
-                data.actions = actions_from_str(&cmd[1], &error);
+                data.options = actions_from_str(&cmd[1], &error);
                 if (error ||
-                    (plugin->t->get_app_actions != NULL &&
-                     !validate_actions(data.actions,
-                                       plugin->t->get_app_actions(config, &error),
-                                       NULL, &error))) { /* invalid request */
+                    (plugin->t->get_app_options != NULL &&
+                     !validate_actions(data.options,
+                                       plugin->t->get_app_options(config, &error),
+                                       NULL, wm_name, &error))) { /* invalid request */
                     g_prefix_error(&error, _("Invalid request: "));
+                    free_actions(data.options);
                     goto _exit;
                 }
                 data.exec = g_strndup(argv[2], cmd - argv[2]);
             } else {
-                data.actions = NULL;
+                data.options = NULL;
                 data.exec = g_strdup(argv[2]);
             }
             // FIXME: validate exec
             if (!plugin->t->set_app_key(config, &data, &error) ||
                 !plugin->t->save(config, &error)) {
                 g_prefix_error(&error, _("Problems saving configuration: "));
-                free_actions(data.actions);
+                free_actions(data.options);
                 g_free(data.exec);
                 goto _exit;
             }
-            free_actions(data.actions);
+            free_actions(data.options);
             g_free(data.exec);
         } else { /* show by mask */
             const char *mask = NULL;
@@ -603,16 +628,17 @@ int main(int argc, char *argv[])
             if (argc > 2)
                 mask = argv[2]; /* mask given */
             keys = plugin->t->get_app_keys(config, mask, NULL);
-            printf("%24s %s\n", _("EXEC"), _("KEY(s)"));
+            ulc_printf(" %-48s %s\n", _("EXEC"), _("KEY(s)"));
             for (key = keys; key; key = key->next) {
                 data = key->data;
                 if (data->accel2)
-                    printf("%24s %s %s\n", data->exec, data->accel1,
-                                           data->accel2);
+                    ulc_printf("%-48s %s %s\n", data->exec, data->accel1,
+                                                data->accel2);
                 else
-                    printf("%24s %s\n", data->exec, data->accel1);
-                print_suboptions(data->actions, 0);
+                    ulc_printf("%-48s %s\n", data->exec, data->accel1);
+                print_suboptions(data->options, 0);
             }
+            g_list_free(keys);
         }
     } else if (strcmp(argv[1], "show") == 0) { /* lxkeys show ... */
         // TODO!
@@ -630,7 +656,7 @@ _exit:
     if (config)
         plugin->t->free(config);
     if (error) {
-        // FIXME: if do_gui then show an alert window instead of stderr
+        /* if do_gui then show an alert window instead of stderr */
         if (gui_plugin && gui_plugin->t->alert)
             gui_plugin->t->alert(error);
         else
@@ -638,7 +664,8 @@ _exit:
         g_error_free(error);
     }
     fm_module_unregister_type("lxkeys");
-    fm_module_unregister_type("lxkeys_gui");
+    if (do_gui)
+        fm_module_unregister_type("lxkeys_gui");
     while (plugins) {
         plugin = plugins;
         plugins = plugin->next;
index e2f7a42..68fcfb2 100644 (file)
 
 G_BEGIN_DECLS
 
-/* data for actions and options */
+/**
+ * LXKeysAttr:
+ * @name: action or option name
+ * @values: (element-type char *): option value
+ * @subopts: (element-type LXKeysAttr): (allow-none): list of suboptions
+ * @has_actions: %TRUE if @subopts contains actions, %FALSE if @subopts contains options
+ *
+ * Data descriptor for actions and options. Actions are ativated by keybinding.
+ * Each action may contain arbitrary number of options that alter its execution.
+ *
+ * This data is also used in a result on LXKeysPluginInit:get_wm_actions() or
+ * LXKeysPluginInit:get_app_options() call. In that case for each option the
+ * @values list should be either %NULL if option accepts any value, or list of
+ * acceptable values for the option ("#" value has a special meaning: integer
+ * value matches it, the same as "%" for percent value). For such purpose it is
+ * advisable to make @name and @values translateable constants because GUI might
+ * represent them in target locale which might be convenient for users.
+ */
 typedef struct {
-    gchar *name; /* action or option name */
-    GList *values; /* elements are char*, multiple only for get_wm_actions() */
-    GList *subopts; /* elements are LXKeysAttr */
-    gboolean has_actions; /* TRUE if option contains actions instead of subopts */
+    gchar *name;
+    GList *values;
+    GList *subopts;
+    gboolean has_actions;
 } LXKeysAttr;
 
-/* data containing action(s), description, and key(s) */
+/**
+ * LXKeysGlobal:
+ * @actions: (element-type LXKeysAttr): list of actions
+ * @accel1: a keybinding to activate @actions, in GDK accelerator format
+ * @accel2: optional alternate keybinding to activate @actions
+ * @data1: a pointer for using by WM plugin
+ * @data2: a pointer for using by WM plugin
+ *
+ * Descriptor of a keybinding which isn't a single command line action.
+ * The keybinding string is in format that looks like "<Control>a" or
+ * "<Shift><Alt>F1" or "<Release>z" (note that not each WM supports last one
+ * variant).
+ */
 typedef struct {
-    GList *actions; /* elements are LXKeysAttr, no value usually */
+    GList *actions;
     gchar *accel1;
     gchar *accel2;
+    gpointer data1;
+    gpointer data2;
 } LXKeysGlobal;
 
-/* data containing exec line, description, and key(s)
-   only for single-exec commands, multiple-action exec are handled as global */
+/**
+ * LXKeysApp:
+ * @exec: a command line to execute
+ * @actions: (element-type LXKeysAttr): (allow-none): list of options
+ * @accel1: a keybinding to activate @exec, in GDK accelerator format
+ * @accel2: optional alternate keybinding to activate @exec
+ * @data1: a pointer for using by WM plugin
+ * @data2: a pointer for using by WM plugin
+ *
+ * Descriptor of a keybinding for a single command line action. The command
+ * execution may be altered by some @options. See also #LXKeysGlobal.
+ */
 typedef struct {
     gchar *exec;
-    GList *actions; /* elements are LXKeysAttr, empty value acceptable */
+    GList *options;
     gchar *accel1;
     gchar *accel2;
+    gpointer data1;
+    gpointer data2;
 } LXKeysApp;
 
-/* for errors */
-extern GQuark LXKEYS_ERROR;
-typedef enum {
-    LXKEYS_BAD_ARGS, /* invalid commandline arguments */
-    LXKEYS_NOT_SUPPORTED /* operation not supported */
-} LXKeysError;
 
 /* WM support plugins */
 
@@ -70,7 +107,17 @@ typedef enum {
  * @get_wm_actions: (allow-none): callback to get global actions list provided by WM
  * @get_app_keys: (allow-none): callback to get keys bound to commands
  * @set_app_key: (allow-none): callback to set a key for a command
- * @get_app_actions: (allow-none): callback to get possible actions for commands
+ * @get_app_options: (allow-none): callback to get possible actions for commands
+ *
+ * Callbacks @get_wm_keys and @get_app_keys return list which should be freed
+ * by caller (transfer container).
+ * Callbacks @get_wm_actions and @get_app_actions return list that should be
+ * not modified nor freed by caller (transfer none).
+ * Callback @get_wm_keys returns list of keybindings by @mask which is a shell
+ * style pattern for keys.
+ * Callback @set_wm_key changes a keybinding for list of actions provided. If
+ * @data::accel1 is %NULL then all keybindings for @data::actions will be
+ * cleared, otherwise keybindings will be changed accordingly.
  */
 typedef struct {
     /*< public >*/
@@ -82,7 +129,7 @@ typedef struct {
     GList *(*get_wm_actions)(gpointer config, GError **error);
     GList *(*get_app_keys)(gpointer config, const char *mask, GError **error);
     gboolean (*set_app_key)(gpointer config, LXKeysApp *data, GError **error);
-    GList *(*get_app_actions)(gpointer config, GError **error);
+    GList *(*get_app_options)(gpointer config, GError **error);
     /*< private >*/
     gpointer _reserved1;
     gpointer _reserved2;
@@ -117,12 +164,13 @@ typedef struct {
  */
 extern LXKeysPluginInit fm_module_init_lxkeys;
 
+
 /* GUI plugins */
 
 #define FM_MODULE_lxkeys_gui_VERSION 1 /* version of this API */
 
 /**
- * LXKeysPluginInit:
+ * LXKeysGUIPluginInit:
  * @run: callback to run GUI
  * @alert: callback to show an error message
  *
@@ -139,6 +187,10 @@ typedef struct {
     gpointer _reserved3;
 } LXKeysGUIPluginInit;
 
+/**
+ * This descriptor instance should be defined in each plugin code as main
+ * entry point for a GUI plugin creation.
+ */
 extern LXKeysGUIPluginInit fm_module_init_lxkeys_gui;
 
 G_END_DECLS