#!/usr/bin/python

# Xhotkeys - Bind keys and mouse events to commands in the X-Window
#
# Copyright (C) 2006 Arnau Sanchez
#
# 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 2
# of the License or 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, write to the Free Software Foundation, 
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.

# Some ideas taken from KeyLauch (thanks to Ken Lynch and Stefan Pfetzing)
# Feel free to contact me at <arnau@ehas.org>

# Python modules
import os, sys, time, signal
import re, optparse, string, select
import inspect, threading

# Xlib modules
import Xlib.display, Xlib.X, Xlib.XK
import Xlib.Xatom, Xlib.keysymdef.xkb

# PyGTK and Glade modules
import pygtk
pygtk.require("2.0")
import gtk, gtk.glade, gobject

####################################
__version__ = "$Revision: 1.7 $"
__author__ = "Arnau Sanchez <arnau@ehas.org>"
__depends__ = ['Xlib', 'PyGTK2', 'Glade', 'Python-2.3']
__copyright__ = """Copyright (C) 2006 Arnau Sanchez <arnau@ehas.org>.
This code is distributed under the terms of the GNU General Public License."""

### Global variables
NAME = "xhotkeys"
DISABLE_HOTKEY = Xlib.XK.XK_BackSpace
DISABLED_STRING = "disabled"
MOUSE_BUTTONS = "button"

## Verbose levels
LEVELS_STRING = "error", "info", "debug"
ERROR, INFO, DEBUG = range(3)

### KeySymbol to (XMask/strMask) conversion
x, xk, xkb = Xlib.X, Xlib.XK, Xlib.keysymdef.xkb

keysym_to_mask = {
	xk.XK_Shift_L: (x.ShiftMask, "shift"), 
	xk.XK_Shift_R: (x.ShiftMask, "shift"),
	xk.XK_Control_L: (x.ControlMask, "control"),
	xk.XK_Control_R: (x.ControlMask, "control"),
	xk.XK_Alt_L: (x.Mod1Mask, "alt"),
	xk.XK_Alt_R: (x.Mod1Mask, "alt"),
	xk.XK_Super_L: (x.Mod4Mask, "winkey"),
	xkb.XK_ISO_Level3_Shift: (x.Mod5Mask, "altgr")
}

########################################
## From Peter Norvig's Infrequently Answered Questions
def create_dict(**dict): 
	return dict

########################################
def keycodes_to_mask(display, keycodes):
	"""Walk keycodes elements and returns only modifiers masks"""
	mask = 0
	for keycode in keycodes: 
		for keysym, tmask in keysym_to_mask.items():
			if keycode == display.keysym_to_keycode(keysym):
				mask = mask | tmask[0]
	return mask

########################################
def string_to_mask(str_modifiers):
	"""Return modifier mask from its name: "shift", "alt", ..."""
	mask = 0
	for str_modifier in str_modifiers:
		for keysym, tmask in keysym_to_mask.items():
			if tmask[1] == str_modifier.lower():
				mask = mask | keysym_to_mask[keysym][0]
				break
	return mask

########################################
def get_total_mask():
	"""Return OR-mask of all modifiers"""
	mask = 0
	for xmask, name in keysym_to_mask.values():
		mask = mask | xmask
	return mask
	
#######################################################
def debug(text, verbose_level, level, exit):
	"""Give some debug messages.
	
	verbose_level -- verbose level of this message: DEBUG, INFO, ERROR
	level -- level: DEBUG, INFO, ERROR
	exit -- if not None, exit with that return value"""
	if level <= verbose_level:
		text = "%s: %s" %(LEVELS_STRING[level], text)
		sys.stderr.write(text + "\n")
		sys.stderr.flush()
	if exit != None: 
		sys.exit(exit)

###################################################
def run_command(command):
	"""Run command using SHELL variable from environment. If not found, use sh"""
	if not command: return
	shell = os.getenv("SHELL", "/bin/sh")
	os.spawnvp(os.P_NOWAIT, shell, [os.path.basename(shell), "-c", command])

