#!/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.8 $"
__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_STRING = "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=None):
	"""Give some debug messages.
	
	verbose_level -- verbose level for this message: DEBUG, INFO, ERROR
	level -- log-level: DEBUG, INFO, ERROR
	exit -- 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()
		self.stop_flag = False
		threading.Thread.__init__(self)

	########################################
	def string_to_keycode(self, strcode):
		if len(strcode) > 2 and strcode[0] == "@" and strcode[-1] == "@":
			return "key", int(strcode[1:-1])
		if strcode.find(MOUSE_STRING) == 0:
			return "button", int(strcode[-1])
		for key in inspect.getmembers(Xlib.XK):
			if 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"]
			type = mask = keycode = 0
			if hotkey.lower() != DISABLED_STRING:
				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
				try: type, keycode = self.string_to_keycode(key)
				except: self.debug("error parsing: %s" %key, ERROR); continue

			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 
		temporally dead. 
		
		So, here we directly make a call to GrabButton() 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"""
		while not self.stop_flag:
			while self.display.pending_events():
				event = self.display.next_event()
				mask = event.state & get_total_mask()
				#print event
				if event.type == Xlib.X.KeyPress:
					keycode = event.detail					
					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
					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 = dict([ (comb["name"], comb["hotkey"] + ":" + comb["command"]) for comb in self.combinations])
		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.oldcomb = self.current_comb.copy()
		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 not a %s daemon, so deleting it" %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()			
			model, 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)
		
		if self.oldcomb != self.current_comb:
			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)
		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 + MOUSE_STRING + str(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_status_key_release_event(self, widget, event):
		pass

	#########################################
	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_button_press_event(self, treeview, event):
		if event.button != 3: return
		menu = gtk.Menu()
		if not treeview.get_path_at_pos(int(event.x), int(event.y)): 
			items = ("gtk-add", self.on_add_clicked),
		else: items = ("gtk-edit", self.on_modify_clicked), ("gtk-delete", self.on_delete_clicked)
		for stock_id, connect in items:
			item = gtk.ImageMenuItem(stock_id=stock_id)
			menu.append(item)
			item.connect("activate", connect)
			item.show()
		menu.popup( None, None, None, 3, event.time)
	
	#########################################
	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:
			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", 300, True), ("Hotkey", 150, 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 self.check_active():
			self.widgets["status"].push(0, " Xhotkeys daemon is running (pid %d)" %self.daemon_pid)
		else:
			# There is no hotkey server running, run it temporally
			self.widgets["status"].push(0, " Xhotkeys daemon is not running")
			self.debug("starting Xhotkeys 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 Xhotkeys 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] 

Bind keys and mouse combinations to commands 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='Open 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()