#!/usr/bin/python

# Xhotkeys - X-Window hotkeys launcher
#
# 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, socket
import re, optparse, string, select
import inspect, threading
import SocketServer

# 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.6 $"
__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"

## 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):
	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):
	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():
	mask = 0
	for xmask, name in keysym_to_mask.values():
		mask = mask | xmask
	return mask
	
#######################################################
def debug(text, verbose_level, level, exit):
	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):
	if not command: return
	os.spawnvp(os.P_NOWAIT, "sh", ["sh", "-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 get_keylock_masks(self):
		"""Get Caps/Num/Scroll Lock masks. We need them because
		they 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 run_command(self, command):
		"""Run given command with a os.spawn"""
		if not command: self.debug("command not defined", INFO); return
		self.debug("executing: %s" %command, DEBUG)
		os.spawnvp(os.P_NOWAIT, "sh", ["sh", "-c", command])

	#######################################
	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)

	#######################################
	def set_combination(self, combinations):
		"""Set the current hotkey table"""
		if not combinations: return
		self.ungrab()
		self.combinations = combinations
		self.configure()

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

	##################################################
	def configure(self):
		"""Set hotkeys from the current hotkey table"""
		mode = Xlib.X.GrabModeAsync
		for comb in self.combinations:
			if not comb["keycode"]:	continue
			key, mod = comb["keycode"], comb["mask"]
			for mask in (0, self.locks_mask["caps"], self.locks_mask["num"], self.locks_mask["scroll"], \
				self.locks_mask["caps"] | self.locks_mask["num"], self.locks_mask["caps"] | self.locks_mask["scroll"], \
				self.locks_mask["caps"] | self.locks_mask["num"] | self.locks_mask["scroll"]):
				self.root.grab_key(key, mod | mask, 1, mode, mode)					

	##################################################
	def run(self):
		"""Main run thread. Read X events and look for configured hotkeys"""
		self.root.change_attributes(event_mask = Xlib.X.KeyPressMask | Xlib.X.KeyReleaseMask)
		self.configure()
		self.stop_flag = False
		
		while 1:
			if self.stop_flag:
				break
			while self.display.pending_events():
				event = self.display.next_event()
				keycode = event.detail
				mask = event.state & get_total_mask()
				if not event.type == Xlib.X.KeyPress: continue
				for comb in self.combinations:
					if comb["keycode"] == keycode and comb["mask"] == mask:
						self.debug("keycode=%d, mask=%d, command=%s" %(keycode, mask, comb["command"]))
						run_command(comb["command"])
			
			# Give the CPU a breath
			time.sleep(0.5)
		
		self.ungrab()


#######################
#######################
class XhotkeyHandler(SocketServer.StreamRequestHandler):
	##############################################
	def handle(self):
		allowed_keys = {"name": str, "command": str, "mask": int, "keycode": int}
		self.wfile.write(NAME + "\n")
		combinations = []
		while 1:
			data = self.rfile.readline().strip()
			if not data: break
			comb = {}
			for key, value in re.findall("(\S+)=([^\t]+)", data):
				if key not in allowed_keys: continue
				comb[key] = allowed_keys[key](value)
			if not comb: continue
			combinations.append(comb)		
		if not combinations: return
		self.server.last_combination = combinations

#######################
#######################
class Xhotkeys:
	####################################
	def __init__(self, cfile, socket, verbose_level = DEBUG):
		self.cfile = cfile
		self.verbose_level = verbose_level 
		self.socket = socket
		self.socket_created = False
		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)
			self.delete_socket()
			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):
		combinations = self.read_configuration(self.cfile)
		self.hkserver.set_combination(combinations)

	########################################
	def string_to_keycode(self, strcode):
		if len(strcode) > 2 and strcode[0] == "@" and strcode[-1] == "@":
			return int(strcode[1:-1])
		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 self.display.keysym_to_keycode(key[1])

	########################################
	def is_keycode_modifier(self, keycode):
		for keysym in keysym_to_mask:
			if keycode == self.display.keysym_to_keycode(keysym):
				return True
		return False

	########################################
	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, file, exit_on_error = True):
		self.combinations = []
		self.debug("opening configuration file: %s" %file, INFO)
		try: fd = open(file)
		except IOError: 
			if not exit_on_error: return []
			self.debug("configuration file not found: %s" %file)
			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
			name, options = [x.strip() for x in line.split("=")]
			keys, command = options.split(":")
			if keys.lower() == DISABLED_STRING:
				mask = keycode = 0
			else:
				modifiers = re.findall("<(\w+)>", keys)
				mask = string_to_mask(modifiers)
				try: key = re.findall("(@?\w+@?)$", keys)[0]
				except: self.debug("error parsing line %d: %s" %(numline, line), ERROR); continue
				keycode = self.string_to_keycode(key)
			comb = create_dict(name = name, mask = mask, 	keycode = keycode, command = command)
			comb["hotkey"] = keys
			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.combinations.append(comb)
			self.debug("adding key combination: %s = %s -> %s" %(comb["name"], comb["hotkey"], comb["command"]))
		self.debug("%d combinations installed" %len(self.combinations), INFO)
		fd.close()
		return self.combinations

	###################################
	def delete_socket(self):
		if not self.socket_created:
			return
		self.debug("deleting socket: %s" %self.socket)
		try: os.unlink(self.socket)
		except: self.debug("error deleting socket", 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("=")
			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.current_comb["mask"] = self.current_comb["keycode"] = 0
			self.set_hotkey_text(self.current_comb["hotkey"])
			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["keycode"]: continue
			if comb["keycode"] == newcomb["keycode"] and comb["mask"] == newcomb["mask"]:
				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["keycode"] = newcomb["keycode"]
		self.current_comb["mask"] = newcomb["mask"]
		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 open_socket(self):
		sd = socket.socket(socket.AF_UNIX)
		sd.connect(self.socket)
		line = sd.recv(256).strip()
		if line == "xhotkeys":
			return sd
		sd.close()
		self.debug("error, socket returned: %s" %line, ERROR)
		return
		

	#######################################
	def check_active(self):
		try: sd = self.open_socket()
		except: self.debug("cannot access socket: %s" %self.socket); return
		if not sd: return
		sd.close()
		return "active"

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

	#######################################
	def exit_conf(self, save = True):
		gtk.main_quit()
		if self.hkserver and self.hkserver.isAlive():
			self.stop_server()
		if save: self.write_configuration()
		os._exit(0)

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

	#######################################		
	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.widgets["mainwindow"].set_sensitive(True)

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

	#######################################
	def on_mainwindow_destroy(self, widget):
		self.exit_conf(save = True)

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

	#######################################
	def on_exit_clicked(self, widget):
		self.exit_conf(save = True)

	#######################################
	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()
			
			list, iter = self.listview.get_selection().get_selected()
			liststore.set_value(iter, 0, self.current_comb["name"])
			liststore.set_value(iter, 1, self.current_comb["command"])
			liststore.set_value(iter, 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.update_combination_config(self.combinations)

	#######################################
	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.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.update_combination_config([]) # could be removed...
		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):
		keycode = event.hardware_keycode
		
		if event.keyval == gtk.keysyms.Escape:
			if self.state == "recording": self.hotkey_end()
			else: self.on_cancel_clicked(widget)
			return
		elif self.state == "recording" and event.keyval == DISABLE_HOTKEY: 
			self.hotkey_end(disable = True)
		if self.state != "recording":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 keycodes_to_mask(self.display, self.keycodes_pressed) and not self.is_keycode_modifier(keycode):
			self.hotkey_end(self.keycodes_pressed, string)
		widget.stop_emission("key_press_event")

	#########################################
	def on_addwindow_key_release_event(self, widget, event):
		if self.state != "recording": return
		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
			return
		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 update_combination_config(self, combination):
		if self.hkserver:
			self.hkserver.set_combination(combination)
			return
		try: sd = self.open_socket()
		except: self.debug("cannot open socket: %s" %self.socket); return
		self.debug("sending configuration to socket: %s" %self.socket)
		for comb in combination:
			data = "name=%s\tcommand=%s\tmask=%s\tkeycode=%s\n" % \
				tuple([comb[x] for x in ("name", "command", "mask", "keycode")])
			sd.send(data)
		sd.close()
		return 1

	#####################################################
	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)			
		index = 0		
		for name, min_width in 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_min_width(min_width)
			self.listview.append_column(column)
			index += 1
		self.apply_widgets("set_sensitive", delete = False, modify = False)
		self.active_row = -1

	#####################################################
	def config(self):
		self.state = "config"
		glade_files = [NAME + ".glade", os.path.join("/usr/share/", NAME, NAME + ".glade")]
		self.open_glade(glade_files)
		if not os.path.exists(self.cfile): 
			self.debug("user configuration not found: %s" %self.cfile, INFO)
		self.load_widgets()
		table = ("Name", 100), ("Command", 200), ("Hotkey", 200)
		self.combinations = 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.update_combination_config(self.combinations):
			# There is no hotkey server running, we have to run one 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("no configuration files found (run %s --config)" %NAME, ERROR, 1)
		else:
			self.debug("using configuration file: %s" %self.cfile, INFO)
		self.hkserver = None
		if self.check_active():
			self.debug("oops, %s is already loaded (socket: %s)" %(NAME, self.socket), ERROR, exit = 1)
		try: os.unlink(self.socket)
		except: pass
		
		self.configure_signals((signal.SIGTERM, signal.SIGINT, signal.SIGHUP))
		self.debug("starting hotkey server")
		self.hkserver = XhotkeysServer(self.verbose_level)
		combinations = self.read_configuration(self.cfile)
		self.hkserver.set_combination(combinations)
		self.hkserver.start()
		
		self.debug("opening socket server: %s" %self.socket)
		socket_server = SocketServer.UnixStreamServer(self.socket, XhotkeyHandler)
		self.socket_created = True
		socket_server.last_combination = None
		sfileno = socket_server.fileno()
		while 1:
			self.hkserver.join(0.0)
			if not self.hkserver.isAlive(): break
			try: s = select.select([sfileno], [], [], 0.1)
			except: self.debug("interrupted"); continue
			if not s or not sfileno in s[0]: continue
			self.debug("waiting socket connections...")
			socket_server.handle_request()
			self.debug("socket connection finished")
			comb = socket_server.last_combination
			if not comb: continue		
			for c in comb:
				self.debug("socket: name=%s, keycode=%d, mask=%d, command=%s" \
					%(c["name"], c["keycode"], c["mask"], c["command"]))
			self.hkserver.set_combination(comb)
			socket_server.last_combination = None
			
		self.delete_socket()
		os._exit(0)

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

X hotkey server to run external applications (with graphical configurator)"""
	default_cfile = os.path.join(os.getenv("HOME"), ".%s" %NAME)
	default_socket = 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 = 'Verbose level')
	parser.add_option('-c', '--config', dest='config', default = False, action='store_true', help = 'Opens a graphic interface configuration')
	parser.add_option('-f', '--configuration-file', dest='cfile', default = default_cfile, metavar='FILE', type='string', help = 'Use FILE as configuration file')
	parser.add_option('-s', '--socket-file', dest='socket', default = default_socket, metavar='FILE', type='string', help = 'Use FILE as configuration file')
	options, args = parser.parse_args()
	xhk = Xhotkeys(options.cfile, options.socket, options.verbose_level)
	if options.config: xhk.config()
	else: xhk.server()
	
#########
#############
#########################

if __name__ == '__main__':
	main()