/* $Id$ Copyright (C) 2008-2011 tooar This file is part of emelfm2. emelfm2 is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version. emelfm2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with emelFM2; see the file GPL. If not, see http://www.gnu.org/licenses. */ /** @file src/e2_mousegesture.c @brief functions for handling pointer (mouse) gesture bindings */ #include "e2_mousebinding.h" #ifdef E2_MOUSECUSTOM #ifdef E2_PTRGESTURES #include "e2_option_tree.h" #include //#define EMBEDDED #include "stroke.h" //#undef EMBEDDED /* widget-specific object-data-keys */ #define WATCHES_DATA_KEY "__watches-active__" #define BUTTONS_DATA_KEY "__watch-buttons-mask__" #define GESTURE_BINDINGS_KEY "__bound-gesturecats__" /*this is GDK_MODIFIER_MASK (0x5c001fff) less GDK_LOCK_MASK = 1 << 1, GDK_MOD2_MASK = 1 << 4, (numlock) GDK_BUTTON1_MASK = 1 << 8, GDK_BUTTON2_MASK = 1 << 9, GDK_BUTTON3_MASK = 1 << 10, GDK_BUTTON4_MASK = 1 << 11, GDK_BUTTON5_MASK = 1 << 12, GDK_RELEASE_MASK = 1 << 30, i.e. 1 << 0 | 1 << 2 | 1 << 3 | 1 << 5 | 1 << 6 | 1 << 7 | 1 << 26 | 1 << 27 | 1 << 28 */ #define WATCHER_MODMASK 0x1c0000ed //config data treestore columns (some of them assumed to be same as MCOL_... for buttons store) enum { GCOL_CAT, GCOL_BTN, GCOL_NAME, GCOL_SEQ, GCOL_ACT, GCOL_ARG }; /* sizeof (gesture_id) must be a multiple of 4 (32-bits) */ typedef struct _gesture_id { //next 2 must be adjacent for checks guint32 button_flags; //flags for 32 active buttons GdkModifierType state; //flags for mod-keys (GDK_BUTTON1 etc ignored) GQuark sequark; //quarked ascii string, non-static } gesture_id; typedef struct _gesture_watch { gesture_id id; GtkWidget *from; //the watched widget gpointer device; //pointer device signature Stroke *handle; //next 2 must be adjacent for checks against part of a gesture_id guint32 transition_button_flags; //flags for 32 active buttons GdkModifierType transition_state; //flags for mod-keys (GDK_BUTTON1 etc ignored) gboolean is_stroke; gboolean maybe_stroke; //transition flag //coordinates of previous reported position, for incremental checking during transitions gdouble prevx; gdouble prevy; } gesture_watch; typedef struct _gesture_runtime { gesture_id id; GtkWidget *from;//where a pointer is/was being watched (used for widget-specific watches) void (*func) (GtkWidget*, gpointer); //after a recognised stroke, call func (from, data) gpointer data; //callback data GDestroyNotify destroyer; //data cleaner, or NULL } gesture_runtime; typedef struct _E2_GestureCatRuntime { GQuark name_quark; //quark for the category-name (which is structured like level1[.level2.level3 ...] GNode *node; //pointer to treenode for which this struct is data guint level; //level in nodes-tree and bindings-tree, effectively the //no. of '.'-separated parts of the category name - 1 GtkTreeRowReference *ref; //reference to config data model iter that's the root for this category //#ifdef E2_IDLE_BTNSYNC // gboolean synced; //TRUE when config treestore data have been converted to runtime format //#endif GPtrArray *gestures;//gesture_runtime's for each gesture in the category GSList *instances; //list of widgets bound to this binding group } E2_GestureCatRuntime; static void _e2_mousegesture_run_task (GtkWidget *from, gpointer action_string); static gboolean _stroke_watcher_button_event_cb (GtkWidget *widget, GdkEventButton *event, gpointer user_data); //tree of E2_GestureCatRuntime's static GNode *movebindings = NULL; /** @brief Find the first if any gesture_runtime for the widget described in @a gw and that conforms to: any device, current device-btns & mod-keys-state & stroke-quark @param gw pointer to watch data for item to match @param rt pointer to bindings category data @return runtime data for matching gesture or NULL if no match */ gesture_runtime *_stroke_watcher_get_stroke (gesture_watch *gw, E2_GestureCatRuntime *rt) { if (rt->gestures != NULL) { guint indx; for (indx = 0; indx < rt->gestures->len; indx++) { gesture_runtime *this; this = (gesture_runtime*) g_ptr_array_index (rt->gestures, indx); if (memcmp (&this->id, &gw->id, sizeof (gesture_id)) == 0) return this; } } return NULL; } /** @brief Find any watch on @a from that conforms to current device + btns + modes @param watches array of watch-data to check, or NULL @param from widget that is being watched @param device pointer device identifier @param buttonsmask flags for pressed button(s) @param state masked mmodifier-keys state from current event @return pointer to matching data or NULL if no match */ gesture_watch *_stroke_watcher_find_watch (GPtrArray *watches, GtkWidget *from, gpointer device, guint32 buttonsmask, GdkModifierType state) { if (watches != NULL) { guint this; for (this = 0; this < watches->len; this++) { gesture_watch *gw; gw = (gesture_watch *) g_ptr_array_index (watches, this); if (gw->from == from && gw->id.button_flags == buttonsmask && gw->id.state == state && gw->device == device ) return gw; } } return NULL; } /** @brief cleanup watches array during widget destruction etc @param watches array of gesture_watch structs or NULL @return */ static void _stroke_watcher_clear_current_watches (GPtrArray *watches) { if (watches != NULL) { printd (DEBUG, "clear all gesture_watch's for a widget"); guint count; for (count = 0; count < watches->len; count++) { gesture_watch *gw = (gesture_watch *) g_ptr_array_index (watches, count); if (gw->handle != NULL) { Stroke *tmp = gw->handle; gw->handle = NULL; stroke_destroy (tmp); } DEALLOCATE (gesture_watch, gw) } g_ptr_array_free (watches, TRUE); } } /* * @brief cleanup watches array during widget destruction etc @param watches array of gesture_watch structs or NULL @return */ /* for stand-alone usage static void _stroke_watcher_clear_bindings (GPtrArray *bindings) { if (bindings != NULL) g_ptr_array_free (bindings, TRUE); } */ /** @brief "configure-event" signal callback Take any appropriate action(s) when @a widget changes size @param widget where the signal was initiated @param event pointer to event data @param gw pointer to watch data @return FALSE always */ static gboolean _stroke_watcher_configured_cb (GtkWidget *widget, GdkEventConfigure *event, gesture_watch *gw) { /* this is the first callback when dialog is created, & also arrives later when re-sized */ if (gw->handle == NULL) { printd (DEBUG, "watched widget first configured to %d X %d", event->width, event->height); gw->handle = stroke_new (event->width, event->height); } else { printd (DEBUG, "watched widget re-configured to %d X %d", event->width, event->height); stroke_scale (gw->handle, event->width, event->height); } return FALSE; } /** @brief "motion-notify" signal callback @param widget where the signal was initiated @param event pointer to event data @param rt pointer to runtime data @return FALSE always */ static gboolean _stroke_watcher_pointermove_cb (GtkWidget *widget, GdkEventMotion *event, gesture_watch *gw) { if (event->device != gw->device) return FALSE; //ignore cb for a different pointer device // printd (DEBUG, "_stroke_watcher_pointermove_cb data: %x widget: %x at %.0f, %.0f", // gw, widget, event->x, event->y); if (gw->maybe_stroke) //data in gw indicates a transitional state { //check how much pointer movement gint diffx, diffy; diffx = ABS (event->x - gw->prevx); diffy = ABS (event->y - gw->prevy); if (diffx > 8 || diffy > 8 || (diffx + diffy) > 15) { //it is moved far enough e.g. drag threshold ? / 2 ? // (? and for race-insurance, check btns & mods too ?) if (gw->handle == NULL) //WHERE IS BEST FOR THIS ? { GtkAllocation alloc; #ifdef USE_GTK2_18 gtk_widget_get_allocation (widget, &alloc); #else alloc = widget->allocation; #endif printd (DEBUG, "create new %d X %d stroke handle for widget: %x", alloc.width, alloc.height, widget); gw->handle = stroke_new (alloc.width, alloc.height); } else //just replace existing watch-data where possible stroke_clear (gw->handle); //clear stroke backend //log 2 pts stroke_record (gw->handle, (gint)gw->prevx, (gint)gw->prevy); stroke_record (gw->handle, (gint)event->x, (gint)event->y); //real data = threshold data gw->id.button_flags = gw->transition_button_flags; gw->id.state = gw->transition_state; gw->transition_button_flags = 0; gw->transition_state = 0; gw->is_stroke = TRUE; gw->maybe_stroke = FALSE; gw->prevx = 0; gw->prevy = 0; printd (DEBUG, "_stroke_watcher_pointermove_cb: real drag started"); } else //ignore it ? or log it if (gw->is_event) ? printd (DEBUG, "_stroke_watcher_pointermove_cb: drag distance too small"); } else if (gw->is_stroke) stroke_record (gw->handle, (gint)event->x, (gint)event->y); return FALSE; } /** @brief "key-press-event" and "key-release-event" signals callback @param widget where the signal was initiated @param event pointer to event data @param gw pointer to runtime watch data @return FALSE = empower further handlers */ static gboolean _stroke_watcher_key_event_cb (GtkWidget *widget, GdkEventKey *event, gesture_watch *gw) { #ifdef USE_GTK2_10 if (event->is_modifier) #else if (e2_utils_key_is_modifier (event)) #endif { gint x, y; #ifdef USE_GTK3_0 //CHECKME pointer position if (gdk_window_get_device_position (event->window, gw->device, &x, &y, NULL) == event->window) #else if (gdk_window_get_pointer (event->window, &x, &y, NULL) == event->window) #endif { /* CHECKME GPtrArray *watches = (GPtrArray *) g_object_get_data (G_OBJECT (widget), WATCHES_DATA_KEY); //must exist here guint32 last_buttons = (guint32) g_object_get_data (G_OBJECT (widget), BUTTONS_DATA_KEY); if (_stroke_watcher_find_watch (watches, widget, gw->device, last_buttons, event->state & WATCHER_MODMASK) != NULL) { */ gw->prevx = x; gw->prevy = y; gw->maybe_stroke = TRUE; /* } else { } */ } } return FALSE; } /** @brief "button-press-event" and "button-release-event" signals callback @param widget where the signal was initiated @param event pointer to event data @param user_data UNUSED pointer specified when cb was connected @return FALSE to empower further handlers */ static gboolean _stroke_watcher_button_event_cb (GtkWidget *widget, GdkEventButton *event, gpointer user_data) { gboolean new; gboolean retval; guint32 bmask, last_buttons; GPtrArray *watches; gesture_watch *gw; printd (DEBUG,"_stroke_watcher_button_event_cb begins: event-type: %d", event->type); new = FALSE; bmask = 1 << (event->button - 1); last_buttons = (guint32) g_object_get_data (G_OBJECT (widget), BUTTONS_DATA_KEY); watches = (GPtrArray *) g_object_get_data (G_OBJECT (widget), WATCHES_DATA_KEY); if (watches != NULL) { printd (DEBUG,"current watch on widget"); //if no current watch on widget that conforms to prior (device + btns + modes) combo gw = _stroke_watcher_find_watch (watches, widget, event->device, last_buttons, event->state & WATCHER_MODMASK); //setup for watching current combo if (gw == NULL && event->type == GDK_BUTTON_PRESS) { gw = ALLOCATE0 (gesture_watch); CHECKALLOCATEDWARN (gw, return FALSE;); new = TRUE; printd (DEBUG,"BUT watch is different so new watch data created"); } } else //no watch yet if (event->type == GDK_BUTTON_PRESS) { printd (DEBUG,"_stroke_watcher_button_event_cb, UNWATCHED PRESS"); watches = g_ptr_array_new (); g_object_set_data_full (G_OBJECT (widget), WATCHES_DATA_KEY, watches, (GDestroyNotify) _stroke_watcher_clear_current_watches); gw = ALLOCATE0 (gesture_watch); CHECKALLOCATEDWARN (gw, return FALSE;); new = TRUE; } else { printd (DEBUG, "_stroke_watcher_button_event_cb, UNWATCHED RELEASE"); gw = NULL; //do nothing for new releases } if (gw == NULL) { printd (DEBUG,"_stroke_watcher_button_event_cb, nothing to do"); if (event->type == GDK_BUTTON_PRESS) last_buttons |= bmask; else { last_buttons &= ~bmask; if (last_buttons == 0) { g_signal_handlers_disconnect_matched ((gpointer) widget, G_SIGNAL_MATCH_FUNC, 0, 0, NULL, _stroke_watcher_key_event_cb, NULL); g_signal_handlers_disconnect_matched ((gpointer) widget, G_SIGNAL_MATCH_FUNC, 0, 0, NULL, _stroke_watcher_pointermove_cb, NULL); g_signal_handlers_disconnect_matched ((gpointer) widget, G_SIGNAL_MATCH_FUNC, 0, 0, NULL, _stroke_watcher_configured_cb, NULL); //kill any watches-array g_object_set_data (G_OBJECT (widget), WATCHES_DATA_KEY, NULL); } } g_object_set_data (G_OBJECT (widget), BUTTONS_DATA_KEY, GUINT_TO_POINTER (last_buttons)); return FALSE; } retval = FALSE; if (event->type == GDK_BUTTON_PRESS) { // printd (DEBUG, "_stroke_watcher_button_event_cb - PRESS"); //remember and make accessible up to 32 buttons last_buttons |= bmask; g_object_set_data (G_OBJECT (widget), BUTTONS_DATA_KEY, GUINT_TO_POINTER (last_buttons)); if (new) //no existing watch or confirmed drag yet { gw->from = widget; gw->device = event->device; gw->transition_state = event->state & WATCHER_MODMASK; gw->transition_button_flags = last_buttons; gw->maybe_stroke = TRUE; gw->prevx = event->x; gw->prevy = event->y; g_ptr_array_add (watches, gw); //probably not needed, but insurance ? // gtk_widget_add_events (widget, GDK_POINTER_MOTION_MASK); //duplicated callbacks are a potential problem g_signal_handlers_disconnect_matched ((gpointer) widget, G_SIGNAL_MATCH_FUNC, 0, 0, NULL, _stroke_watcher_key_event_cb, NULL); g_signal_handlers_disconnect_matched ((gpointer) widget, G_SIGNAL_MATCH_FUNC, 0, 0, NULL, _stroke_watcher_pointermove_cb, NULL); g_signal_handlers_disconnect_matched ((gpointer) widget, G_SIGNAL_MATCH_FUNC, 0, 0, NULL, _stroke_watcher_configured_cb, NULL); g_signal_connect (G_OBJECT (widget), "key-press-event", G_CALLBACK (_stroke_watcher_key_event_cb), gw); g_signal_connect (G_OBJECT (widget), "key-release-event", G_CALLBACK (_stroke_watcher_key_event_cb), gw); g_signal_connect (G_OBJECT (widget), "motion-notify-event", G_CALLBACK (_stroke_watcher_pointermove_cb), gw); g_signal_connect (G_OBJECT (widget), "configure-event", G_CALLBACK (_stroke_watcher_configured_cb), gw); } else //a watch is in progress but this new button may invalidate it { GPtrArray *boundto = (GPtrArray *) g_object_get_data (G_OBJECT (widget), GESTURE_BINDINGS_KEY); guint indx; for (indx = 0; indx < boundto->len; indx++) { GNode *node = ((E2_GestureCatRuntime *) g_ptr_array_index (boundto, indx))->node; while (1) { if (_stroke_watcher_get_stroke (gw, (E2_GestureCatRuntime *)node->data) != NULL) { //new button is start of a potential or actual new drag if (gw->transition_button_flags == 0) gw->transition_button_flags = gw->id.button_flags; gw->transition_button_flags |= bmask; gw->maybe_stroke = TRUE; gw->prevx = event->x; gw->prevy = event->y; break; } else if (G_NODE_IS_ROOT (node)) break; else node = node->parent; } if (gw->maybe_stroke) break; } // if (!gw->maybe_stroke) //no matching alternative gesture // { //ignore ? OR abort existing stroke ? CHECKME // } } } else if (event->type == GDK_BUTTON_RELEASE) { // printd (DEBUG, "_stroke_watcher_button_event_cb - RELEASE"); //remember and make accessible up to 32 buttons last_buttons &= ~bmask; g_object_set_data (G_OBJECT (widget), BUTTONS_DATA_KEY, GUINT_TO_POINTER (last_buttons)); if (gw->transition_button_flags == 0) gw->transition_button_flags = gw->id.button_flags; gw->transition_button_flags &= ~bmask; if (gw->transition_button_flags == 0) //all buttons gone now { printd (DEBUG, "btns all gone for widget: %x", widget); //clear redundant things before any complications arising from a match and therefore callback task //can't be any more release cb's to clear more, so zap it all now /* #ifdef USE_GTK2_20 if (!gtk_widget_get_realized(widget)) //FIXME #else if (!GTK_WIDGET_REALIZED (widget)) //FIXME #endif { gint mask = gtk_widget_get_events (widget); mask &= ~GDK_POINTER_MOTION_MASK; gtk_widget_set_events (widget, mask); } */ g_signal_handlers_disconnect_matched ((gpointer) widget, G_SIGNAL_MATCH_FUNC, 0, 0, NULL, _stroke_watcher_key_event_cb, NULL); g_signal_handlers_disconnect_matched ((gpointer) widget, G_SIGNAL_MATCH_FUNC, 0, 0, NULL, _stroke_watcher_pointermove_cb, NULL); g_signal_handlers_disconnect_matched ((gpointer) widget, G_SIGNAL_MATCH_FUNC, 0, 0, NULL, _stroke_watcher_configured_cb, NULL); if (gw->handle != NULL) { gchar *sequence; if (stroke_translate (gw->handle, TRUE, TRUE, &sequence)) { gw->id.sequark = g_quark_try_string (sequence); if (gw->id.sequark > 0) //no quark must mean no registration so no match { //============== GPtrArray *boundto = (GPtrArray *) g_object_get_data (G_OBJECT (widget), GESTURE_BINDINGS_KEY); GNode *topnode = NULL; guint indx; for (indx = 0; indx < boundto->len; indx++) { GNode *node = ((E2_GestureCatRuntime *) g_ptr_array_index (boundto, indx))->node; while (1) { GPtrArray *gestures = ((E2_GestureCatRuntime *)node->data)->gestures; guint member; for (member = 0; member < gestures->len; member++) { //============= gesture_runtime *grt = _stroke_watcher_get_stroke ( gw, (E2_GestureCatRuntime *)node->data); if (grt != NULL) { (*grt->func) (widget, grt->data); retval = TRUE; break; } //============= } if (member < gestures->len || G_NODE_IS_ROOT (node)) break; else { node = node->parent; if (node == topnode) break; } } } //============ } } } g_object_set_data (G_OBJECT (widget), WATCHES_DATA_KEY, NULL); //also kills the array } else //if still some btn(s) down { printd (DEBUG, "_stroke_watcher_button_event_cb - more btn(s)"); //the new context is potentially a different stroke ... // ? if (_stroke_watcher_find_watch (watches, widget, event->device, last_buttons, // event->state & WATCHER_MODMASK) != NULL) // { gw->prevx = event->x; gw->prevy = event->y; gw->maybe_stroke = TRUE; // } // else // { // } } } // printd (DEBUG,"_stroke_watcher_button_event_cb returns %s", (retval) ? "TRUE":"FALSE"); return retval; } /** @brief cleanup bound-gestures data in array @a gestures @param gestures pointer to an array of gesture_runtime's, or NULL @return */ static void _e2_mousegesture_free_gestures (GPtrArray *gestures) { if (gestures != NULL) { gint indx; for (indx = 0; indx < gestures->len; indx++) { //FIXME faster to use incrementing pointer directly gesture_runtime *rt = (gesture_runtime *) g_ptr_array_index (gestures, indx); if (rt->destroyer != NULL) (*rt->destroyer) (rt->data); //g_free (rt->data); DEALLOCATE (gesture_runtime, rt); } g_ptr_array_set_size (gestures, 0); } } /** @brief cleanup data for the bound gestures for the category associated with @a rt @param rt pointer to device gestures category data struct @return */ static void _e2_mousegesture_free_category (E2_GestureCatRuntime *rt) { if (rt->gestures != NULL && rt->gestures->len > 0) { _e2_mousegesture_free_gestures (rt->gestures); g_ptr_array_free (rt->gestures, TRUE); rt->gestures = NULL; } } /** @brief recursively translate a gestures treestore branch into an arrayed runtme data struct This is for "core" widgets with a standard E2_BtnWidget value. Stored data are validated before listing @param model model for the bindings treestore @param iter pointer to iter to be used to interrogate @a model @param gestures pointer to initialised array to which the created struct(s) will be appended @return */ static void _e2_mousegesture_sync_one (GtkTreeModel *model, GtkTreeIter *iter, GPtrArray *gestures) { gchar *cat, *button, *sequence, *action, *action_data; guint32 buttonsmask = 0; GdkModifierType state = 0; gboolean valid; gesture_runtime *g; gtk_tree_model_get (model, iter, GCOL_CAT, &cat, GCOL_BTN, &button, GCOL_SEQ, &sequence, GCOL_ACT, &action, GCOL_ARG, &action_data, -1); valid = (*cat == '\0'//for a gesture row, the category should be empty && *button != '\0' && *sequence != '\0' && *action != '\0'); //data may be empty, but not this if (valid) { valid = e2_mousebinding_parse_name (button, &buttonsmask, &state, NULL, TRUE); } if (valid) { E2_Action *act = e2_action_check (action); valid = (act == NULL || !(act->exclude & E2_ACTION_EXCLUDE_POINTER)); } if (valid) { g = ALLOCATE0 (gesture_runtime); #if (CHECKALLOCATEDWARN) CHECKALLOCATEDWARN (g, valid = FALSE;) #else if (g == NULL) valid = FALSE; #endif } else g = NULL; //warning prevention if (valid) { g->id.sequark = g_quark_from_string (sequence); g->id.button_flags = buttonsmask; g->id.state = state & WATCHER_MODMASK; g->func = _e2_mousegesture_run_task; g->data = (*action_data == '\0') ? g_strdup (action) : g_strconcat (action, " ", action_data, NULL); g->destroyer = g_free; g_ptr_array_add (gestures, g); } g_free (cat); g_free (button); g_free (sequence); g_free (action); g_free (action_data); } /** @brief translate, from primary treestore to runtime list, the data for a "core" widget gestures category This can be called at any time, including when there are no bindings for the category in the treestore. Pre-existing data for the category are cleared, so this cannot be used to _add_ gestures to the category @param rt pointer to data struct for the button-binding category to be processed @return if E2_IDLE_BTNSYNC applies, TRUE if the process completed properly */ static #ifdef E2_IDLE_BTNSYNC gboolean #else void #endif _e2_mousegesture_sync_category (E2_GestureCatRuntime *rt) { if (!gtk_tree_row_reference_valid (rt->ref)) { printd (WARN, "internal gesturebinding error, row reference not valid anymore"); #ifdef E2_IDLE_BTNSYNC return FALSE; #else return; #endif } GtkTreePath *path = gtk_tree_row_reference_get_path (rt->ref); if (path == NULL) { printd (WARN, "internal gestursbinding error, path is NULL"); #ifdef E2_IDLE_BTNSYNC return FALSE; #else return; #endif } printd (DEBUG, "%s e2_mousebinding_gestures_sync", g_quark_to_string (rt->name_quark)); _e2_mousegesture_free_category (rt); //get rid of any old gesture data for this category rt->gestures = g_ptr_array_new (); E2_OptionSet *set = e2_option_get ("mousedrags"); GtkTreeIter iter; gtk_tree_path_down (path); //down to the buttons level while (gtk_tree_model_get_iter (set->ex.tree.model, &iter, path)) { //convert current button to runtime form _e2_mousegesture_sync_one (set->ex.tree.model, &iter, rt->gestures); gtk_tree_path_next (path); } gtk_tree_path_free (path); //CHECKME walk the array and set data->next ptrs ? #ifdef E2_IDLE_BTNSYNC // rt->synced = TRUE; return TRUE; #else return; #endif } #ifdef E2_IDLE_BTNSYNC static gboolean _e2_mousegesture_walk_sync (GNode *node, gpointer data) { _e2_mousegesture_sync_category ((E2_GestureCatRuntime *) node->data); return FALSE; } /** @brief convert data for one or all binding category(ies) from treestore to runtime arrays (if any category remains to be processed) This func is mainloop idle callback CHECKME why sync that way? @param data pointer to binding to be processed, or NULL to do whole list @return FALSE (so the idle is cancelled) */ static gboolean _e2_mousegesture_idle_sync_cb (E2_GestureCatRuntime *data) { if (data != NULL) //sync a single BtnCatRuntime _e2_mousegesture_sync_category (data); else { g_node_traverse (movebindings, G_PRE_ORDER, G_TRAVERSE_ALL, -1, (GNodeTraverseFunc) _e2_mousegesture_walk_sync, NULL); } return FALSE; //remove this fn from event sources } #endif /** @brief find the first if any category whose name-quark matches the one in @a data @param tree node data @param data pointer to walker data struct @return TRUE when a match is found */ static gboolean _e2_mousegesture_walk_names (GNode *node, E2_NodeMatch *data) { if (data->qname == ((E2_GestureCatRuntime *) node->data)->name_quark) { data->match = node; return TRUE; } return FALSE; } /** @brief get a gesture-binding category treenode for @a name @param name the name of the binding category to search for, non-NULL and non-empty Any missing ancestor nodes are created and partially-populated @return the node for @a name, or NULL upon error */ static GNode *_e2_mousegesture_get_category (const gchar *name) { const gchar *e; guint level; GNode *retval = NULL; for (e = name, level = 0; e != NULL; e++, level++) { e = strchr (e, '.'); gchar *levelname = (e == NULL) ? (gchar *)name : g_strndup (name, e - name); GQuark qname = g_quark_from_string (levelname); if (levelname != name) g_free (levelname); E2_NodeMatch data = { qname, NULL }; if (movebindings != NULL) g_node_traverse (movebindings, G_PRE_ORDER, G_TRAVERSE_ALL, -1, (GNodeTraverseFunc) _e2_mousegesture_walk_names, &data); if (data.match == NULL) { //init runtime data E2_GestureCatRuntime *rt = ALLOCATE0 (E2_GestureCatRuntime); CHECKALLOCATEDWARN (rt, return NULL;) rt->name_quark = qname; rt->node = g_node_new (rt); if (movebindings == NULL) { // rt->node = g_node_new (rt); movebindings = rt->node; } else { //pity about doing this scan again ... g_node_traverse (movebindings, G_PRE_ORDER, G_TRAVERSE_ALL, -1, (GNodeTraverseFunc) e2_keybinding_walk_toparent, &data); /* if (data.match == NULL) can't fail { DEALLOCATE (E2_GestureCatRuntime, rt); return NULL; } */ // rt->node = g_node_new (rt); g_node_append (data.match, rt->node); } retval = rt->node; //in case we're finished now rt->level = level; } else retval = data.match; if (e == NULL) break; } return retval; } /** @brief do something after a mouse gesture has been detected This is called from inside an event callback, so BGL is on @param from the button, menu item etc where the gesture occurred @param action_string (const gchar*) instruction supplied when the gesture was registered @return */ static void _e2_mousegesture_run_task (GtkWidget *from, gpointer action_string) { gchar *command = g_strdup ((const gchar *)action_string); gchar *arg = e2_utils_bare_strchr (command, ' '); if (arg != NULL) { *arg = '\0'; arg = e2_utils_pass_whitespace (++arg); } E2_Action *action = e2_action_check (command); if (action != NULL) { e2_action_run_simple_from (command, arg, from); } else { const gchar *workdir = NULL; if (arg != NULL) { E2_PaneRuntime *rt = e2_pane_get_runtime (from, arg, &workdir); if (rt == NULL || workdir == NULL) workdir = curr_view->dir; else workdir = rt->view.dir; #ifdef E2_VFSTMP CHECKME #endif } else workdir = curr_view->dir; e2_command_run_at (command, workdir, E2_COMMAND_RANGE_DEFAULT, from); } g_free (command); } /** @brief apply gesture-binding category @a category to "core" widget @a widget If E2_TREEINCREMENT applies and @a category doesn't exist in the bindings treestore, it (and any missing ancestor(s)) is added to that store. If @a category hasn't been used before, its data are setup for runtime usage, or if E2_IDLE_BTNSYNC applies, such setup is arranged (to be done at idle time). If non-NULL, the function named @a defaults_func must have the same form as _e2_keybinding_tree_defaults (). But there is no enrolment if @a category is already in the keybindings treestore (from config file or a prior instance of a transient item that uses the category) @param widget the widget to which the binding will be attached, or NULL @param category name of the binding, structured like level1[.level2.level3 ...] @param defaults_func pointer to function which creates a tree-option array, or NULL @return */ void e2_mousegesture_enrol (GtkWidget *widget, const gchar *category, void (*defaults_func)(E2_OptionSet*)) { GNode *member = _e2_mousegesture_get_category (category); if (member == NULL) return; E2_GestureCatRuntime *rt = (E2_GestureCatRuntime *)member->data; gboolean first = (rt->ref == NULL); //first-time usage of category if (first) { //add iter to bindings config treestore if not already present E2_OptionSet *set = e2_option_get ("mousedrags"); GtkTreeIter iter; #ifdef E2_TRANSIENTBINDINGS gboolean old = #endif e2_tree_get_lowest_iter_for_str (set->ex.tree.model, GCOL_CAT, &iter, category); GtkTreePath *path = gtk_tree_model_get_path (set->ex.tree.model, &iter); rt->ref = gtk_tree_row_reference_new (set->ex.tree.model, path); gtk_tree_path_free (path); #ifdef E2_TRANSIENTBINDINGS if (defaults_func != (void(*)(E2_OptionSet*))NULL && !old) { //run the defaults-install function, which in turn calls //e2_option_tree_setup_defaults() which sets up a pointer array at //dummy_set.ex.tree.def.tree_strings E2_OptionSet dummyset; (*defaults_func) (&dummyset); gchar **split = dummyset.ex.tree.def.tree_strings; //adjust start-index of parsed array const gchar *p = strrchr (category, '.'); p = (p == NULL) ? category : p + sizeof (gchar); gint i; for (i = 0; split[i] != NULL && strstr (split[i], p) == NULL; i++); if (G_UNLIKELY (split[i] == NULL)) i = 0; //parse the data & add it to the bindings store e2_option_tree_set_from_array (set->name, &split[i], NULL, &iter); g_strfreev (split); } #endif #ifdef E2_IDLE_BTNSYNC g_idle_add ((GSourceFunc) _e2_mousegesture_idle_sync_cb, rt); printd (DEBUG, "arranged gestures bindings idle-sync for %s", category); #else //translate the option tree data to rt form, now _e2_mousegesture_sync_category (rt); #endif } if (G_LIKELY (widget != NULL)) { printd (DEBUG, "register widget %x for gestures category '%s'", widget, category); GPtrArray *boundto = (GPtrArray *) g_object_get_data (G_OBJECT (widget), GESTURE_BINDINGS_KEY); if (boundto == NULL) //this is the first time widget is enrolled { //the model is: just one cb for all bindings g_signal_connect (G_OBJECT (widget), "button-press-event", G_CALLBACK (_stroke_watcher_button_event_cb), NULL); g_signal_connect (G_OBJECT (widget), "button-release-event", G_CALLBACK (_stroke_watcher_button_event_cb), NULL); boundto = g_ptr_array_new (); g_object_set_data_full (G_OBJECT (widget), GESTURE_BINDINGS_KEY, boundto, (GDestroyNotify) e2_keybinding_clear_widget_bindings); //or _stroke_watcher_clear_bindings); //widget remembers it's in the category g_ptr_array_add (boundto, rt); //and vice versa rt->instances = g_slist_append (rt->instances, widget); } else //already in category(ies) { //ensure widget is in only 1 category per categories-branch gboolean descendant = FALSE; guint indx; for (indx = 0; indx < boundto->len; indx++) { E2_GestureCatRuntime *this = (E2_GestureCatRuntime *)g_ptr_array_index (boundto, indx); if (g_node_is_ancestor (this->node, rt->node)) { g_ptr_array_remove (boundto, rt); this->instances = g_slist_remove_all (this->instances, widget); break; } else if (this->node == rt->node || g_node_is_ancestor (rt->node, this->node)) { descendant = TRUE; break; } } if (!descendant) //no same- or lower-level binding yet for this widget { //widget remembers it's in the category g_ptr_array_add (boundto, rt); //and vice versa rt->instances = g_slist_append (rt->instances, widget); } } } } /** @brief unregister a widget from a category @param node node in categories tree @param data pointer to data for widget to be unregistered @return FALSE to keep the walk going */ static gboolean _e2_mousegesture_walk_unregister (GNode *node, E2_NodeData *data) { E2_GestureCatRuntime *rt = node->data; if (data->qname == rt->name_quark || data->qname == 0) { GSList *widgets; for (widgets = rt->instances; widgets != NULL; widgets = widgets->next) { if (data->widget == widgets->data || data->widget == NULL) { if (GTK_IS_WIDGET (widgets->data)) { //in case widget isn't to be destroyed g_signal_handlers_disconnect_matched (widgets->data, G_SIGNAL_MATCH_FUNC, 0, 0, NULL, _stroke_watcher_button_event_cb, NULL); printd (DEBUG, "UNregister gesture-binding for widget"); gpointer boundto = g_object_get_data (G_OBJECT (widgets->data), GESTURE_BINDINGS_KEY); if (boundto != NULL) { while (g_ptr_array_remove_fast ((GPtrArray *)boundto, rt)); //probably only one } } else printd (DEBUG, "No UNregister gesture-binding for destroyed widget"); widgets->data = NULL; //don't remove, list pointer cannot change yet } } rt->instances = g_slist_remove_all (rt->instances, NULL); } return FALSE; } /** @brief cancel binding of @a category to @a widget @param widget the widget which is to be unbound, or NULL for all widgets @param category name of binding to be processed, or NULL for all categories @return */ void e2_mousegesture_disrol (GtkWidget *widget, const gchar *category) { printd (DEBUG, "e2_mousegesture_disrol, category: %s, widget: _", (category) ? category : "ALL"); GQuark qname; if (category != NULL) { qname = g_quark_try_string (category); if (qname == 0) return; //nothing will match, so nothing to do } else qname = 0; E2_NodeData data = { qname, widget }; g_node_traverse (movebindings, G_PRE_ORDER, G_TRAVERSE_ALL, -1, (GNodeTraverseFunc) _e2_mousegesture_walk_unregister, &data); } /** @brief block all gesture callbacks for a widget @param node node in categories tree @param data pointer to data for widget to be blocked @return FALSE */ static gboolean _e2_mousegesture_walk_block (GNode *node, E2_NodeData *data) { E2_GestureCatRuntime *rt = node->data; if (data->qname == rt->name_quark || data->qname == 0) { GSList *widgets; for (widgets = rt->instances; widgets != NULL; widgets = widgets->next) { if (data->widget == widgets->data || data->widget == NULL) g_signal_handlers_block_matched (widgets->data, G_SIGNAL_MATCH_FUNC, 0, 0, NULL, _stroke_watcher_button_event_cb, NULL); } } return FALSE; } /** @brief block operation of binding @a category to @a widget @param widget the widget which is to be blocked, or NULL for all widgets @param category name of binding to be processed, or NULL for all categories @return */ void e2_mousegesture_block (GtkWidget *widget, const gchar *category) { printd (DEBUG, "e2_mousegesture_block, category: %s, widget: _", (category) ? category : "ALL"); GQuark qname; if (category != NULL) { qname = g_quark_try_string (category); if (qname == 0) return; //nothing will match, so nothing to do } else qname = 0; E2_NodeData data = { qname, widget }; g_node_traverse (movebindings, G_PRE_ORDER, G_TRAVERSE_ALL, -1, (GNodeTraverseFunc) _e2_mousegesture_walk_block, &data); } /** @brief unblock all gesture callbacks for a widget @param node node in categories tree @param data pointer to data for widget to be unblocked @return FALSE to keep the walk going */ static gboolean _e2_mousegesture_walk_unblock (GNode *node, E2_NodeData *data) { E2_GestureCatRuntime *rt = node->data; if (data->qname == rt->name_quark || data->qname == 0) { GSList *widgets; for (widgets = rt->instances; widgets != NULL; widgets = widgets->next) { if (data->widget == widgets->data || data->widget == NULL) g_signal_handlers_unblock_matched (widgets->data, G_SIGNAL_MATCH_FUNC, 0, 0, NULL, _stroke_watcher_button_event_cb, NULL); } } return FALSE; } /** @brief unblock operation of binding @a category to @a widget @param widget the widget which is to be unblocked, or NULL for all widgets @param category name of binding to be processed, or NULL for all categories @return */ void e2_mousegesture_unblock (GtkWidget *widget, const gchar *category) { printd (DEBUG, "e2_mousegesture_unblock, category: %s, widget: _", (category) ? category : "ALL"); GQuark qname; if (category != NULL) { qname = g_quark_try_string (category); if (qname == 0) return; //nothing will match, so nothing to do } else qname = 0; E2_NodeData data = { qname, widget }; g_node_traverse (movebindings, G_PRE_ORDER, G_TRAVERSE_ALL, -1, (GNodeTraverseFunc) _e2_mousegesture_walk_unblock, &data); } /** @brief cleanup runtime data for a binding @param node node in categories tree @param data pointerised TRUE/FALSE, TRUE to fully kill the node data @return FALSE to keep the walk going */ static gboolean _e2_mousegesture_walk_categories (GNode *node, gpointer data) { E2_GestureCatRuntime *rt = node->data; gtk_tree_row_reference_free (rt->ref); rt->ref = NULL; _e2_mousegesture_free_category (rt); if (rt->instances != NULL) { /* if (data != NULL) { //if bound widgets are not being destroyed, disconnect signals with data = rt GSList *widgets; for (widgets = rt->instances; widgets != NULL; widgets = widgets->next) { if (GTK_IS_WIDGET (widgets->data)) //the bound widget may have been destroyed { g_signal_handlers_disconnect_matched (widgets->data, G_SIGNAL_MATCH_FUNC, //| G_SIGNAL_MATCH_DATA, 0, 0, NULL, _stroke_watcher_button_event_cb, NULL); } } } */ g_slist_free (rt->instances); rt->instances = NULL; } if (data != NULL) { DEALLOCATE (E2_GestureCatRuntime, rt); g_node_destroy (node); } return FALSE; } /** @brief cleanup some gestures bindings' runtime data The bindings data list is not affected Callbacks are checked before re-connection @return */ void e2_mousegesture_clean (void) { g_node_traverse (movebindings, G_PRE_ORDER, G_TRAVERSE_ALL, -1, (GNodeTraverseFunc) _e2_mousegesture_walk_categories, NULL); } /* * @brief cleanup all gestures data Presently unused @return */ /*void e2_mousegesture_free_all (void) { g_node_traverse (movebindings, G_POST_ORDER, G_TRAVERSE_ALL, -1, (GNodeTraverseFunc) _e2_mousegesture_walk_categories, GINT_TO_POINTER (1)); } */ /** @brief install default tree options for gesture bindings This function is called only if set data is missing from the config file @param set pointer to set data @return */ static void _e2_mousegesture_tree_defaults (E2_OptionSet *set) { //the button name strings are parsed by gtk, no translation is possible //button codes may be 1 ... whatever //columns: 0 cat, 1 button, 2 identifier, 3 sequence, 4 action, 5 action_data e2_option_tree_setup_defaults (set, g_strdup("mousedrags=<"), //internal name g_strconcat(_C(17),"|||||",NULL), //_("general" g_strconcat("\t",_C(23),"|||||",NULL), //_("main" g_strconcat("\t\t",_C(33),"|||||",NULL), //_("panes" g_strconcat("\t\t\t|3|^|8,2|",_A(13),".",_A(51),"|",NULL), //"go up g_strconcat("\t\t\t|3|<-|6,4|",_A(13),".",_A(49),"|",NULL), //"go back g_strconcat("\t\t\t|3|->|4,6|",_A(13),".",_A(50),"|",NULL), //"go forward g_strconcat("\t\t",_C(12),"|||||",NULL), //_"directory line" g_strconcat("\t\t",_C(5),"|||||",NULL), //_("command line" g_strconcat("\t\t",_C(28),"|||||",NULL), //_("output" g_strconcat("\t\t\t|3||2,8|",_A(10),".",_A(31),"|1",NULL), //"adjust ratio g_strconcat("\t\t\t|3|^|8,2|",_A(10),".",_A(69),"|",NULL), //"page up" g_strconcat("\t",_C(11),"|||||",NULL), //_("dialogs" g_strdup(">"), NULL); } /** @brief initialize gestures tree-option @return */ void e2_mousegesture_options_register (void) { gchar *title = _("pointer gestures"); //no screen rebuilds needed after any change to these options gchar *group_name = g_strconcat(_C(20),".",title,NULL); E2_OptionSet *set = e2_option_tree_register ("mousedrags", group_name, title, NULL, e2_mousebinding_tree_selection_check_cb, e2_mousebinding_tree_draggable_check_cb, E2_OPTION_TREE_UP_DOWN | E2_OPTION_TREE_ADD_DEL, E2_OPTION_FLAG_ADVANCED | E2_OPTION_FLAG_FREEGROUP | E2_OPTION_FLAG_BUILDBUTTONS); e2_option_tree_add_column (set, _("Category"), E2_OPTION_TREE_TYPE_STR, 0, "", E2_OPTION_TREE_COL_NOT_EDITABLE, NULL, NULL); e2_option_tree_add_column (set, _("Button"), E2_OPTION_TREE_TYPE_BUTTON, 0, "", 0, e2_mousebinding_visible_check_cb, NULL); e2_option_tree_add_column (set, _("Identifier"), E2_OPTION_TREE_TYPE_STR, 0, "", 0, e2_mousebinding_visible_check_cb, NULL); e2_option_tree_add_column (set, _("Sequence"), E2_OPTION_TREE_TYPE_GESTURE, 0, "", 0, e2_mousebinding_visible_check_cb, NULL); e2_option_tree_add_column (set, _("Action"), E2_OPTION_TREE_TYPE_SEL, 0, "", 0, e2_mousebinding_visible_check_cb, GINT_TO_POINTER (E2_ACTION_EXCLUDE_LAYOUT)); e2_option_tree_add_column (set, _("Argument"), E2_OPTION_TREE_TYPE_SEL , 0, "", 0, e2_mousebinding_visible_check_cb, GINT_TO_POINTER (E2_ACTION_EXCLUDE_GENERAL | E2_ACTION_EXCLUDE_ACCEL //CHECKME EXCLUDE POINTER ? | E2_ACTION_EXCLUDE_LAYOUT | E2_ACTION_EXCLUDE_TOGGLE)); e2_option_tree_create_store (set); e2_option_tree_prepare_defaults (set, _e2_mousegesture_tree_defaults); } #endif //def E2_PTRGESTURES #endif //def E2_MOUSECUSTOM