######################################
######################################
class XhotkeysServer(threading.Thread):
	"""Wait for key combinations and run command if configured"""

	##################################################
	def __init__(self, verbose_level = INFO):
		"""Init server Hotkey server. verbose_level -- DEBUG, INFO, ERROR"""
		self.verbose_level = verbose_level
		self.display = Xlib.display.Display()
		self.root = self.display.screen().root
		self.combinations = []
		self.init_signals([signal.SIGCHLD])
		self.get_keylock_masks()
		threading.Thread.__init__(self)

	########################################
	def string_to_keycode(self, strcode):
		if len(strcode) > 2 and strcode[0] == "@" and strcode[-1] == "@":
			value = strcode[1:-1].lower()
			if value.find(MOUSE_BUTTONS) == 0:
				return "button", int(value[-1])
			return "key", int(value)
		for key in inspect.getmembers(Xlib.XK):
			if len(key) != 2 or key[0].find("XK_") != 0 or strcode != key[0].replace("XK_", ""): 
				continue
			return "key", self.display.keysym_to_keycode(key[1])

	##################################################
	def get_keylock_masks(self):
		"""Get Caps/Num/Scroll Lock masks. Needed because must their value 
		must be ignored in key combinations"""
		kc, ktk = Xlib.XK, self.display.keysym_to_keycode
		keycodes = {ktk(kc.XK_Caps_Lock): "caps", ktk(kc.XK_Num_Lock): "num", ktk(kc.XK_Scroll_Lock): "scroll"}
		self.locks_mask = dict([(x, 0) for x in keycodes.values()])
		for index, mask in enumerate(self.display.get_modifier_mapping()):
			try: self.locks_mask[keycodes[mask[0]]] = 1 << index
			except: pass

	##################################################
	def init_signals(self, signals):
		"""Get name signals and set handler for child process signals"""
		self.signames = {}
		for key, value in inspect.getmembers(signal):
			if key.find("SIG") == 0 and key.find("SIG_") != 0: 
				self.signames[value] = key
		for sig in signals:
			signal.signal(sig, self.signal_handler)

	####################################
	def signal_handler(self, signum, frame):
		"""Process received signals:
		
		SIGCHLD -- Waits return value for a finished child
		"""	
		signame = self.signames.get(signum, "unknown")
		self.debug("signal received: %s" %signame)
		if signum == signal.SIGCHLD:
			self.debug("wait a child")
			try: pid, retval = os.wait()
			except: self.debug("error waiting a child"); return
			self.debug("child pid = %d - return value = %d" %(pid, retval))

	#######################################################
	def debug(self, text, level = DEBUG, exit = None):
		debug(text, self.verbose_level, level, exit)

	#######################################
	def ungrab(self):
		"""Ungrab keycoard bindings"""
		self.display.ungrab_keyboard(Xlib.X.CurrentTime)
		self.display.flush()
		self.root.ungrab_key(Xlib.X.AnyKey, Xlib.X.AnyModifier)
		self.root.ungrab_button(Xlib.X.AnyButton, Xlib.X.AnyModifier)
		
	#######################################
	def set_combination(self, combinations):
		"""Set the current hotkey table"""
		self.ungrab()
		self.combinations = []
		for comb in combinations:
			hotkey = comb["hotkey"]
			if hotkey.lower() == DISABLED_STRING:
				type = mask = keycode = 0
			else:
				modifiers = re.findall("<(\w+)>", hotkey)
				mask = string_to_mask(modifiers)
				try: key = re.findall("(@?\w+@?)$", hotkey)[0]
				except: self.debug("error parsing hotkey: %s" %hotkey, ERROR); continue
				type, keycode = self.string_to_keycode(key)

			newcomb = create_dict(name=comb["name"], command=comb["command"], \
				type=type, mask=mask, keycode=keycode)
			self.combinations.append(newcomb)

		self.configure()

	##################################################
	def stop(self):
		"""Stop the thread"""
		self.stop_flag = True

	##################################
	def grab_button_corrected(self, button, modifiers, owner_events, event_mask, \
		pointer_mode, keyboard_mode, confine_to, cursor, onerror = None):
		"""This need an explanation: at this moment, June 2006, Python-Xlib is broken
		at grab_button(), a basic function to grab mouse events. A bug on this issue was 
		already reported at the ends of 2004, with no respone, the project seems 
		temporaly dead. 
		
		For now, we must call GrabButton() ourselves, with the correct parameters. 
		The problem was that the "cursor=cursor" parameter was missing, that's all"""
		Xlib.protocol.request.GrabButton(display = self.root.display, onerror = onerror, \
			owner_events = owner_events, grab_window = self.root.id, event_mask = event_mask, \
			pointer_mode = pointer_mode, keyboard_mode = keyboard_mode, \
			confine_to = confine_to, button = button, cursor = cursor, modifiers = modifiers)

	##################################################
	def configure(self):
		"""Set hotkeys and hotbuttons from the current table"""
		mode = Xlib.X.GrabModeAsync
		# I don't know why, but events are received even without calling change_attributes...
		self.root.change_attributes(event_mask = Xlib.X.KeyPressMask | Xlib.X.ButtonPressMask)
		for comb in self.combinations:
			if not comb["keycode"]:	continue
			key, mod, type = [comb[x] for x in ("keycode", "mask", "type")]
			cl, nl, sl = [self.locks_mask[x] for x in ("caps", "num", "scroll")]
			for mask in (0, cl, nl, sl, cl | nl, cl | sl, cl | nl | sl):
				if type == "button":
					self.grab_button_corrected(key, mod | mask, self.root, \
						Xlib.X.ButtonPressMask, mode, mode, 0, 0)
				elif type == "key":
					self.root.grab_key(key, mod | mask, 0, mode, mode)
		

	##################################################
	def run(self):
		"""Main run thread. Read X events and look for configured hotkeys"""
		self.configure()
		self.stop_flag = False
		while not self.stop_flag:
			while self.display.pending_events():
				event = self.display.next_event()
				#print event
				if event.type == Xlib.X.KeyPress:
					keycode = event.detail
					mask = event.state & get_total_mask()
					for comb in self.combinations:
						if comb["type"] == "key" and comb["keycode"] == keycode and comb["mask"] == mask:
							run_command(comb["command"])
				elif event.type == Xlib.X.ButtonPress:
					button = event.detail
					mask = event.state & get_total_mask()
					for comb in self.combinations:
						if comb["type"] == "button" and comb["keycode"] == button and comb["mask"] == mask:
							run_command(comb["command"])
			
			# Give the CPU a breath
			time.sleep(0.2)
		
		self.ungrab()


