#!/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, getpass
import re, subprocess, optparse, string
import inspect, commands, 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.5 $"
__author__ = "Arnau Sanchez <arnau@ehas.org>"
__depends__ = ['Xlib', 'PyGTK2', 'Glade', 'Python-2.4']
__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"

### 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():
			modmask = tmask[0]
			if keycode == display.keysym_to_keycode(keysym):
				mask = mask | modmask
	return mask

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

#######################################################
def verbose(text, lverbose, level, exit):
	if level <= lverbose:
		text = "%s: %s" %(LEVELS_STRING[level], text)
		sys.stderr.write(text + "\n")
		sys.stderr.flush()
	if exit != None: 
		sys.exit(exit)

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

	##################################################
	def __init__(self, display, conffile, lverbose = INFO):
		self.display = display
		self.lverbose = lverbose
		self.root = display.screen().root
		self.conffile = conffile
		self.running = False
		self.signals = {}
		self.combinations = None
		for var, value in inspect.getmembers(signal):
			if var.find("SIG") == 0 and var.find("SIG_") != 0: 
				self.signals[value] = var
		for signum in (signal.SIGHUP, signal.SIGUSR1, signal.SIGCHLD):
			signal.signal(signum, self.signal_handler)
		threading.Thread.__init__(self)

	####################################
	def signal_handler(self, signum, frame):
		"""Process received signals:
		
		SIGCHLD: Waits return value for a finished child
		SIGUSR1: Disable xhotkeys server
		SIGHUP: Reload configuration and enable xhotkeys server
		"""	
		signame = self.signals.get(signum, "unknown")
		self.verbose("signal received: %s" %signame)
		if signum == signal.SIGHUP:
			self.verbose("reload configuration", INFO)
			self.reload_conf()
			
		elif signum == signal.SIGCHLD:
			self.verbose("wait a child")
			try: pid, retval = os.wait()
			except: return
			self.verbose("child pid = %d - return value = %d" %(pid, retval))

		elif signum == signal.SIGUSR1:
			self.verbose("hotkeys disabled", INFO)
			self.display.ungrab_keyboard(Xlib.X.CurrentTime)
			self.display.flush()
			self.root.ungrab_key(Xlib.X.AnyKey, Xlib.X.AnyModifier)

	#######################################################
	def verbose(self, text, level = DEBUG, exit = None):
		verbose(text, self.lverbose, level, exit)

	###################################################
	def run_command(self, command):
		self.verbose("executing: %s" %command, DEBUG)
		try: p = subprocess.Popen(command, shell = True)
		except: self.verbose("command execution error: %s" %command, ERROR)

	########################################
	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 string_to_mask(self, 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 read_configuration(self, onerrorexit = True):
		self.combinations = []
		file = self.conffile
		self.verbose("opening configuration file: %s" %file, INFO)
		try: fd = open(file)
		except IOError: 
			if not onerrorexit: return []
			self.debug("configuration file not found: %s" %file)
			sys.exit(1)
			
		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 = map(string.strip, line.split("="))
			keys, command = options.split(":")
			if keys.lower() == DISABLED_STRING:
				mask = keycode = 0
			else:
				modifiers = re.findall("<(\w+)>", keys)
				mask = self.string_to_mask(modifiers)
				try: key = re.findall("(@?\w+@?)$", keys)[0]
				except: self.verbose("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
			self.combinations.append(comb)
			self.verbose("adding key combination: %s = %s -> %s" %(comb["name"], comb["hotkey"], comb["command"]))
		self.verbose("%d combinations installed" %len(self.combinations), INFO)
		fd.close()
		return self.combinations

	#######################################
	def set_combination(self, combinations):
		self.display.ungrab_keyboard(Xlib.X.CurrentTime)
		self.display.flush()
		self.root.ungrab_key(Xlib.X.AnyKey, Xlib.X.AnyModifier)
		if combinations:
			self.combinations = combinations
			self.configure = True

	##################################################
	def stop(self):
		self.loop = False

	##################################################
	def reload_conf(self):
		self.reload = True

	##################################################
	def run(self):
		if self.combinations == None:
			self.verbose("no hotkeys combinations configured", ERROR)
			os._exit(0)
		self.running = True
		kc = Xlib.XK
		CapsLockKeyCode, NumLockKeyCode, ScrollLockKeyCode = \
			map(self.display.keysym_to_keycode, [kc.XK_Caps_Lock, kc.XK_Num_Lock, kc.XK_Scroll_Lock])
		
		CapsLockMask = NumLockMask = ScrollLockMask = 0
		for index, mask in enumerate(self.display.get_modifier_mapping()):
			if mask[0] == CapsLockKeyCode:
				CapsLockMask = 1 << index
			elif mask[0] == NumLockKeyCode:
				NumLockMask = 1 << index
			elif mask[0] == ScrollLockKeyCode:
				ScrollLockMask = 1 << index
	
		self.loop = self.configure = True
		self.reload = False
		self.root.change_attributes(event_mask = Xlib.X.KeyPressMask)
		mode = Xlib.X.GrabModeAsync
		while self.loop:
			if self.configure:
				keycode_to_command = {}
				for comb in self.combinations:
					key, mod = comb["keycode"], comb["mask"]
					if not key: continue
					keycode_to_command[key] = comb["command"]
					self.root.grab_key(key, mod, 1, mode, mode)						
					self.root.grab_key(key, mod | CapsLockMask, 1, mode, mode)
					self.root.grab_key(key, mod | NumLockMask, 1, mode, mode)
					self.root.grab_key(key, mod | ScrollLockMask, 1, mode, mode)
					self.root.grab_key(key, mod | CapsLockMask | NumLockMask, 1, mode, mode)
					self.root.grab_key(key, mod | CapsLockMask | ScrollLockMask, 1, mode, mode)
					self.root.grab_key(key, mod | CapsLockMask | NumLockMask | ScrollLockMask, 1, mode, mode)
				self.configure = False

			if self.reload:
				self.verbose("reloading configuration", INFO)
				self.read_configuration()
				self.configure = True
				self.reload = False
				continue

			while self.running and self.display.pending_events():
				event = self.display.next_event()
				keycode = event.detail
				if event.type == Xlib.X.KeyPress:
					self.verbose("key pressed: %d" %keycode)
					if keycode_to_command.has_key(keycode):
						self.run_command(keycode_to_command[keycode])
		
			time.sleep(0.1)


#######################
#######################
class Xhotkeys:
	####################################
	def signal_handler(self, signum, frame):
		"""Process SIGTERM and SIGINT and make some clean"""
		
		signame = self.signals.get(signum, "unknown")
		self.verbose("signal received: %s" %signame)
		
		if signum == signal.SIGTERM or signum == signal.SIGINT:
			self.verbose("%s stopped (pid %d)" %(NAME, os.getpid()), INFO)
			if self.state == "config":
				self.exit_conf(save = False)
			self.root.ungrab_key(Xlib.X.AnyKey, Xlib.X.AnyModifier)
			self.delete_pidfile()
			if self.server: 
				self.server.stop()
			os._exit(0)

	#######################################################
	def verbose(self, text, level = DEBUG, exit = None):
		verbose(text, self.lverbose, level, exit)

	#######################################################
	def parse_args(self):
		usage = """usage: %s [options] 
	
X-Shortcuts to run external applications with graphical configurator""" %NAME
	
		parser = optparse.OptionParser(usage)
		
		parser.add_option('-v', '--verbose-level', 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='conffile', default = "", metavar='FILE', type='string', help = 'Use FILE as configuration file')
		self.options, args = parser.parse_args()
		if not self.options.verbose_level:
			self.options.verbose_level = 0

	########################################
	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 = ""
		for keycode in keycodes:
			if keycode in keycodes_to_modifier:
				strmod += "<" + keycodes_to_modifier[keycode] + ">"
				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 delete_pidfile(self):
		self.verbose("deleting pidfile: %s" %self.pidfile)
		try: os.unlink(self.pidfile)
		except: self.verbose("error deleting pidfile", ERROR)

	###################################
	def create_pidfile(self):
		self.verbose("creating pidfile: %s" %self.pidfile)
		try: fd = open(self.pidfile, "w")
		except: self.verbose("cannot open file for writing: %s" %self.pidfile, ERROR); return
		pid = fd.write(str(os.getpid()) + "\n")
		fd.close()

	#######################################
	def write_configuration(self):
		self.verbose("opening configuration file to get old configuration: %s" %self.conffile, INFO)
		try: fd = open(self.conffile, "r")
		except IOError: original = []
		original = fd.readlines()	
		fd.close()
		
		self.verbose("opening configuration file for writing: %s" %self.conffile, INFO)
		try: fd = open(self.conffile, "w")
		except IOError:
			self.verbose("configuration file couldn't be opened for writing: %s" %self.conffile, 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.verbose("old entry removed: %s" %oline.strip())
				continue
			if value == entries[name]:
				self.verbose("keep old entry: %s" %oline.strip())
				entries_processed.append(name)
				fd.write(oline)
				continue
			else:
				line = name + "=" + entries[name]
				self.verbose("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.verbose("new entry saved: %s" %line)
			fd.write(line + "\n")
				
		fd.close()

	#######################################
	def end_combinations_keys(self):
		if self.server.isAlive():
			self.server.join(0.01)
			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 hotkeyused_closed(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 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.widgets["accept"].set_sensitive(True)
		self.state = "normal"
		self.server.set_combination(self.combinations)

		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: continue
			if not comb["keycode"]: continue
			if (comb["keycode"], comb["mask"]) == (newcomb["keycode"], newcomb["mask"]):
				message = gtk.MessageDialog(None, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR, \
					gtk.BUTTONS_CLOSE, "This hotkey combination (%s) is already used by %s" %(newcomb["hotkey"], comb["name"]))
				message.connect("response", self.hotkeyused_closed)
				self.widgets["addwindow"].set_sensitive(False)
				message.show()
				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()
		if not self.current_comb["name"] or not self.current_comb["command"]:
			self.widgets["accept"].set_sensitive(False)
		else:
			self.widgets["accept"].set_sensitive(True)

	#######################################
	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 check_active(self, stop = False):
		try: fd = open(self.pidfile)
		except IOError: self.verbose("no pidfile found, we can continue"); return
		try: pid = int(fd.readline().strip())
		except: self.verbose("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.verbose("cannot read process status (%s)" %statfile); return
		if fd.read().split()[1] != "(%s)" %NAME: 
			self.verbose("pidfile found but the running process is not %s" %NAME)
			try: os.unlink(self.pidfile)
			except: self.verbose("error deleting pidfile" %self.pidfile, ERROR)
			return
		# Ok, no doubt it's a xhotkey process: now stop or disable it.
		if not stop: return pid
		self.verbose("%s already loaded (pid %s): disabling temporally" %(NAME, pid))
		os.kill(pid, signal.SIGUSR1)
		self.reload_pid = pid
		return pid

	#######################################
	def exit_conf(self, save = True):
		gtk.main_quit()
		if self.server and self.server.isAlive():
			self.server.stop()
			self.verbose("waiting hotkey server to finish")
			self.server.join()
		self.display.ungrab_keyboard(Xlib.X.CurrentTime)
		self.display.flush()
		self.display.screen().root.ungrab_key(Xlib.X.AnyKey, Xlib.X.AnyModifier)
		if save: 
			self.write_configuration()
		if self.reload_pid:
			self.verbose("enabling previously stopped %s (pid %d)" %(NAME, self.reload_pid))
			try: os.kill(self.reload_pid, signal.SIGHUP)
			except: self.verbose("process not found: %d" %self.reload_pid)
		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_destroy(self, widget):
		self.on_cancel_clicked(widget)

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

	#######################################
	def on_accept_clicked(self, widget):
		if self.new_window == "modify":
			self.combinations[self.active_row] = self.current_comb
			liststore, rows = self.listview.get_selection().get_selected_rows()
			model, iter = self.listview.get_selection().get_selected()
			for num, item in enumerate(("name", "command", "Uhotkey")):
				value = self.current_comb[item.replace("U", "")]
				if item[0] == "U": value = value.title()
				liststore.set_value(iter, num, value)
		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.server:
			self.server.set_combination(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_add_clicked(self, widget):
		hotkey = self.keycodes_to_string([])
		self.current_comb = create_dict(name = "", command = "", hotkey = hotkey, mask = 0, keycode = 0)
		self.widgets["accept"].set_sensitive(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.server.set_combination(False)
		self.set_hotkey_text("press a combination")
		self.apply_widgets("set_sensitive", hotkey = True, accept = False, \
			cancel = False, name = False, command = False, browse = False)

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


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

	#########################################
	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):
		for signum in (signal.SIGTERM, signal.SIGINT):
			signal.signal(signum, self.signal_handler)

	#####################################################
	def config(self):
		self.state = "config"
		glade_files = (NAME + ".glade", os.path.join("/usr/share/", NAME, NAME + ".glade"))
		for file in glade_files:
			self.verbose("trying to open: %s" %file)
			try: os.stat(file)
			except: self.verbose("not found: %s" %file); continue
			try: self.tree = gtk.glade.XML(file)
			except: continue
			else: break
		else: self.verbose("fatal error: no glade files found", ERROR, exit = 1)

		file = self.options.conffile
		if not file:
			file = os.path.join(os.getenv("HOME"), "." + NAME)
			if not os.path.exists(file): 
				self.verbose("user configuration not found: %s" %file, INFO)
			
		self.conffile = file

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

		columns = ("Name", {"min_width": 100}), ("Command", {"min_width": 200}), \
			("Hotkey", {"min_width": 200, "max_width": 200})
		self.listview = self.tree.get_widget("hotkeytable")
		format = tuple([str]) * len(columns)
		self.liststore = gtk.ListStore(*format)
		self.listview.set_model(self.liststore)			
		index = 0
		
		for name, options in columns:
			renderer = gtk.CellRendererText()
			column = gtk.TreeViewColumn(name, renderer, text = index)
			column.set_clickable(True)
			column.set_resizable(True)
			if options.has_key("min_width"):
				column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
				column.set_min_width(options.get("min_width"))
			if options.has_key("max_width"):
				column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
				column.set_max_width(options.get("max_width"))
			self.listview.append_column(column)
			index += 1
	
		self.apply_widgets("set_sensitive", delete = False, modify = False)
		self.active_row = -1
		pid = self.check_active(stop = True)
		if not pid:
			self.create_pidfile()
		self.server = XhotkeysServer(self.display, self.conffile, self.lverbose)
		self.combinations = self.server.read_configuration(onerrorexit = False)
		for comb in self.combinations:
			self.liststore.append([comb["name"], comb["command"], comb["hotkey"].title()])
		self.configure_signals()
		self.server.start()
		gobject.idle_add(self.end_combinations_keys)
		self.current_comb = {}
		gtk.main()


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

		self.reload_pid = self.server = None
		self.pidfile = os.path.join(os.getenv("HOME"), ".%s.%s" %(NAME, os.getenv("USER")))
		self.state = "normal"
		self.display = Xlib.display.Display()
		self.root = self.display.screen().root
		self.parse_args()
		self.lverbose = self.options.verbose_level
		if self.options.config:
			self.config()
			
		self.state = "server"
		pid = self.check_active()
		if pid: self.verbose("%s already active (pid %d): exiting" %(NAME, pid), ERROR, exit=1)

		file = self.options.conffile
		if not file:
			files = [os.path.join(os.getenv("HOME"), "." + NAME), os.path.join("/etc", NAME + ".conf")]
			for file in files:
				self.verbose("looking for: %s" %file, INFO)
				if os.path.exists(file): break
				self.verbose("configuration not found: %s" %file, ERROR)
			else:
				self.verbose("fatal error: no configuration files found (run %s --config)" %NAME, ERROR, exit = True)
		self.verbose("using configuration file: %s" %file, INFO)
		self.conffile = file
		self.create_pidfile()

		self.configure_signals()
		self.server = XhotkeysServer(self.display, self.conffile, self.lverbose)
		
		self.server.read_configuration()
		self.server.start()
		
		while 1:
			self.server.join(0.1)
			if not self.server.isAlive(): break
		self.delete_pidfile()
		os._exit(0)
	

#########
#############
#########################

if __name__ == '__main__':
	xhk = Xhotkeys()
	xhk.run()