/////////////////////////////////////////////////////////////////////////
// Copyright (c) 2011-2017 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;
/////////////////////////////////////////////////////////////////////
/// Used to identify wayland sessions.
/////////////////////////////////////////////////////////////////////
private bool wayland = GLib.Environment.get_variable("XDG_SESSION_TYPE") == "wayland";
/////////////////////////////////////////////////////////////////////
/// 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) {
// global key grabbing is impossible on wayland
if (!wayland && 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 || wayland) {
//no key_code or wayland: 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.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.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) {
on_release((uint32)xevent.xkey.time);
this.cancel_activate_delayed();
}
return Gdk.FilterReturn.CONTINUE;
}
/////////////////////////////////////////////////////////////////////
/// This method is always called when a trigger is activated which is
/// delayed. Therefore on_press() is only emitted, when
/// cancel_activate_delayed is not called again 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) {
if (this.delayed_binding == null) {
// the current event count is captured in the lambda below. If
// cancel_activate_delayed is not called within 300 milliseconds,
// the binding can be activated
var current_count = this.delayed_count;
// 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;
});
}
}
private void cancel_activate_delayed() {
if (this.delayed_event != null) {
// increase event count, so any waiting event will realize that
// something happened in the meantime
++this.delayed_count;
// 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;
}
}
}
}