#######################
#######################
class Xhotkeys:
	####################################
	def __init__(self, cfile, pidfile, verbose_level = DEBUG):
		self.cfile = cfile
		self.verbose_level = verbose_level 
		self.pidfile = pidfile
		self.display = Xlib.display.Display()
		self.root = self.display.screen().root
		self.hkserver = None

	####################################
	def signal_handler(self, signum, frame):
		"""Process signals
		
		SIGHUP -- reload configuration
		SIGTERM/SIGINT -- make a clean exit"""
		
		signame = self.signames.get(signum, "unknown")
		self.debug("signal received: %s" %signame)
		
		if signum == signal.SIGHUP:
			self.debug("send reload configuration command")
			self.reload()
		elif signum in (signal.SIGTERM, signal.SIGINT):
			self.debug("%s stopped (pid %d)" %(NAME, os.getpid()), INFO)
			if self.state == "server": self.delete_pidfile()
			if self.hkserver: self.stop_server()
			os._exit(0)

	#######################################################
	def debug(self, text, level = DEBUG, exit = None):
		debug(text, self.verbose_level, level, exit)

	#########################################
	def reload(self):
		self.read_configuration()
		self.hkserver.set_combination(self.combinations)

	########################################
	def is_keycode_modifier(self, keycode):
		return bool([ks for ks in keysym_to_mask if keycode == self.display.keysym_to_keycode(ks)])

	########################################
	def keycodes_to_string(self, keycodes, default_string = DISABLED_STRING):
		if not keycodes: return default_string			
		keycodes_to_modifier = {}
		for keysym, tmask in keysym_to_mask.items():
			modifier = tmask[1]
			keycodes_to_modifier[self.display.keysym_to_keycode(keysym)] = modifier

		strmod = strkey = ""
		current_modifiers = []
		for keycode in keycodes:
			if keycode in keycodes_to_modifier:
				string = keycodes_to_modifier[keycode]
				if string in current_modifiers: continue
				strmod += "<" + string + ">"
				current_modifiers.append(string)
				continue
			keysym = self.display.keycode_to_keysym(keycode, 0)
			for key in inspect.getmembers(Xlib.XK):
				if len(key) != 2 or key[0].find("XK_") != 0 or key[1] != keysym: continue
				strkey = key[0].replace("XK_", "")
				break
			else: strkey = "@%d@" %keycode
			break
		return strmod + strkey

	#######################################
	def read_configuration(self, exit_on_error = True):
		self.combinations = []
		self.debug("opening configuration file: %s" %self.cfile, INFO)
		try: fd = open(self.cfile)
		except IOError: 
			if not exit_on_error: return []
			self.debug("configuration file not found: %s" %self.cfile)
			return self.combinations
			
		for numline, line in enumerate(fd.readlines()):
			# example: calculator=<shift><control>F1:xcalc
			line = line.strip()
			if not line or line[0] == "#": continue
			try: 
				name, options = [x.strip() for x in line.split("=", 1)]
				hotkey, command = options.split(":", 1)
			except: continue
			comb = create_dict(name=name, hotkey=hotkey, command=command)
			for combination in self.combinations:
				if combination["name"] == name: 
					self.debug("hotkey name (%s) already loaded, ignored" %name, ERROR)
					comb = None
					break
			if not comb: continue
			self.debug("adding key combination: %s = %s -> %s" %(comb["name"], comb["hotkey"], comb["command"]))
			self.combinations.append(comb)
		self.debug("%d combinations installed" %len(self.combinations), INFO)
		fd.close()

	###################################
	def delete_pidfile(self):
		self.debug("deleting pidfile: %s" %self.pidfile)
		try: os.unlink(self.pidfile)
		except: self.debug("error deleting pidfile", ERROR)

	#######################################
	def write_configuration(self):
		self.debug("opening configuration file to get old configuration: %s" %self.cfile, INFO)
		try: fd = open(self.cfile, "r")
		except IOError: original = []
		else: original = fd.readlines()	; fd.close()
		
		self.debug("opening configuration file for writing: %s" %self.cfile, INFO)
		try: fd = open(self.cfile, "w")
		except IOError:
			self.debug("configuration file couldn't be opened for writing: %s" %self.cfile, ERROR)
			sys.exit(1)

		entries = {}
		for comb in self.combinations:
			entries[comb["name"]] = comb["hotkey"] + ":" + comb["command"]

		entries_processed = []
		for oline in original:
			line = oline.strip()
			if not line: fd.write(oline); continue
			try: name, value = line.split("=", 1)
			except: fd.write(oline); continue
			name, value = name.strip(), value.strip()
			if name not in entries:
				self.debug("old entry removed: %s" %oline.strip())
				continue
			if value == entries[name]:
				self.debug("keep old entry: %s" %oline.strip())
				entries_processed.append(name)
				fd.write(oline)
				continue
			else:
				line = name + "=" + entries[name]
				self.debug("modified entry saved: %s" % line)
				entries_processed.append(name)
				fd.write(line + "\n")
				continue
		
		for entry, value in entries.items():
			if entry in entries_processed: continue
			line = entry + "=" + value
			self.debug("new entry saved: %s" %line)
			fd.write(line + "\n")
				
		fd.close()

	#######################################
	def end_combinations_keys(self):
		if self.hkserver.isAlive():
			self.hkserver.join(0.1)
			gobject.idle_add(self.end_combinations_keys)

	#######################################
	def filechooser_file(self, widget):
		command = widget.get_filename()
		self.widgets["command"].set_text(command)
		widget.destroy()

	#######################################
	def set_hotkey_text(self, text):
		self.widgets["hotkey"].set_text(text.title())
		
	#######################################
	def error_message_response(self, widget, retval):
		widget.destroy()
		self.apply_widgets("set_sensitive", addwindow = True, hotkey = False, change = True)
		self.set_hotkey_text(self.current_comb["hotkey"])

	######################################
	def error_message(self, text):
		message = gtk.MessageDialog(None, gtk.DIALOG_MODAL, \
			gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, text)
		message.connect("response", self.error_message_response)
		self.widgets["addwindow"].set_sensitive(False)
		message.show()
		return

	######################################
	def hotkey_end(self, keycodes = None, hotkey = None, disable = False):
		self.widgets["change"].set_label(self.old_change_label)
		self.apply_widgets("set_sensitive", hotkey=False, change= True, cancel=True, \
			name=True, command=True, browse=True)
		if self.current_comb["name"] and self.current_comb["command"]:
			self.apply_widgets("set_sensitive", accept=True, test=True)
		self.state = "normal"
		if disable:
			self.current_comb["hotkey"] = DISABLED_STRING
			self.set_hotkey_text(DISABLED_STRING)
			return
		if not keycodes: self.set_hotkey_text(self.current_comb["hotkey"]); return			
		mask = keycodes_to_mask(self.display, keycodes)
		keycode = keycodes[-1]
		newcomb = create_dict(mask = mask, keycode = keycode, hotkey = hotkey)

		for index, comb in enumerate(self.combinations):
			if index == self.active_row or not comb["hotkey"]: continue
			if comb["hotkey"] == newcomb["hotkey"]:
				text =  "This hotkey (%s) is already used by %s" %(newcomb["hotkey"], comb["name"])
				self.error_message(text)
				return
	
		self.set_hotkey_text(newcomb["hotkey"])
		self.current_comb["hotkey"] = newcomb["hotkey"]
		
	###################################
	def textbox_check(self, widget, key):
		self.current_comb[key] = widget.get_text()
		self.widgets["accept"].set_sensitive(bool(self.current_comb["name"] and self.current_comb["command"]))

	#######################################
	def open_add_window(self):
		self.widgets["mainwindow"].set_sensitive(False)
		self.widgets["name"].set_text(self.current_comb["name"])
		self.widgets["command"].set_text(self.current_comb["command"])
		self.set_hotkey_text(self.current_comb["hotkey"])
		self.widgets["addwindow"].set_transient_for(self.widgets["mainwindow"])
		self.widgets["addwindow"].set_position(gtk.WIN_POS_CENTER_ON_PARENT)
		self.widgets["addwindow"].show()

	###################################
	def create_pidfile(self):
		self.debug("creating pidfile: %s" %self.pidfile)
		try: fd = open(self.pidfile, "w")
		except: self.debug("cannot open file for writing: %s" %self.pidfile, ERROR, exit =1)
		pid = fd.write(str(os.getpid()) + "\n")
		fd.close()
		
	#######################################
	def check_active(self, stop = False):
		try: fd = open(self.pidfile)
		except IOError: self.debug("no pidfile found"); return
		try: pid = int(fd.readline().strip())
		except: self.debug("could not read pid from %s" %self.pidfile, ERROR); return
		
		# Check /proc info to check if it is really a xhotkeys process
		statfile = "/proc/%d/stat" % pid 
		try: fd = open(statfile)
		except IOError: self.debug("cannot read process status (%s)" %statfile); return
		if fd.read().split()[1] != "(%s)" %NAME: 
			self.debug("pidfile found but the running process is not %s" %NAME)
			try: os.unlink(self.pidfile)
			except: self.debug("error deleting pidfile" %self.pidfile, ERROR)
			return
		# Ok, no doubt it's a xhotkeys process, return PID
		return pid

	#######################################
	def stop_server(self):
		self.debug("stopping xhotkey server")
		self.hkserver.stop()
		self.hkserver.join()
		self.debug("hotkey server finished")

	#######################################
	def exit_conf(self):
		gtk.main_quit()
		if self.hkserver and self.hkserver.isAlive():
			self.stop_server()
		os._exit(0)

	#######################################		
	def get_combinations(self, row):
		if row < 0: return
		cr = self.combinations[row]
		return create_dict(name = cr["name"], command = cr["command"], hotkey = cr["hotkey"])

	#######################################		
	def apply_widgets(self, function, **args):
		for key, value in args.items():
			getattr(self.widgets[key], function)(value)

	#########################################
	def get_current_row(self):
		liststore, rows = self.listview.get_selection().get_selected_rows()
		if not rows or len(rows) < 1 or len(rows[0]) < 1: return -1
		return rows[0][0]
	
	#######################################
	def delete_entry(self, widget, answer, iter, row):
		widget.destroy()
		if answer == gtk.RESPONSE_YES:
			self.liststore.remove(iter)
			del self.combinations[row]
			self.active_row = -1
			self.apply_widgets("set_sensitive", delete = False, modify = False)
			self.save_and_reload()
		self.widgets["mainwindow"].set_sensitive(True)

	#######################################
	## WIDGETS ###############################
	#######################################

	#######################################
	def on_mainwindow_destroy(self, widget):
		self.exit_conf()

	#######################################
	def on_addwindow_delete_event(self, widget, event):
		self.on_cancel_clicked(widget)
		# Return True to keep the window (we only want to hide it, not destroy it!)
		return True

	#######################################
	def on_exit_clicked(self, widget):
		self.exit_conf()

	#######################################
	def on_accept_clicked(self, widget):
		for index, combination in enumerate(self.combinations):
			if self.new_window == "modify" and index == self.active_row: continue
			if combination["name"] == self.current_comb["name"]:
				self.error_message("This hotkey name (%s) is already being used" %combination["name"])
				return

		if self.new_window == "modify":
			self.combinations[self.active_row] = self.current_comb
			liststore, rows = self.listview.get_selection().get_selected_rows()
			
			listid, ite = self.listview.get_selection().get_selected()
			liststore.set_value(ite, 0, self.current_comb["name"])
			liststore.set_value(ite, 1, self.current_comb["command"])
			liststore.set_value(ite, 2, self.current_comb["hotkey"].title())
		else:
			self.combinations.append(self.current_comb)
			self.liststore.insert(len(self.combinations)-1, [self.current_comb["name"], self.current_comb["command"], self.current_comb["hotkey"].title()])
			self.listview.set_cursor(len(self.combinations)-1)

		self.widgets["addwindow"].hide()
		self.widgets["mainwindow"].set_sensitive(True)
		self.save_and_reload()

	#######################################
	def on_cancel_clicked(self, widget):
		self.widgets["addwindow"].hide()
		self.widgets["mainwindow"].set_sensitive(True)
		self.current_comb = self.get_combinations(self.active_row)

	#######################################
	def on_modify_clicked(self, widget):
		self.open_add_window()
		self.new_window = "modify"

	#######################################
	def on_test_clicked(self, widget):
		command = self.widgets["command"].get_text()
		run_command(command)

	#######################################
	def on_add_clicked(self, widget):
		hotkey = self.keycodes_to_string([])
		self.current_comb = create_dict(name = "", command = "", hotkey = hotkey, mask = 0, keycode = 0)
		self.apply_widgets("set_sensitive", accept=False, test=False)		
		self.widgets["name"].grab_focus()
		self.open_add_window()
		self.new_window = "add"
		
	#######################################
	def on_change_clicked(self, widget):
		if self.state == "recording": self.hotkey_end(); return
		self.keycodes_pressed = []
		self.old_change_label = self.widgets["change"].get_label()
		self.widgets["change"].set_label("Cancel")
		self.state = "recording"
		self.set_hotkey_text("BackSpace to disable")
		self.apply_widgets("set_sensitive", hotkey=True, accept =False, \
			cancel=False, name=False, test=False, command=False, browse=False)
		self.widgets["hotkey"].grab_focus()
		
	#########################################
	def on_name_key_press_event(self, widget, event):
		if event.string == "\r": 
			self.on_accept_clicked(self.widgets["accept"])

	#########################################
	def on_command_key_press_event(self, widget, event):		
		if event.string == "\r": 
			self.on_accept_clicked(self.widgets["accept"])

	#########################################
	def on_addwindow_key_press_event(self, widget, event):
		if event.keyval == gtk.keysyms.Escape and self.state != "recording": 
			self.on_cancel_clicked(widget)

	#########################################
	def on_hotkey_button_press_event(self, widget, event):
		if not keycodes_to_mask(self.display, self.keycodes_pressed): return
		string = self.keycodes_to_string(self.keycodes_pressed)
		self.hotkey_end(self.keycodes_pressed, string + "@%s%d@" %(MOUSE_BUTTONS.title(), event.button))
		widget.stop_emission("button_press_event")

	#########################################
	def on_hotkey_key_press_event(self, widget, event):
		keycode = event.hardware_keycode		
		if event.keyval == gtk.keysyms.Escape: self.hotkey_end(); return
		elif event.keyval == DISABLE_HOTKEY: self.hotkey_end(disable = True); return
		if keycode not in self.keycodes_pressed:	
			self.keycodes_pressed.append(keycode)
		string = self.keycodes_to_string(self.keycodes_pressed)
		self.set_hotkey_text(string)
		if not self.is_keycode_modifier(keycode):
			self.hotkey_end(self.keycodes_pressed, string)
		widget.stop_emission("key_press_event")

	#########################################
	def on_hotkey_key_release_event(self, widget, event):
		keycode = event.hardware_keycode
		while keycode in self.keycodes_pressed:
			self.keycodes_pressed.remove(keycode)
		string = self.keycodes_to_string(self.keycodes_pressed, "")
		self.set_hotkey_text(string)

	#######################################
	def on_delete_clicked(self, widget):
		model, iter = self.listview.get_selection().get_selected()
		message = gtk.MessageDialog(None, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR, \
			gtk.BUTTONS_YES_NO, "Are you sure you want to delete %s?" %self.current_comb["name"])
		message.connect("response", self.delete_entry, iter, self.active_row)
		self.widgets["mainwindow"].set_sensitive(False)
		message.show()
		return

	#######################################
	def on_browse_clicked(self, widget):
		self.widgets["filechooser"] = gtk.FileChooserDialog()
		self.widgets["filechooser"].connect("file-activated", self.filechooser_file)
		self.widgets["filechooser"].show()

	#########################################
	def on_name_changed(self, widget):
		self.textbox_check(widget, "name")
		
	#########################################
	def on_command_changed(self, widget):
		self.textbox_check(widget, "command")
		self.widgets["test"].set_sensitive(self.widgets["command"].get_text() != "")
			
	#########################################
	def on_hotkey_changed(self, widget):
		pass

	#######################################
	def on_hotkeytable_row_activated(self, widget, rows, column):
		self.open_add_window()
		self.new_window = "modify"

	#########################################
	def on_hotkeytable_cursor_changed(self, *args):
		row = self.get_current_row()
		if row < 0: 
			self.apply_widgets("set_sensitive", delete = False, modify = False)
			self.active_row = -1
		else:
			self.apply_widgets("set_sensitive", delete = True, modify = True)
			self.current_comb = self.get_combinations(row)
			self.active_row = row
	
	#####################################################
	#####################################################

	#############################################
	def configure_signals(self, signals):
		# Create signals dictionary: key = <signal_int> - value = <signal_name">
		for signum in signals:
			signal.signal(signum, self.signal_handler)
		self.signames = {}
		for key, value in inspect.getmembers(signal):
			if key.find("SIG") == 0 and key.find("SIG_") != 0: 
				self.signames[value] = key

	#######################################
	def save_and_reload(self):
		self.write_configuration()
		if self.hkserver: 
			self.read_configuration()
			self.hkserver.set_combination(self.combinations)
			return
		if not self.daemon_pid: return
		try: os.kill(self.daemon_pid, signal.SIGHUP)
		except IOError: self.debug("error sending SIGHUP to daemon", ERROR); return
		return self.daemon_pid

	#####################################################
	def open_glade(self, files):
		for file in files:
			self.debug("trying to open: %s" %file)
			try: os.path.exists(file)
			except: self.debug("not found: %s" %file); continue
			try: self.tree = gtk.glade.XML(file)
			except: continue
			else: break
		else: self.debug("fatal error: no glade files found", ERROR, exit = 1)

	#####################################################
	def load_widgets(self):
		self.widgets = {}
		for item, value in inspect.getmembers(self):
			if item.find("on_") != 0:
				continue
			widget, sig = item.split("_", 2)[1:]
			if not self.widgets.has_key(widget):
				self.widgets[widget] = self.tree.get_widget(widget)
			self.widgets[widget].connect(sig, getattr(self, item))

	#####################################################
	def create_table(self, widget_name, options):
		self.listview = self.tree.get_widget("hotkeytable")
		format = tuple([str]) * len(options)
		self.liststore = gtk.ListStore(*format)
		self.listview.set_model(self.liststore)			
		for index, (name, min_width, expand) in enumerate(options):
			renderer = gtk.CellRendererText()
			column = gtk.TreeViewColumn(name, renderer, text = index)
			column.set_clickable(True)
			column.set_resizable(True)
			column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
			column.set_expand(expand)
			column.set_min_width(min_width)
			self.listview.append_column(column)
		self.apply_widgets("set_sensitive", delete = False, modify = False)
		self.active_row = -1

	#####################################################
	def config(self):
		self.state = "config"
		self.daemon_pid = self.check_active()	
		glade_files = [NAME + ".glade", os.path.join("/usr/share/", NAME, NAME + ".glade")]
		self.open_glade(glade_files)
		self.load_widgets()
		table = ("Name", 100, False), ("Command", 250, True), ("Hotkey", 200, True)
		self.read_configuration(self.cfile)
		self.create_table("hotkeytable", table)		
		for comb in self.combinations:
			self.liststore.append([comb["name"], comb["command"], comb["hotkey"].title()])
		
		if not self.check_active():
			# There is no hotkey server running, run it temporally
			self.debug("starting X hotkey server")
			self.hkserver = XhotkeysServer(self.verbose_level)
			self.hkserver.set_combination(self.combinations)
			self.hkserver.start()
			gobject.idle_add(self.end_combinations_keys)
		
		self.configure_signals([signal.SIGTERM, signal.SIGINT])
		gtk.main()

	#########################################
	def server(self):
		self.state = "server"
		if not os.path.exists(self.cfile):
			self.debug("configuration file not found: %s" %self.cfile, ERROR)
			self.debug("run %s --config" %NAME, ERROR, 1)
		else: self.debug("using configuration file: %s" %self.cfile, INFO)
		self.hkserver = None
		pid = self.check_active()
		if pid: self.debug("oops, %s is already loaded (pid: %s)" %(NAME, pid), ERROR, exit = 1)

		self.debug("starting hotkey server")
		self.configure_signals([signal.SIGTERM, signal.SIGINT, signal.SIGHUP])
		self.hkserver = XhotkeysServer(self.verbose_level)
		self.read_configuration(self.cfile)
		self.hkserver.set_combination(self.combinations)
		self.hkserver.start()
		self.create_pidfile()
		while 1:
			try: self.hkserver.join(0.1)
			except: break
			
		self.delete_pidfile()
		os._exit(0)

#######################################################
def main():
	usage = """usage: xhotkeys [options] 

Hotkey server (keyboard and mouse shortcuts) to launch applications 
in the X-Window System. It provides a graphical configurator (see options)"""
	default_cfile = os.path.join(os.getenv("HOME"), ".%s" %NAME)
	default_pidfile = os.path.join(os.getenv("HOME"), ".%s.%s" %(NAME, os.getenv("USER")))
	parser = optparse.OptionParser(usage)
	parser.add_option('-v', '--verbose-level', default = 0, dest='verbose_level', action="count", help = 'Increase verbose level')
	parser.add_option('-c', '--config', dest='config', default = False, action='store_true', help = 'Opens a GTK+ interface configurator')
	parser.add_option('-f', '--configuration-file', dest='cfile', default = default_cfile, metavar='FILE', type='string', help = 'Alternative configuration file')
	parser.add_option('-p', '--pidfile', dest='pidfile', default = default_pidfile, metavar='FILE', type='string', help = 'Alternative pidfile')
	options, args = parser.parse_args()
	xhk = Xhotkeys(options.cfile, options.pidfile, options.verbose_level)
	if options.config: xhk.config()
	else: xhk.server()
	
#########
#############
#########################

if __name__ == '__main__':
	main()