///////////////////////////////////////////////////////////////////////// // Copyright (c) 2011-2016 by Simon Schneegans // // This program 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 of the License, or (at // your option) any later version. // // This program 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 this program. If not, see . ///////////////////////////////////////////////////////////////////////// namespace GnomePie { ///////////////////////////////////////////////////////////////////////// /// Globally binds key stroke to given ID's. When one of the bound /// strokes is invoked, a signal with the according ID is emitted. ///////////////////////////////////////////////////////////////////////// public class BindingManager : GLib.Object { ///////////////////////////////////////////////////////////////////// /// Called when a stored binding is invoked. The according ID is /// passed as argument. ///////////////////////////////////////////////////////////////////// public signal void on_press(string id); ///////////////////////////////////////////////////////////////////// /// Called when a previously pressed binding is released again. ///////////////////////////////////////////////////////////////////// public signal void on_release(uint32 time_stamp); ///////////////////////////////////////////////////////////////////// /// A list storing bindings, which are invoked even if Gnome-Pie /// doesn't have the current focus ///////////////////////////////////////////////////////////////////// private Gee.List bindings = new Gee.ArrayList(); ///////////////////////////////////////////////////////////////////// /// Ignored modifier masks, used to grab all keys even if these locks /// are active. ///////////////////////////////////////////////////////////////////// private static uint[] lock_modifiers = { 0, Gdk.ModifierType.MOD2_MASK, Gdk.ModifierType.LOCK_MASK, Gdk.ModifierType.MOD5_MASK, Gdk.ModifierType.MOD2_MASK|Gdk.ModifierType.LOCK_MASK, Gdk.ModifierType.MOD2_MASK|Gdk.ModifierType.MOD5_MASK, Gdk.ModifierType.LOCK_MASK|Gdk.ModifierType.MOD5_MASK, Gdk.ModifierType.MOD2_MASK|Gdk.ModifierType.LOCK_MASK|Gdk.ModifierType.MOD5_MASK }; ///////////////////////////////////////////////////////////////////// /// Some variables to remember which delayed binding was delayed. /// When the delay passes without another event indicating that the /// Trigger was released, the stored binding will be activated. ///////////////////////////////////////////////////////////////////// private uint32 delayed_count = 0; private X.Event? delayed_event = null; private Keybinding? delayed_binding = null; ///////////////////////////////////////////////////////////////////// /// Helper class to store keybinding ///////////////////////////////////////////////////////////////////// private class Keybinding { public Keybinding(Trigger trigger, string id) { this.trigger = trigger; this.id = id; } public Trigger trigger { get; set; } public string id { get; set; } } ///////////////////////////////////////////////////////////////////// /// C'tor adds the event filter to the root window. ///////////////////////////////////////////////////////////////////// public BindingManager() { // init filter to retrieve X.Events Gdk.Window rootwin = Gdk.get_default_root_window(); if(rootwin != null) { rootwin.add_filter(event_filter); } } ///////////////////////////////////////////////////////////////////// /// Binds the ID to the given accelerator. ///////////////////////////////////////////////////////////////////// public void bind(Trigger trigger, string id) { if (trigger.key_code != 0) { unowned X.Display display = Gdk.X11.get_default_xdisplay(); X.ID xid = Gdk.X11.get_default_root_xwindow(); Gdk.error_trap_push(); // if bound to super key we need to grab MOD4 instead // (for whatever reason...) var modifiers = prepare_modifiers(trigger.modifiers); foreach(uint lock_modifier in lock_modifiers) { if (trigger.with_mouse) { display.grab_button(trigger.key_code, modifiers|lock_modifier, xid, false, X.EventMask.ButtonPressMask | X.EventMask.ButtonReleaseMask, X.GrabMode.Async, X.GrabMode.Async, xid, 0); } else { display.grab_key(trigger.key_code, modifiers|lock_modifier, xid, false, X.GrabMode.Async, X.GrabMode.Async); } } Gdk.flush(); Keybinding binding = new Keybinding(trigger, id); bindings.add(binding); display.flush(); } else { //no key_code: just add the bindind to the list to save optional trigger parameters Keybinding binding = new Keybinding(trigger, id); bindings.add(binding); } } ///////////////////////////////////////////////////////////////////// /// Unbinds the accelerator of the given ID. ///////////////////////////////////////////////////////////////////// public void unbind(string id) { foreach (var binding in bindings) { if (id == binding.id) { if (binding.trigger.key_code == 0) { //no key_code: just remove the bindind from the list bindings.remove(binding); return; } break; } } unowned X.Display display = Gdk.X11.get_default_xdisplay(); X.ID xid = Gdk.X11.get_default_root_xwindow(); Gee.List remove_bindings = new Gee.ArrayList(); foreach(var binding in bindings) { if(id == binding.id) { // if bound to super key we need to ungrab MOD4 instead // (for whatever reason...) var modifiers = prepare_modifiers(binding.trigger.modifiers); foreach(uint lock_modifier in lock_modifiers) { if (binding.trigger.with_mouse) { display.ungrab_button(binding.trigger.key_code, modifiers|lock_modifier, xid); } else { display.ungrab_key(binding.trigger.key_code, modifiers|lock_modifier, xid); } } remove_bindings.add(binding); } } bindings.remove_all(remove_bindings); display.flush(); } ///////////////////////////////////////////////////////////////////// /// Returns a human readable accelerator for the given ID. ///////////////////////////////////////////////////////////////////// public string get_accelerator_label_of(string id) { foreach (var binding in bindings) { if (binding.id == id) { return binding.trigger.label_with_specials; } } return _("Not bound"); } ///////////////////////////////////////////////////////////////////// /// Returns the accelerator to which the given ID is bound. ///////////////////////////////////////////////////////////////////// public string get_accelerator_of(string id) { foreach (var binding in bindings) { if (binding.id == id) { return binding.trigger.name; } } return ""; } ///////////////////////////////////////////////////////////////////// /// Returns whether the pie with the given ID is in turbo mode. ///////////////////////////////////////////////////////////////////// public bool get_is_turbo(string id) { foreach (var binding in bindings) { if (binding.id == id) { return binding.trigger.turbo; } } return false; } ///////////////////////////////////////////////////////////////////// /// Returns whether the pie with the given ID opens centered. ///////////////////////////////////////////////////////////////////// public bool get_is_centered(string id) { foreach (var binding in bindings) { if (binding.id == id) { return binding.trigger.centered; } } return false; } ///////////////////////////////////////////////////////////////////// /// Returns whether the pie with the given ID is in warp mode. ///////////////////////////////////////////////////////////////////// public bool get_is_warp(string id) { foreach (var binding in bindings) { if (binding.id == id) { return binding.trigger.warp; } } return false; } ///////////////////////////////////////////////////////////////////// /// Returns whether the pie with the given ID is auto shaped ///////////////////////////////////////////////////////////////////// public bool get_is_auto_shape(string id) { foreach (var binding in bindings) { if (binding.id == id) { return (binding.trigger.shape == 0); } } return false; } ///////////////////////////////////////////////////////////////////// /// Returns the prefered pie shape number ///////////////////////////////////////////////////////////////////// public int get_shape_number(string id) { foreach (var binding in bindings) { if (binding.id == id) { if (binding.trigger.shape == 0) break; //return default if auto-shaped return binding.trigger.shape; //use selected shape } } return 5; //default= full pie } ///////////////////////////////////////////////////////////////////// /// Returns the name ID of the Pie bound to the given Trigger. /// Returns "" if there is nothing bound to this trigger. ///////////////////////////////////////////////////////////////////// public string get_assigned_id(Trigger trigger) { var second = Trigger.remove_optional(trigger.name); if (second != "") { foreach (var binding in bindings) { var first = Trigger.remove_optional(binding.trigger.name); if (first == second) { return binding.id; } } } return ""; } ///////////////////////////////////////////////////////////////////// /// If SUPER_MASK is set in the input, it will be replaced with /// MOD4_MASK. For some reason this is required to listen for key /// presses of the super button.... ///////////////////////////////////////////////////////////////////// private Gdk.ModifierType prepare_modifiers(Gdk.ModifierType mods) { if ((mods & Gdk.ModifierType.SUPER_MASK) > 0) { mods |= Gdk.ModifierType.MOD4_MASK; mods = mods & ~ Gdk.ModifierType.SUPER_MASK; } mods &= ~(Gdk.ModifierType.BUTTON1_MASK | Gdk.ModifierType.BUTTON2_MASK | Gdk.ModifierType.BUTTON3_MASK | Gdk.ModifierType.BUTTON4_MASK | Gdk.ModifierType.BUTTON5_MASK); return mods & ~lock_modifiers[7]; } ///////////////////////////////////////////////////////////////////// /// Event filter method needed to fetch X.Events. ///////////////////////////////////////////////////////////////////// private Gdk.FilterReturn event_filter(Gdk.XEvent gdk_xevent, Gdk.Event gdk_event) { #if VALA_0_16 || VALA_0_17 X.Event* xevent = (X.Event*) gdk_xevent; #else void* pointer = &gdk_xevent; X.Event* xevent = (X.Event*) pointer; #endif if (xevent->type == X.EventType.KeyRelease) { on_release((uint32)xevent.xkey.time); } else if (xevent->type == X.EventType.KeyPress) { // remove NumLock, CapsLock and ScrollLock from key state var event_mods = prepare_modifiers((Gdk.ModifierType)xevent.xkey.state); foreach(var binding in bindings) { var bound_mods = prepare_modifiers(binding.trigger.modifiers); if(xevent->xkey.keycode == binding.trigger.key_code && event_mods == bound_mods) { if (binding.trigger.delayed) { this.activate_delayed(binding, *xevent); } else { on_press(binding.id); } } } } else if(xevent->type == X.EventType.ButtonRelease) { on_release((uint32)xevent.xkey.time); } else if(xevent->type == X.EventType.ButtonPress) { // remove NumLock, CapsLock and ScrollLock from key state var event_mods = prepare_modifiers((Gdk.ModifierType)xevent.xbutton.state); foreach(var binding in bindings) { var bound_mods = prepare_modifiers(binding.trigger.modifiers); if(xevent->xbutton.button == binding.trigger.key_code && event_mods == bound_mods) { if (binding.trigger.delayed) { this.activate_delayed(binding, *xevent); } else { on_press(binding.id); } } } } else if(xevent->type == X.EventType.ButtonRelease || xevent->type == X.EventType.KeyRelease) { this.activate_delayed(null, *xevent); } return Gdk.FilterReturn.CONTINUE; } ///////////////////////////////////////////////////////////////////// /// This method is always called when a trigger is activated which is /// delayed. Therefore on_press() is only emitted, when this method /// is not called again within 300 milliseconds. Else a fake event is /// sent in order to simulate the actual key which has been pressed. ///////////////////////////////////////////////////////////////////// private void activate_delayed(Keybinding? binding , X.Event event) { // increase event count, so any waiting event will realize that // something happened in the meantime var current_count = ++this.delayed_count; if (binding == null && this.delayed_event != null) { // if the trigger is released and an event is currently waiting // simulate that the trigger has been pressed without any inter- // ference of Gnome-Pie unowned X.Display display = Gdk.X11.get_default_xdisplay(); // unbind the trigger, else we'll capture that event again ;) unbind(delayed_binding.id); if (this.delayed_binding.trigger.with_mouse) { // simulate mouse click XTest.fake_button_event(display, this.delayed_event.xbutton.button, true, 0); display.flush(); XTest.fake_button_event(display, this.delayed_event.xbutton.button, false, 0); display.flush(); } else { // simulate key press XTest.fake_key_event(display, this.delayed_event.xkey.keycode, true, 0); display.flush(); XTest.fake_key_event(display, this.delayed_event.xkey.keycode, false, 0); display.flush(); } // bind it again bind(delayed_binding.trigger, delayed_binding.id); this.delayed_binding = null; this.delayed_event = null; } else if (binding != null) { // if the trigger has been pressed, store it and wait for any interuption // within the next 300 milliseconds this.delayed_event = event; this.delayed_binding = binding; Timeout.add(300, () => { // if nothing has been pressed in the meantime if (current_count == this.delayed_count) { this.delayed_binding = null; this.delayed_event = null; on_press(binding.id); } return false; }); } } } }