/*
Copyright (c) 2011 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);
/////////////////////////////////////////////////////////////////////
/// 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
};
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) {
Gdk.Window rootwin = Gdk.get_default_root_window();
X.Display display = Gdk.x11_drawable_get_xdisplay(rootwin);
X.ID xid = Gdk.x11_drawable_get_xid(rootwin);
Gdk.error_trap_push();
foreach(uint lock_modifier in lock_modifiers) {
if (trigger.with_mouse) {
display.grab_button(trigger.key_code, trigger.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, trigger.modifiers|lock_modifier,
xid, false, X.GrabMode.Async, X.GrabMode.Async);
}
}
Gdk.flush();
Keybinding binding = new Keybinding(trigger, id);
bindings.add(binding);
display.flush();
}
}
/////////////////////////////////////////////////////////////////////
/// Unbinds the accelerator of the given ID.
/////////////////////////////////////////////////////////////////////
public void unbind(string id) {
Gdk.Window rootwin = Gdk.get_default_root_window();
X.Display display = Gdk.x11_drawable_get_xdisplay(rootwin);
X.ID xid = Gdk.x11_drawable_get_xid(rootwin);
Gee.List remove_bindings = new Gee.ArrayList();
foreach(var binding in bindings) {
if(id == binding.id) {
foreach(uint lock_modifier in lock_modifiers) {
if (binding.trigger.with_mouse) {
display.ungrab_button(binding.trigger.key_code, binding.trigger.modifiers|lock_modifier, xid);
} else {
display.ungrab_key(binding.trigger.key_code, binding.trigger.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 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) {
foreach (var binding in bindings) {
var first = binding.trigger.name.replace("[turbo]", "").replace("[delayed]", "");
var second = trigger.name.replace("[turbo]", "").replace("[delayed]", "");
if (first == second) {
return binding.id;
}
}
return "";
}
/////////////////////////////////////////////////////////////////////
/// Event filter method needed to fetch X.Events.
/////////////////////////////////////////////////////////////////////
private Gdk.FilterReturn event_filter(Gdk.XEvent gdk_xevent, Gdk.Event gdk_event) {
void* pointer = &gdk_xevent;
X.Event* xevent = (X.Event*) pointer;
if(xevent->type == X.EventType.KeyPress) {
foreach(var binding in bindings) {
// remove NumLock, CapsLock and ScrollLock from key state
uint event_mods = xevent.xkey.state & ~ (lock_modifiers[7]);
if(xevent->xkey.keycode == binding.trigger.key_code && event_mods == binding.trigger.modifiers) {
if (binding.trigger.delayed) {
this.activate_delayed(binding, *xevent);
} else {
on_press(binding.id);
}
}
}
}
else if(xevent->type == X.EventType.ButtonPress) {
foreach(var binding in bindings) {
// remove NumLock, CapsLock and ScrollLock from key state
uint event_mods = xevent.xbutton.state & ~ (lock_modifiers[7]);
if(xevent->xbutton.button == binding.trigger.key_code && event_mods == binding.trigger.modifiers) {
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
Gdk.Window rootwin = Gdk.get_default_root_window();
X.Display display = Gdk.x11_drawable_get_xdisplay(rootwin);
// unbind the trigger, else we'll capture that event again ;)
unbind(delayed_binding.id);
if (this.delayed_binding.trigger.with_mouse) {
// simulate mouse click
X.Test.fake_button_event(display, this.delayed_event.xbutton.button, true, 0);
display.flush();
X.Test.fake_button_event(display, this.delayed_event.xbutton.button, false, 0);
display.flush();
} else {
// simulate key press
X.Test.fake_key_event(display, this.delayed_event.xkey.keycode, true, 0);
display.flush();
X.Test.fake_key_event(display, this.delayed_event.xkey.keycode, false, 0);
display.flush();
}
// bind it again
bind(delayed_binding.trigger, delayed_binding.id);
} 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;
});
}
}
}
}