#!/usr/bin/env python

# $id$

from Tkinter import *
import tkMessageBox
try:
	import tkFont
	HAVE_FONTS = 1
except ImportError:
	HAVE_FONTS = 0

import tkColorChooser
from types import *
import re, string, time, cPickle, os, sys, signal, getopt
import traceback

import Pmw
import buttonbar	# Peter's buttonbar
import search, thumbcache		# dialogs
import config, connection	# Connections to the gale server
from HistoryCombo import *
from pygale import *

# Default fonts
if sys.platform == 'win32':
	FONTS = { 
		'BOLD_FONT': 'Helvetica 10 bold',
		'HEADER_FONT': 'Helvetica 10',
		'SMALL_FONT': 'Helvetica 6',
		'HR_FONT': 'SmallFont 2',
		'HR_SPACING_FONT': 'SmallFont 2',
		'HR_BACKGROUND': '',
		'HR_RELIEF': 'raised',
		'QUOTE_FONT': 'Helvetica 8 italic',
		'TEXT_FONT': 'Helvetica 8'
	}
else:
	FONTS = {
		'BOLD_FONT': 'Helvetica -14 bold',
		'HEADER_FONT': 'Helvetica -12',
		'SMALL_FONT': 'Helvetica -8',
		'HR_FONT': 'fixed -3',
		'HR_SPACING_FONT': 'fixed -4',
		'HR_BACKGROUND': '',
		'HR_RELIEF': 'sunken',
		'QUOTE_FONT': '-*-courier-medium-o-*-*-12-*-*-*-*-*-*-*',
		'TEXT_FONT': 'Courier -12'
	}

# Number of lines to save in log
LOG_MESG_BUFFER = 100

# Default presence strings
DEFAULT_PRESENCES = ['in/perhaps', 'out/somewhere/else']

# Enable or disable debugging features
DEBUG = 0

# Timeout (in ms) before the status bar text is cleared
STATUS_TIMEOUT = 60 * 1000		# 1 minute

# Actually send this puff
# Returns 0 on success, something else otherwise
class PuffSender:
	def __init__(self, galewin, location, keyw, pufftext,
		callback = None):
		self.galewin = galewin
		self.puff = pygale.Puff()
		self.location = location
		self.keyw = keyw
		self.pufftext = pufftext
		self.callback = callback
		self.locs = []

		self.recp_ids = []
		self.called = 0	# only here until race conditions are debugged

	def send(self):
		"Main interface for PuffSender"
		if DEBUG > 1: print 'UI: Puffsender.send'
		# Filter out null locations
		locs = filter(None, string.split(self.location, None))
		# Expand out aliases and default domains
		locs = map(pygale.expand_aliases, locs)
		self.locs = []
		for loc in locs:
			if loc not in self.locs:
				self.locs.append(loc)

		# Collect locations
		pygale.lookup_all_locations(self.locs, self.got_recipients)

	def got_recipients(self, loc_key_list):
		# TODO
		if DEBUG > 1:
			print 'UI: GOT RECIPIENTS', loc_key_list
		if self.called:
			print 'ERROR: already got this callback!'
			traceback.print_stack()
			return
		self.called = 1

		assert len(loc_key_list) == len(self.locs)
		new_locs = []
		badlocs = []
		self.recp_ids = []
		for i in range(len(self.locs)):
			(new_loc, keylist) = loc_key_list[i]
			if keylist is None:
				# Do not know how to send to this location
				badlocs.append(self.locs[i])
			else:
				self.recp_ids = self.recp_ids + keylist
				new_locs.append(new_loc)
		self.locs = new_locs
		if '' in self.recp_ids:
			self.recp_ids = []

		if badlocs:
			# Invalid location
			if len(badlocs) > 1:
				msg = 'Cannot find locations: ' +\
					string.join(badlocs, ', ')
			else:
				msg = 'Cannot find location ' + badlocs[0]
			m = tkMessageBox.showerror(
				title='Error: unknown location',
				parent=self.galewin,
				message=msg)
			if self.callback:
				self.callback(1)
			return 1

		self._send_puff()

	def _send_puff(self):
		self.puff.set_time('id/time', int(time.time()))
		self.puff.set_text('id/instance', pygale.getinstance())
		if Config.SENDER:
			self.puff.set_text('message/sender', Config.SENDER)
		if Config.THUMBNAIL_URL:
			self.puff.set_text('message/image', Config.THUMBNAIL_URL)
		self.puff.set_text('id/class', FUGU_VERSION_LONG)
		self.pufftext = re.sub('\n', '\r\n', self.pufftext)
		self.puff.set_text('message/body', self.pufftext)
		if DEBUG > 1:
			print 'UI: Setting puff location to:',\
				string.join(self.locs, ' ')
		self.puff.set_loc(string.join(self.locs, ' '))
		if type(self.keyw) is ListType:
			for word in self.keyw:
				self.puff.set_text('message.keyword', word)
		else:
			# string coercion
			self.puff.set_text('message.keyword', '%s' % self.keyw)

		# Ask for a return receipt only if the message is encrypted and
		# our id is not in the list of encryption recipients
		private = self.recp_ids and\
			not authcache.have_a_privkey(self.recp_ids)
		if private:
			self.puff.set_text('question.receipt',
				Config.SIGNER)
			self.puff.set_text('question/receipt',
				pygale.id_category(Config.SIGNER, 'user', 'notice'))

		# Sign
		if Config.SIGNER:
			out = self.puff.sign_message(Config.SIGNER)
			if out is None:
				# Uh oh, can't sign message
				if self.callback:
					self.callback(1)
				return 1
		else:
			out = self.puff

		# Encrypt
		# TODO dangermouse
		if self.recp_ids:
			if DEBUG > 1:
				print 'UI: Encrypting message to', self.recp_ids
			# Encrypt if being sent to individuals
			out.encrypt_message(self.recp_ids, lambda puff, s=self:
				s.finish_send(puff))
			# This is just here for the shortcircuiting puff logging
			self.puff.set_recipients(self.recp_ids)
		else:
			self.finish_send(out)
	
	def finish_send(self, out):
		try:
			if DEBUG > 1:
				print 'UI: transmitting puff to', out.get_loc()
			self.galewin._galeconn.transmit_puff(out)
		except pygale.PyGaleErr, e:
			# server connection died
			self.galewin.gale_error(e)
			# Assume retry is already in progress
			return
		# Log copy of puffs as requested
		for c in Config.SUBLIST:
			log = c['logsent']
			# TODO dangermouse
			private = self.recp_ids and\
				not authcache.have_a_privkey(self.recp_ids)
			if (log >= 1 and private) or log == 2:
				c.addpuff(self.puff, new=0)
		if self.callback:
			self.callback(0)
		return 0

def waitforzombie(*args):
	try:
		os.wait()
	except:
		pass

# Transition from old-style to new-style config class
class ConfigClass: pass
class Connection:
	def __getitem__(self, key):
		uname = '_' + key
		return self.__dict__[uname]

# ----------------------------------------------------------------------
# Handler for SIGUSR1
# Dump the contents of our screens into ~/.gale/fugu-<screen#>.dump
# For rescue/debug purposes only
def handle_usr1(*args):
	print 'Dumping contents of screens to', FUGUDIR
	for i in range(len(Config.SUBLIST)):
		f = open('%s/fugu-%i.dump' % (FUGUDIR, i), 'w')
		text = string.replace(
			Config.SUBLIST[i].widget().get(1.0, 'end'),
			'\n\n\n', '\n' + ('-' * 79))
		f.write(text)
		f.close()

class GaleWin(Frame):
	def __init__(self, master):
		Frame.__init__(self, master)
		self.master.title('Fugu')
		self.master.withdraw()
		self.master.protocol('WM_DELETE_WINDOW', self.quit)
		self.pack(expand=1, fill=BOTH)

		self._tagid = 0			# sequence number for tags
		self._curscreen = None		# currently selected screen
		self._buttonlist = []		# List of screen buttons
		self._clipboard = None			# currently selected url
		self._thumbs = {}			# thumbnail pics
		self._thumb_cbs = {}		# callbacks on thumbnails
		self._status_clear = None	# handle to status bar clear func

		# Load global configuration object
		# Be sure to do this before creating widgets, below
		#self.load_config()
		self._get_resource_fonts()

		self._create_widgets()
		self._bind_hotkeys()

		# Set up an error handler
		pygale.set_error_handler(self.gale_error)
		pygale.set_update_handler(self.gale_error)
		# Set up SIGUSR1 handler
		if sys.platform != 'win32':
			signal.signal(signal.SIGUSR1, handle_usr1)
			signal.signal(signal.SIGCHLD, waitforzombie)
			signal.signal(signal.SIGPIPE, self.quit)
		signal.signal(signal.SIGTERM, self.quit)
		signal.signal(signal.SIGINT, self.quit)

		# Set up writeable gale connection and presence state
		self.make_send_conn(reconnect=0)
		
		# Create screen popup
		self.screenpop = ScreenPop(self)
		self.screenpop.withdraw()

		# Create log window
		self.logwin = LogWin(self)
		self.logwin.withdraw()

		# Set us up with the public category in focus
		self._puffwidget.set_loc_focus()
		self.master.deiconify()

		# Set up icon
		for dir in sys.path:
			if dir == "": dir = '.'
			try:
				self.master.iconbitmap("@%s/fugu-icon.xbm" % dir)
				break
			except TclError:
				continue

		# Persist pufflog every 5 minutes
		if Config.PERSIST_INTERVAL:
			self.after(Config.PERSIST_INTERVAL*1000, self.persist_pufflog)

	def _get_resource_fonts(self):
		resources = [
			('boldFont', 'BOLD_FONT'),
			('headerFont', 'HEADER_FONT'),
			('smallFont', 'SMALL_FONT'),
			('hrFont', 'HR_FONT'),
			('hrSpacingFont', 'HR_SPACING_FONT'),
			('hrBackground', 'HR_BACKGROUND'),
			('hrRelief', 'HR_RELIEF'),
			('quoteFont', 'QUOTE_FONT'),
			('textFont', 'TEXT_FONT')]
		for (xres, var) in resources:
			val = self.master.option_get(xres, xres)
			if val:
				try:
					exec 'FONTS[%s] = %s' % (`var`, `val`)
				except Exception, e:
					print 'Error setting value for %s: %s' % (var, e)

	# Persist the pufflog
	def persist_pufflog(self, *args):
		try:
			Config.persist_pufflog(FUGUDIR)
		except:
			pass
		if Config.PERSIST_INTERVAL:
			self.after(Config.PERSIST_INTERVAL*1000, self.persist_pufflog)
	
	# Clear out the presence box
	def clear_presences(self):
		Config.PRESENCE_STRINGS = ['in/perhaps']
		Config.DEFAULT_PRESENCE = Config.PRESENCE_STRINGS[0]
		self.presence_box.component('scrolledlist').setlist(
			Config.PRESENCE_STRINGS)
		self.presence_box.selectitem(0)
		self.change_presence()

	# Invoke and focus the presence box
	def drop_presence(self, *args):
		self.presence_box.invoke()
		self.presence_box.component('listbox').focus_set()

	def change_presence(self, in_presence = None):
		if in_presence is None:
			in_presence = self.presence_box.get(first=None)
		if Config.BROADCAST_PRESENCE:
			while 1:
				try:
					pygale.notify_in(in_presence,
						self._galeconn, userid=Config.SIGNER,
						fullname=Config.SENDER,
						version=FUGU_VERSION_LONG,
						instance=pygale.getinstance())
					pygale.notify_out('out/quit',
						self._galeconn,
						userid=Config.SIGNER,
						fullname=Config.SENDER,
						version=FUGU_VERSION_LONG,
						instance=pygale.getinstance())
				except pygale.PyGaleErr:
					self.make_send_conn()
				else:
					break
	
	def make_send_conn(self, reconnect=1):
		self._galeconn = pygale.GaleClient()
		self._galeconn.set_onconnect(lambda h, r=reconnect, s=self:
			s.on_sendconn(h, r))
		self._galeconn.set_ondisconnect(self.on_disconnect)
		self._galeconn.connect(self.make_send_conn2)
	
	def make_send_conn2(self, host):
		if not host:
			self.gale_error('Unable to connect to a gale server')
			return
		self.gale_error('Connected to server at %s' % host)

	def on_sendconn(self, host, reconnect):
		# Set up AKD server, ignore host
		self.create_akd_server(reconnect)

	def create_akd_server(self, reconnect):
		cb = lambda b, g, s=self, r=reconnect: s.on_sendconn2(b, g, r)
		akd_cat = '_gale.query.' + Config.SIGNER
		self._galeconn.sub_to([akd_cat], cb)

	def on_sendconn2(self, badlocs, goodlocs, reconnect):
		if badlocs:
			# error
			self.gale_error('Could not subscribe to %s' %
				string.join(retval, ', '))
		self._galeconn.set_puff_callback(lambda p, s=self:
			s.akd_request(p))

		# Transmit login notice
		if reconnect:
			self.change_presence('in/reconnected')
		else:
			self.change_presence()

	def on_disconnect(self):
		self.gale_error('Disconnected from server')
	
	def gale_error(self, msg):
		date = time.strftime('%m-%d %H:%M:%S ',
			time.localtime(time.time()))
		msg = date + string.strip(msg)
		self.set_status(msg)
		# TODO: 1) fix hasattr ickiness
		if hasattr(self, 'logwin'):
			self.logwin.add_message(msg)

	def _create_widgets(self):
		# Menus
		menubar = Frame(self, relief='flat', borderwidth=0)
		menubar.pack(side=TOP, fill=X, expand=0)
		self.menubar = menubar

		filemenu = Menubutton(menubar, text="File", underline=0)
		filemenu.pack(side=LEFT)
		filemenu.menu = Menu(filemenu, tearoff=0)

		filemenu.menu.add_command(label='Reconnect to server',
			underline=0, command=self.reconnect_all)
		filemenu.menu.add_command(label='Reset presence string menu',
			underline=0, command=self.clear_presences)
			
		filemenu.menu.add_separator()
		filemenu.menu.add_command(label='Preferences',
			underline=0, command=self.config_pop)
		filemenu.menu.add_command(label='Exit',
			underline=1, command=self.quit)
		filemenu['menu'] = filemenu.menu

		puffmenu = Menubutton(menubar, text="Puff", underline=0)
		puffmenu.pack(side=LEFT)
		puffmenu.menu = Menu(puffmenu, tearoff=0)
		puffmenu.menu.add_command(label='Clear puff (A-c)',
			underline=0, command=self.clearpuff)
		puffmenu.menu.add_command(label='Send puff (C-Enter)',
			underline=0, command=self.sendbutton)
		puffmenu.menu.add_command(label='Detach puff (A-d)',
			underline=0, command=self.detach)
		puffmenu.menu.add_separator()
		puffmenu.menu.add_command(label='Reply to puff (C-r)',
			underline=0, command=self.reply)
		puffmenu.menu.add_command(label='Reply to sender (C-R)',
			command=self.reply_to_sender)
		puffmenu.menu.add_separator()
		puffmenu.menu.add_command(label='Previous in thread (C-p)',
			underline=0, command=self.prev_thread)
		puffmenu.menu.add_command(label='Next in thread (C-n)',
			underline=0, command=self.next_thread)
		puffmenu.menu.add_command(label='Author search (A-a)',
			underline=0, command=self.author_search)
		# For debugging
		if DEBUG:
			puffmenu.menu.add_separator()
			puffmenu.menu.add_command(label='Show marks',
				command=self.show_marks)
			puffmenu.menu.add_command(label='Show tags',
				command=self.show_tags)
			puffmenu.menu.add_command(label='Show pufflist length',
				command=self.show_pufflist)

		puffmenu['menu'] = puffmenu.menu

		self.screenmenu = Menubutton(menubar, text="Screens", underline=0)
		self.screenmenu.pack(side=LEFT)
		self.screenmenu.menu = Menu(self.screenmenu, tearoff=0)
		self.screenmenu.menu.add_command(label='Search (C-s)', underline=0,
			command=self.search_pop)
		self.screenmenu.menu.add_command(label='Configure',
			underline=0, command=self.screen_pop)
		self.screenmenu.menu.add_command(label='Clear current',
			underline=1, command=self.clear_curscreen)
		self.screenmenu.menu.add_separator()
		# Populate the screen menu
		self.regen_screenmenu()

		self.screenmenu['menu'] = self.screenmenu.menu

		if DEBUG:
			debugmenu = Menubutton(menubar, text='Debug')
			debugmenu.pack(side=LEFT)
			debugmenu.menu = Menu(debugmenu, tearoff=0)
			debugmenu.menu.add_command(label='GC', underline=0,
				command=self.garbage_collect)
			self.var_d_asyncurl = BooleanVar()
			self.var_d_asyncurl.set(asyncurl.DEBUG > 0)
			self.var_d_authcache = BooleanVar()
			self.var_d_authcache.set(authcache.DEBUG > 0)
			self.var_d_engine = BooleanVar()
			self.var_d_engine.set(engine.DEBUG > 0)
			self.var_d_pygale = BooleanVar()
			self.var_d_pygale.set(pygale.DEBUG > 0)
			self.var_d_sign = BooleanVar()
			self.var_d_sign.set(sign.DEBUG > 0)
			self.var_d_thumbcache = BooleanVar()
			self.var_d_thumbcache.set(thumbcache.DEBUG > 0)
			debugmenu.menu.add_checkbutton(label='Asyncurl', underline=0,
				variable=self.var_d_asyncurl,
				command=lambda s=self, v=self.var_d_asyncurl,
				n='asyncurl.DEBUG':
				s.toggle_debug_var(v, n))
			debugmenu.menu.add_checkbutton(label='Authcache', underline=0,
				variable=self.var_d_authcache,
				command=lambda s=self, v=self.var_d_authcache,
				n='authcache.DEBUG':
				s.toggle_debug_var(v, n))
			debugmenu.menu.add_checkbutton(label='Engine', underline=0,
				variable=self.var_d_engine,
				command=lambda s=self, v=self.var_d_engine,
				n='engine.DEBUG':
				s.toggle_debug_var(v, n))
			debugmenu.menu.add_checkbutton(label='Pygale', underline=0,
				variable=self.var_d_pygale,
				command=lambda s=self, v=self.var_d_pygale,
				n='pygale.DEBUG':
				s.toggle_debug_var(v, n))
			debugmenu.menu.add_checkbutton(label='Sign', underline=0,
				variable=self.var_d_sign,
				command=lambda s=self, v=self.var_d_sign,
				n='sign.DEBUG':
				s.toggle_debug_var(v, n))
			debugmenu.menu.add_checkbutton(label='Thumbcache', underline=0,
				variable=self.var_d_thumbcache,
				command=lambda s=self, v=self.var_d_thumbcache,
				n='thumbcache.DEBUG':
				s.toggle_debug_var(v, n))
			debugmenu.menu.add_separator()
			self.var_d_level = IntVar()
			self.var_d_level.set(DEBUG)
			debugmenu.menu.add_radiobutton(label='Debug level 1',
				underline=0, variable=self.var_d_level, value=1,
				command=self.set_debug_level)
			debugmenu.menu.add_radiobutton(label='Debug level 2',
				underline=0, variable=self.var_d_level, value=2,
				command=self.set_debug_level)
			debugmenu.menu.add_radiobutton(label='Debug level 3',
				underline=0, variable=self.var_d_level, value=3,
				command=self.set_debug_level)
			debugmenu['menu'] = debugmenu.menu

		helpmenu = Menubutton(menubar, text='Help', underline=0)
		helpmenu.pack(side=RIGHT)
		helpmenu.menu = Menu(helpmenu, tearoff=0)
		helpmenu.menu.add_command(label='Error messages',
			underline=0, command=self.popup_logwin)
		helpmenu.menu.add_command(label='About',
			underline=0, command=self.about)
		helpmenu['menu'] = helpmenu.menu

		# Button bar frame
		self.buttonframe = Frame(self)
		self.buttonframe.pack(side=TOP, expand=0, fill=X)

		# Button bar
		self.buttonbar = buttonbar.ButtonBar(self.buttonframe)
		self.buttonbar['cursor'] = 'top_left_arrow'
		self.buttonbar.pack(side=LEFT, expand=1, fill=X)

		for conf in Config.SUBLIST:
			button = self.add_button(conf)

		# Presence status dropdown
		self.presence_box = Pmw.ComboBox(self.buttonframe, scrolledlist_items=
			Config.PRESENCE_STRINGS,
			selectioncommand=self.change_presence,
			entry_takefocus=0, arrowbutton_takefocus=0)
		self.presence_box.selectitem(Config.DEFAULT_PRESENCE)
		self.presence_box.pack(side=RIGHT, expand=0)
		self.presence_box.component('entry').bind('<Key-Up>',
			self.drop_presence)
		self.presence_box.component('entry').bind('<Key-Down>',
			self.drop_presence)
		# These two don't work yet
		self.presence_box.component('entry').bind('<Return>',
			self.tabForward, add=1)
		#self.presence_box.component('listbox').bind('<Return>',
		#	self.tabForward, add=1)

		# Main text widget
		pane = Pmw.PanedWidget(self, hull_height=Config.PANE_HEIGHT)
		pane.add('bottom', min=0, size=Config.PANE_BOTTOM)
		pane.insert('top', before='bottom', min=0, size=Config.PANE_TOP)
		self.pane = pane
		# Pack the pane after creating its child text widgets
	
		# Status bar
		self._status = Label(pane.pane('bottom'), anchor='w', width=80)

		# Button frame
		self.puffbuts = Frame(pane.pane('bottom'))
		self._sendbutton = Button(self.puffbuts, text='Send',
			command=self.sendbutton, takefocus=0, padx=2, pady=1)
		self._sendbutton.pack(side=LEFT)
		self._detachbutton = Button(self.puffbuts, text='Detach',
			command=self.detach, takefocus=0, pady=1, padx=2)
		self._detachbutton.pack(side=LEFT)
		if Config.MAP_PUFFBUTTONS:
			self.puffbuts.pack(side=TOP, fill=X, expand=0, padx=2)

		# --------------------------------------------------
		# Puff entry widgets

		self._puffwidget = PuffWidget(pane.pane('bottom'))
		self._puffwidget.pack(side=TOP, fill=BOTH, expand=1, padx=2,
			pady=2)

		# Right button menu
		# NOTE: when anything changes in this menu, also modify the
		# popup_context function
		self.context_menu = Menu(self, tearoff=0)
		self.context_menu.add_command(label='Reply (C-r)', command=self.reply,
			underline=0)
		self.context_menu.add_command(label='Reply to sender (C-R)',
			command=self.reply_to_sender, underline=9)
		self.context_menu.add_command(label='Next in thread (C-n)',
			command=self.next_thread, underline=0)
		self.context_menu.add_command(label='Previous in thread (C-p)',
			command=self.prev_thread, underline=0)
		self.context_menu.add_command(label='Author search (A-a)',
			command=self.author_search, underline=0)
		self.context_menu.add_command(label='Cite puff (A-t)',
			command=self.cite_puff, underline=2)
		self.context_menu.add_separator()
		self.context_menu.add_command(label='Copy URL to clipboard',
			command=self.own_selection, underline=1, state='disabled')
		self.context_menu.add_separator()
		self.context_menu.add_command(label='Unsub to this location',
			command=self.kill_cat, underline=0)
		self.context_menu.add_command(label='Detach to new window',
			underline=0, command=self.puffdisp)

		# The ordering here is tricky.  The addpuff() calls must happen
		# after the status bar has been created (above).	The
		# pane.configure must happen after the text widgets have been
		# created.
		for conf in Config.SUBLIST:
			text = self.make_text_widget()
			# Create a Gale connection
			# This should be done later, when the send connection is
			# created
			conf.setwidget(self, text)
			text.after_idle(lambda pl=conf.pufflist(), s=self, c=conf:
				s.idle_pufflist(c, pl))
			conf._pufflist = []

		self.switch_screen(0)

		# Configure the pane width after the text widget's created and
		# packed
		if not Config.SAVE_PANE_WIDTH or Config.PANE_WIDTH == -1:
			pane.update_idletasks()
			width = 0
			for name in pane.panes():
				w = pane.pane(name).winfo_reqwidth()
				if width < w:
					width = w
			pane.configure(hull_width = width)
		else:
			pane.configure(hull_width = Config.PANE_WIDTH)
		pane.pack(side=TOP, expand=1, fill=BOTH)

	# ----------------------------------------------------------------------
	# Debug menu
	def toggle_debug_var(self, boolvar, debugvar):
		val = boolvar.get()
		exec('%s = %i' % (debugvar, val * self.var_d_level.get()))
	
	def set_debug_level(self, *args):
		# TODO: set debug level of everything
		pass

	def garbage_collect(self, *args):
		if sys.version >= '2.0':
			import gc
			retval = gc.collect()
			print 'Collector: %i unreachables' % retval
		else:
			print 'Explicit GC not supported in Python < 2.0'
	# ----------------------------------------------------------------------
	
	def show_keywords(self):
		self._puffwidget.show_keywords()
	
	def hide_keywords(self):
		self._puffwidget.hide_keywords()

	def idle_pufflist(self, conf, pufflist):
		for p in pufflist:
			conf.addpuff(p, 0)
		conf.widget().see('end')
		conf.connect()

	# Make a text widget to contain puffs
	def make_text_widget(self):
		text = Pmw.ScrolledText(self.pane.pane('top'), text_height=50,
			text_width=81, text_selectforeground='white',
			text_selectbackground='black', text_selectborderwidth=0,
			text_wrap='word', text_font=FONTS['TEXT_FONT'],
			vertscrollbar_takefocus=0,
			vertscrollbar_width=10)
		self.set_text_cursor(text)

		text.component('text').bind('<Button-3>', self.url_store)
		text.component('text').bind('<Button-3>', self.popup_context, add=1)
		# Bind these here to override Tk's default binding for
		# Button4/Button5 in the text widget (in 8.3, but not in 8.0)
		text.component('text').bind('<Button-4>', self.page_up)
		text.component('text').bind('<Button-5>', self.page_down)

		# Tags for the text widget
		text.tag_config('bold', font=FONTS['BOLD_FONT'])
		text.tag_config('small', font=FONTS['SMALL_FONT'])
		text.tag_config('header', font=FONTS['HEADER_FONT'])
		text.tag_config('selected-puff', background=Config.HIGHLIGHT)

		# URLs
		text.tag_config('URL', foreground='blue',
			underline=1)
		text.tag_bind('URL', '<Enter>',
			lambda e, s=self, t=text: s.set_text_cursor(t, 'hand2'))
		text.tag_bind('URL', '<Leave>',
			lambda e, s=self, t=text: s.set_text_cursor(t))
		text.tag_bind('URL', '<Button-1>', self.url_store)
		text.tag_bind('URL', '<Button-1>', self.url_click, add=1)
		text.tag_config('URLclick', foreground='red',
			underline=1)
		# Lower it so its binding is executed first
		text.tag_lower('URL')

		# Tags for nifty horizontal rule
		text.tag_config('hr', font=FONTS['HR_FONT'],
			relief=FONTS['HR_RELIEF'],
			borderwidth=1, background=FONTS['HR_BACKGROUND'])
		text.tag_config('hrspacing', font=FONTS['HR_SPACING_FONT'])
		text.tag_config('right', justify='right')

		# Quotation
		if HAVE_FONTS:
			quotefont = tkFont.Font(self, FONTS['QUOTE_FONT'])
			quotewidth = quotefont.measure('> ') + 4
			text.tag_config('quotation', font=FONTS['QUOTE_FONT'],
				lmargin1=4, lmargin2=quotewidth, rmargin=4)
		else:
			# Just make a guess for how much to indent subsequent lines
			text.tag_config('quotation', font=FONTS['QUOTE_FONT'],
				lmargin1=4, lmargin2=10, rmargin=4)

		# Custom colors
		colors = Config.COLORS.values()
		for c in colors:
			text.tag_config('%s-fg' % c, foreground=c)

		text.configure(text_state = 'disabled')
		return text
	
	def add_button(self, conf):
		button = Button(self.buttonbar, text=conf['name'],
			highlightthickness=0, takefocus=0, pady=1, padx=2)
		button['command'] = lambda s=self, b=button:\
			s.switch_screen_unknown(b)
		self.buttonbar.add_button(button)
		self._buttonlist.append(button)
	
	def insert_button(self, button, index=None):
		if index is None:
			self._buttonlist.append(button)
		else:
			self._buttonlist.insert(index, button)
		self.buttonbar.add_button(button, index)
	
	def del_button(self, index):
		button = self._buttonlist[index]
		self.buttonbar.del_button(button)
		self._buttonlist[index:index+1] = []
		return button
	
	def change_button_name(self, index, name):
		self._buttonlist[index]['text'] = name

	# ------------------------------------------------------------
	# Reconnect screens
	def reconnect_all(self):
		for c in Config.SUBLIST:
			c.make_connection()
		self.make_send_conn()

	# ------------------------------------------------------------
	# Callbacks for ScreenPop
	def move_button_up(self, index):
		button = self.del_button(index)
		self.insert_button(button, index-1)

		self.regen_screenmenu()

	def move_button_down(self, index):
		button = self.del_button(index)
		self.insert_button(button, index+1)

		self.regen_screenmenu()

	# ------------------------------------------------------------

	# Switch to the requested screen
	# Index starts at 0
	def switch_screen(self, index):
		# Return immediately if this isn't a valid screen number
		if index >= len(Config.SUBLIST) or index < 0: return
		Config.SUBLIST[index].cancel_highlight()
		# Don't do anything else if we're already lifted
		if index == self._curscreen: return
		if self._curscreen == index: return
		# Raise old button
		if self._curscreen is not None:
			self._buttonlist[self._curscreen]['relief'] = 'raised'
			Config.SUBLIST[self._curscreen].widget().forget()
		Config.SUBLIST[index].widget().pack(side=TOP, expand=1,
			fill=BOTH, padx=2, pady=2)
		self._curscreen = index
		self._buttonlist[index]['relief'] = 'sunken'
	
	# Invoked on a button click
	def switch_screen_unknown(self, button):
		index = self._buttonlist.index(button)
		self.switch_screen(index)

	def is_curscreen(self, screen):
		return Config.SUBLIST[self._curscreen] == screen
	
	def _category_regexp(self, locstr):
		# TODO dangermouse
		# this breaks on locations containing spaces
		# Split locstr into list of locations
		locs = string.split(locstr, None)
		locs = map(re.escape, locs)
		return re.compile(string.join(locs, '|'))
	
	# Jump to the next/prev thread matching the current puff's category
	def next_thread(self, *args):
		client = Config.SUBLIST[self._curscreen]
		if client.curindex() is None: return
		orig_re = self._category_regexp(client.curpuff().get_loc())
		puffindex = client.curindex()
		for i in range(puffindex+1, len(client)):
			puffcats = string.split(client[i].get_loc(), None)
			for d in puffcats:
				if orig_re.match(d):
					client.set_curpuff(i)
					return
		self.set_status('Last puff in this thread')

	def prev_thread(self, *args):
		client = Config.SUBLIST[self._curscreen]
		if client.curindex() is None: return
		orig_re = self._category_regexp(client.curpuff().get_loc())
		puffindex = client.curindex()
		for i in range(puffindex-1, -1, -1):
			puffcats = string.split(client[i].get_loc(), None)
			for d in puffcats:
				if orig_re.match(d):
					client.set_curpuff(i)
					return
		self.set_status('First puff in this thread')

	# Find the next puff by this author
	def author_search(self, *args):
		client = Config.SUBLIST[self._curscreen]
		if client.curindex() is None: return
		puff = client.curpuff()
		sender = puff.get_text_first('message/sender', None)
		signer = puff.get_signer(None)
		puffindex = client.curindex()
		for i in range(puffindex+1, len(client)) +\
			range(0, puffindex):
			if i == 0 and puffindex != 0:
				self.set_status('Author search: wrapping to top')
			if sender and sender ==\
				client[i].get_text_first('message/sender', None):
				client.set_curpuff(i)
				return
			elif signer and signer ==\
				client[i].get_signer(None):
				client.set_curpuff(i)
				return
		self.set_status('No more puffs by this author')
	
	# Add a puff citation to the text widget
	def cite_puff(self, *args):
		puff = Config.SUBLIST[self._curscreen].curpuff()
		signer = puff.get_signer('anonymous')
		atpos = string.find(signer, '@')
		if atpos != -1:
			signer = signer[:atpos]
		pufftime = puff.get_time_first('id/time', None)
		if pufftime is None:
			timestr = ''
		else:
			timestr = time.strftime('%H:%M:%S',
				time.localtime(pufftime))
		self._clipboard = '%s[%s]' % (signer, timestr)
		self._puffwidget.text().insert('insert', self._clipboard, 'sel')
		self._puffwidget.text().tag_remove('sel', '1.0', 'end')
		self.own_selection()
	
	def puffdisp(self):
		import puffdisp
		puff = Config.SUBLIST[self._curscreen].curpuff()
		if puff is None: return
		p = puffdisp.PuffDisp(puff)
	
	# Pop up the right-button context menu
	# WARNING: the constant here has to be changed whenever you add
	# something to the context menu
	def popup_context(self, event):
		if self._clipboard is not None:
			self.context_menu.entryconfigure(7, state='normal')
		else:
			self.context_menu.entryconfigure(7, state='disabled')
		self.context_menu.tk_popup(event.x_root, event.y_root, 0)
	
	def own_selection(self):
		# Manipulate X selection
		self.selection_own()
		self.selection_handle(self.handle_selection)

	def handle_selection(self, start, end):
		if self._clipboard is not None:
			start = string.atoi(start)
			end = string.atoi(end)
			return self._clipboard[start:end]
		else:
			return ''

	# If a URL is under the cursor, store it in self._clipboard
	def url_store(self, event):
		client = Config.SUBLIST[self._curscreen]
		if 'URL' in client.widget().tag_names(
			"@%i,%i" % (event.x, event.y)):
			(start, end) = client.widget().tag_prevrange('URL',
				"@%i,%i" % (event.x, event.y))
			self._clipboard = client.widget().get(start, end)
		else:
			self._clipboard = None

	# Callback for clicking on a URL
	def url_click(self, event):
		if self._clipboard is None:
			return
		client = Config.SUBLIST[self._curscreen]
		(start, end) = client.widget().tag_prevrange('URL',
			"@%i,%i" % (event.x, event.y))
		client.widget().tag_add('URLclick', start, end)
		self.set_status('Loading url: %s' % self._clipboard)
		self.update_idletasks()
		if sys.platform == 'win32':
			try:
				import win32api
			except ImportError:
				self.gale_error('Unable to load URL (win32 ' +\
					'extensions not installed)')
			try:
				url = re.sub(',', '%2C', self._clipboard)
				win32api.ShellExecute(0, None, url, None, '.', 0)
			except Exception, e:
				self.gale_error('Error loading URL: %s' % str(e))
		else:
			try:
				urlcmd = Config.URLLOAD % re.sub(',', '%2C',
					self._clipboard)
			except TypeError, e:
				self.gale_error('Unable to construct URL command: ' +
					str(e))
				client.widget().tag_remove('URLclick', start, end)
				return
			if os.fork():
				# Parent
				client.widget().tag_remove('URLclick', start, end)
				return 'break'
			else:
				try:
					# Child
					args = string.split(urlcmd)
					args = [args[0]] + args
					apply(os.execlp, tuple(args))
				except:
					traceback.print_exc()
					sys.exit(0)

	# Kill these categories
	def kill_cat(self):
		client = Config.SUBLIST[self._curscreen]
		cat = client.curpuff().get_loc()
		negated_cats = map(lambda x: '-' + x, string.split(cat, None))
		negcat = string.join(negated_cats, ' ')
		client['sub'] = client['sub'] + ' ' + negcat
		client.make_connection()

	# What to do on a quit
	def quit(self, *args):
		Config.PANE_HEIGHT = self.pane.winfo_height()
		Config.PANE_TOP = self.pane.pane('top').winfo_height()
		Config.PANE_BOTTOM = self.pane.pane('bottom').winfo_height() + 2
		if Config.SAVE_PANE_WIDTH:
			Config.PANE_WIDTH = self.pane.pane('top').winfo_width()
		else:
			Config.PANE_WIDTH = -1

		# Save presence strings
		Config.PRESENCE_STRINGS = list(self.presence_box.get(0, 'end'))
		Config.DEFAULT_PRESENCE = self.presence_box.get()

		try:
			Config.save(FUGUDIR)
			Config.persist_pufflog(FUGUDIR)
		except:
			traceback.print_exc()
		Frame.quit(self)
	
	def set_text_cursor(self, widget, cur='top_left_arrow'):
		widget.configure(text_cursor = cur)

	# Tab to next widget binding
	def tabForward(self, event):
		w = event.widget.tk_focusNext()
		w.focus_set()
		return 'break'
	
	# Paging up and down
	def page_up(self, event):
		conn = Config.SUBLIST[self._curscreen]
		conn.widget().yview('scroll', -1, 'pages')
		return 'break'

	def page_down(self, event):
		conn = Config.SUBLIST[self._curscreen]
		conn.widget().yview('scroll', 1, 'pages')
		return 'break'
	
	# This is more than just invoking the send button; it also has to
	# return 'break' so the return doesn't get processed and add a newline
	# to the message.
	def control_return(self, event):
		self.sendbutton()
		return 'break'

	# Binding script that returns break
	def ret_break(self, event):
		return 'break'

	# Define some nice keyboard shortcuts
	def _bind_hotkeys(self):
		# Hotkeys
		self._puffwidget.bind('<Control-Return>', self.control_return)

		self._puffwidget.bind('<Alt-c>', lambda e, s=self:
			s.clearpuff())
		self._puffwidget.bind('<Alt-d>', lambda e, s=self:
			s._detachbutton.invoke())
		self._puffwidget.bind('<Control-r>',
			lambda e, s=self: s.reply())
		self._puffwidget.bind('<Control-R>',
			lambda e, s=self: s.reply_to_sender())
		self._puffwidget.bind('<Key-Up>',
			lambda e, s=self: s.key_up(), text=0)
		self._puffwidget.bind('<Key-Down>',
			lambda e, s=self: s.key_down(), text=0)
		self._puffwidget.bind('<Alt-a>', self.author_search)
		self._puffwidget.bind('<Control-z>', lambda e, s=self:
			s.master.iconify())
		self._puffwidget.bind('<Control-s>', self.search_pop)
		self._puffwidget.bind('<Control-Prior>', self.home)
		self._puffwidget.bind('<Control-Next>', self.end)

		# Focus presence widget
		self._puffwidget.bind('<Control-Key-P>', lambda e, s=self:
			s.presence_box.component('entry').focus_set())

		# These bindings only work in the entry fields
		self._puffwidget.bind('<Control-n>', self.next_thread, text=0)
		self._puffwidget.bind('<Control-p>', self.prev_thread, text=0)
		self._puffwidget.bind('<Prior>', self.page_up, text=0)
		self._puffwidget.bind('<Next>', self.page_down, text=0)

		# Mouse button bindings have to be bound to the master frame
		self.master.bind('<Button-4>', self.page_up)
		self.master.bind('<Button-5>', self.page_down)

		# Alt-up/down always move the puff cursor
		self._puffwidget.bind('<Alt-Up>', self.key_up)
		self._puffwidget.bind('<Alt-Down>', self.key_down)
		self._puffwidget.bind('<Alt-Prior>', self.page_up)
		self._puffwidget.bind('<Alt-Next>', self.page_down)

		# Cut the reference to the cursor
		self._puffwidget.bind('<Alt-t>', self.cite_puff)

		# This next line causes problems; it binds to all entry widgets,
		# which it oughtn't do
		#self.master.bind_class('Entry', '<Return>', self.tabForward)

		# Break after certain commands in the text widget so it doesn't
		# change the puff selection
		# Perhaps this ought to bind only to the puff entry widget, and
		# not all text widgets
#		self.master.bind_class('Text', '<Up>', self.ret_break, add=1)
#		self.master.bind_class('Text', '<Down>', self.ret_break, add=1)
#		self.master.bind_class('Text', '<Shift-Up>', self.ret_break, add=1)
#		self.master.bind_class('Text', '<Shift-Down>', self.ret_break, add=1)
#		self.master.bind_class('Text', '<Control-a>', self.ret_break, add=1)
#		self.master.bind_class('Text', '<Control-n>', self.ret_break, add=1)
#		self.master.bind_class('Text', '<Control-p>', self.ret_break, add=1)
#		self.master.bind_class('Text', '<Prior>', self.ret_break, add=1)
#		self.master.bind_class('Text', '<Next>', self.ret_break, add=1)

		# Switching between screens with C-N where 1 <= N <= 9
		for i in range(1, 10):
			self._puffwidget.bind('<Control-Key-%i>' % i, lambda e, i=i,
				s=self: s.switch_screen(i-1))
	
	def home(self, *args):
		Config.SUBLIST[self._curscreen].widget().see(1.0)
	def end(self, *args):
		Config.SUBLIST[self._curscreen].widget().see('end')

	# Pop up a configuration dialog
	def config_pop(self):
		w = ConfigPop(self)
	
	# Pop up a search dialog
	def search_pop(self, *args):
		w = search.SearchDialog(self,
			Config.SUBLIST[self._curscreen].widget(),
			Config.SUBLIST[self._curscreen]['name'])

	def screen_pop(self):
		self.screenpop.deiconify()
		self.screenpop.set_screen(self._curscreen)
		self.screenpop.tkraise()

	def clear_curscreen(self):
		Config.SUBLIST[self._curscreen].clear_puffs()
	
	def show_marks(self):
		marks = Config.SUBLIST[self._curscreen].widget().mark_names()
		for name in marks:
			print name
		print 'Total:', len(marks)
	def show_tags(self):
		tags = Config.SUBLIST[self._curscreen].widget().tag_names()
		for name in tags:
			print name
		print 'Total:', len(tags)
	def show_pufflist(self):
		print 'Puffs in pufflist:',
		print len(Config.SUBLIST[self._curscreen])
	
	def regen_screenmenu(self):
		i = 0
		# WARNING: update this number when the contents of the screen menu
		# change!!!
		self.screenmenu.menu.delete(3, 'end')
		self.screenmenu.menu.add_separator()
		for conf in Config.SUBLIST:
			self.screenmenu.menu.add_command(label=conf['name'] +
				" (C-%i)" % (i+1),
				command=lambda s=self, i=i: s.switch_screen(i))
			i = i + 1
	
	# Actually send this puff
	# Returns 0 on success, something else otherwise
	def sendpuff(self, loc, keyw, pufftext, callback = None):
		puff_sender = PuffSender(self, loc, keyw, pufftext,
			callback)
		puff_sender.send()

	# What to do when we receive a puff on this connection
	def recvpuff(self, conn, puff):
		if puff.get_text('question.receipt'):
			if conn['send_receipts']:
				self.after_idle(lambda s=self, p=puff:
					s.send_recpt(p))
		self.after_idle(lambda p=puff, c=conn: c.addpuff(p))

	def verify_update(self, conn, puff):
		self.after_idle(lambda p=puff, c=conn: c.puff_verified(p))
		
	# Send a return receipt if we're supposed to
	# This must only be called if the puff contains a "question.receipt"
	# fragment
	def send_recpt(self, puff):
		p = pygale.Puff()
		p.set_loc(puff.get_text_first('question.receipt'))
		p.set_text('answer.receipt', Config.SIGNER)
		p.set_text('notice/presence',
			self.presence_box.get(first=None))
		if Config.SENDER:
			p.set_text('message/sender', Config.SENDER)
		p.set_text('id/class', FUGU_VERSION_LONG)
		p.set_time('id/time', int(time.time()))
		p.set_text('id/instance', pygale.getinstance())
		# Sign return receipt
		p = p.sign_message(Config.SIGNER)
		# Encrypt if original message was signed
		old_signer = puff.get_signer(None)
		if old_signer is not None and old_signer not in \
			['*unsigned*', '*unverified*']:
			p = p.encrypt_message(old_signer,
				lambda p, s=self, orig=p: s.send_recpt2(p, orig))
		else:
			try:
				self._galeconn.transmit_puff(p)
			except pygale.PyGaleErr, e:
				# server connection died
				self.gale_error(e)
				# Assume retry is already in progress
				return
	
	def send_recpt2(self, puff, origpuff):
		if puff is None:
			self.gale_error('Unable to encrypt return receipt')
			puff = origpuff

		try:
			self._galeconn.transmit_puff(puff)
		except pygale.PyGaleErr, e:
			# server connection died
			# Assume retry is already in progress
			self.gale_error(e)

	# Send our key if it was requested
	def akd_request(self, reqpuff):
		reqkey = reqpuff.get_text_first('question.key', None)
		if reqkey is not None:
			if reqkey != Config.SIGNER:
				return
			me = pygale.export_pubkey(Config.SIGNER, lambda e, s=self:
				s.akd_request2(e))

		# Return receipt processing
		if reqpuff.get_text('question.receipt'):
			self.after_idle(lambda s=self, p=reqpuff:
				s.send_recpt(p))
	
	def akd_request2(self, export_key):
		if not export_key:
			return
		p = pygale.Puff()
		p.set_binary('answer/key', export_key)
		p.set_text('id/instance', pygale.getinstance())
		p.set_text('id/class', FUGU_VERSION_LONG)
		p.set_loc('_gale.key.' + Config.SIGNER)
		try:
			self._galeconn.transmit_puff(p)
		except pygale.PyGaleErr, e:
			# server connection died
			# Assume retry is already in progress
			self.gale_error(e)

	# Put some nice defaults into the puffwidget to reply to the currently
	# selected puff
	def reply(self):
		client = Config.SUBLIST[self._curscreen] 
		p = client.curpuff()
		if p is None: return

		REPLY_POPUP = 0
		if REPLY_POPUP:
			win = PuffWin(self)
		else:
			win = self._puffwidget

		# Figure out some nice defaults
		# Check for login/logout notices
		notice_re = re.compile('_gale\.notice\.(.*)')
		notice_groups = notice_re.match(p.get_loc())

		dests = []
		recipients = p.get_recipients()
		signer = p.get_signer(None)

		if notice_groups:
			# Handle notice messages separately
			dests = [notice_groups.group(1)]
		elif not recipients or signer in recipients:
			# Public unencrypted puff or group send
			dests = p.get_loc_list()
		else:
			dests = p.get_loc_list()
			if signer is not None and signer not in ['*verifying*',
				'*unverified*', '*unsigned*']:
				dests.append(signer)
		dests = filter(lambda x: x != Config.SIGNER, dests)
		if not dests:
			# No one to send to!  Send to myself
			dests = [Config.SIGNER]
		win.set_loc(string.join(dests, ' '))

		keywords = string.join(p.get_text('message.keyword'), ', ')
		win.set_keyw(keywords)
		win.set_puff_focus()
	
	def reply_to_sender(self):
		client = Config.SUBLIST[self._curscreen] 
		p = client.curpuff()
		if p is None: return
		if p.get_signer(None) is None: return
		self._puffwidget.set_loc(p.get_signer())
		keywords = string.join(p.get_text('message.keyword'), ', ')
		self._puffwidget.set_keyw(keywords)
		self._puffwidget.set_puff_focus()

	# Page up and down through the pufflist
	def key_up(self, *args):
		client = Config.SUBLIST[self._curscreen] 
		if not client.pufflist(): return
		index = client.curindex()
		if index is None:
			client.set_curpuff(len(client) - 1)
		elif index > 0:
			client.set_curpuff(index - 1)
		return 'break'

	def key_down(self, *args):
		client = Config.SUBLIST[self._curscreen] 
		if not client.pufflist(): return
		index = client.curindex()
		if index is None:
			client.set_curpuff(len(client) - 1)
		elif index < len(client) - 1:
			client.set_curpuff(index + 1)
		return 'break'

	def update_highlight(self):
		for i in range(len(Config.SUBLIST)):
			Config.SUBLIST[i].widget().tag_config(
				'selected-puff', background=Config.HIGHLIGHT)
	
	def update_autocomplete(self):
		self._puffwidget.update_autocomplete(Config.AUTOCOMPLETE)

	# Manipulate status bar text
	def set_status(self, text):
		self._status.pack(side=BOTTOM, after=self._puffwidget,
			fill=NONE, expand=0, padx=2)
		self._status['text'] = text
		if self._status_clear:
			# Cancel previous status_clear callback
			self.after_cancel(self._status_clear)
		self._status_clear = self.after(STATUS_TIMEOUT,
			self.clear_status)

	def clear_status(self):
		self._status['text'] = ''
		self._status_clear = None
		self._status.forget()

	# Pop up the log message window
	def popup_logwin(self):
		self.logwin.deiconify()
		self.logwin.tkraise()

	# Pop up an about box
	def about(self):
		Pmw.aboutversion(FUGU_VERSION)
		Pmw.aboutcopyright('Copyright Tessa Lau 2001')
		Pmw.aboutcontact(
			'Tessa Lau\n' +
			'tlau-fugu@ofb.net\n' +
			'http://fugu.gale.org/')
		self.about = Pmw.AboutDialog(self, applicationname='Fugu')
		self.about.show()

	# Clear the puff fields
	def clearpuff(self):
		self._puffwidget.clear()
		self._puffwidget.set_loc_focus()
	
	# Tell the puffwidget to focus on the location field
	def set_loc_focus(self):
		self._puffwidget.set_loc_focus()

	# Detach this puff into its own window
	def detach(self):
		p = PuffWin(self)
		self.clearpuff()
	
	# Actions taken when send button is pressed
	def sendbutton(self, puffwidget=None, callback=None):
		if puffwidget is None:
			puffwidget = self._puffwidget

		if not puffwidget.loc():
			d = tkMessageBox.showerror(
				title='Error: no location',
				parent=self,
				message='No location has been specified.')
			return 1
		if ':' in puffwidget.loc():
			d = tkMessageBox.askyesno('Colon detected',
				'Your location contains a colon.  Locations should ' +
				'now be separated by spaces.  Continue?')
			if not d: return
		keywords = puffwidget.keyw()
		slashed_keywords = filter(lambda x: x and x[0] == '/', keywords)
		if slashed_keywords:
			d = tkMessageBox.askyesno('Keyword starts with slash',
				"One or more of your keywords begins with a slash. " +
				"Keywords should be separated by commas. " +
				"Continue?")
			if not d: return
		spaced_keywords = filter(lambda x: x and ' ' in x, keywords)
		if spaced_keywords:
			d = tkMessageBox.askyesno('Keyword contains space',
				"One or more of your keywords contains a space " +
				"character.  Keywords should be separated by commas. " +
				"Continue?")
			if not d: return
		if not puffwidget.puff():
			d = tkMessageBox.askyesno('Empty puff warning',
				"You're about to send an empty puff.  Continue?")
			if not d: return

		# Make the comboboxes save the categories in their histories
		puffwidget.save_state()
		if callback is None:
			callback = self.puff_sent
		ret = self.sendpuff(puffwidget.loc(),
			puffwidget.keyw(), puffwidget.puff(),
			lambda r, cb=callback: cb(r))

	def puff_sent(self, result):
		if result:
			return
		self._puffwidget.clear_text()
		self._puffwidget.clear_keyw()
		self._puffwidget.set_loc_focus()
	
# ----------------------------------------------------------------------
# A combo widget containing location category entry widgets, and a text
# entry widget

class PuffWidget(Frame):
	def __init__(self, master=None, height=5, **kw):
		Frame.__init__(self, master)
		self._loctext = kw.get('loctext', '')
		self._keywtext = kw.get('keywtext', '')
		self._pufftext = kw.get('pufftext', '')
		self._create_widgets(height)

	def _create_widgets(self, text_height):
		# Category entry
		self._location = HistoryCombo(self,
			label_text='Locations:', labelpos='w',
			arrowbutton_takefocus=0, completion=Config.AUTOCOMPLETE)
		self._location.component('entry').insert(0, self._loctext)
		self._location.component('entry').bind('<Shift-Key-Up>',
			lambda e, s=self, p=self._location: s.drop_presence(p))
		self._location.component('entry').bind('<Shift-Key-Down>',
			lambda e, s=self, p=self._location: s.drop_presence(p))
		self._location.component('entry').bind('<Tab>',
			lambda e, s=self._location.component('entry'):
			s.selection_clear())
		self._location.pack(side=TOP, fill=X, expand=0)

		self._keywords = HistoryCombo(self, label_text='Keywords:',
			labelpos='w', arrowbutton_takefocus=0,
			completion=Config.AUTOCOMPLETE)
		self._keywords.component('entry').insert(0, self._keywtext)
		self._keywords.component('entry').bind('<Shift-Key-Up>',
			lambda e, s=self, p=self._keywords: s.drop_presence(p))
		self._keywords.component('entry').bind('<Shift-Key-Down>',
			lambda e, s=self, p=self._keywords: s.drop_presence(p))
		self._keywords.component('entry').bind('<Tab>',
			lambda e, s=self._keywords.component('entry'):
			s.selection_clear())
		if Config.KEYWORD_PROMPT:
			self._keywords.pack(side=TOP, fill=X, expand=0)

		# Text of the puff
		self._puff = Pmw.ScrolledText(self, text_height=text_height,
			text_width=80, text_wrap='word',
			text_font=FONTS['TEXT_FONT'],
			vertscrollbar_takefocus=0)
		self._puff.pack(side=TOP, fill=BOTH, expand=1)
		self._puff.component('text').bind('<Tab>', self.tabForward)
		self._puff.insert('end', self._pufftext)
		
		if sys.platform != 'win32':
			# Don't bind these on windows; they're already bound
			self._bindkeys()

	def _bindkeys(self):
		# CUA-style bindings
		self.bind_generate('<Control-c>', '<<Copy>>', loc=0)
		self.bind_generate('<Control-v>', '<<Paste>>', loc=0)
	
	def update_autocomplete(self, newval):
		self._location.configure(completion = newval)
	
	def drop_presence(self, widget):
		widget.invoke()
		widget.component('listbox').focus_set()

	def ret_break(self, *args):
		return 'break'
	
	def save_state(self):
		self._location._addHistory()
		self._keywords._addHistory()
	
	def bind(self, seq, func, add=None, loc=1, text=1, keyw=1):
		if loc:
			self._location.component('entry').bind(seq, func, add)
		if keyw:
			self._keywords.component('entry').bind(seq, func, add)
		if text:
			self._puff.component('text').bind(seq, func, add)
	
	def bind_generate(self, seq, event_str, loc=1, text=1, keyw=1):
		if loc:
			self._location.component('entry').bind(seq,
				lambda e, w=self._location.component('entry'), f=event_str:
				w.event_generate(f))
		if keyw:
			self._keywords.component('entry').bind(seq,
				lambda e, w=self._keywords.component('entry'), f=event_str:
				w.event_generate(f))
		if text:
			self._puff.component('text').bind(seq,
				lambda e, w=self._puff.component('text'), f=event_str:
				w.event_generate(f))
	
	def show_keywords(self):
		self._keywords.pack(side=TOP, fill=X, expand=0, before=self._puff)
	
	def hide_keywords(self):
		self._keywords.forget()
	
	def call_and_break(self, func, event):
		func(event)
		return 'break'

	def tabForward(self, event):
		w = event.widget.tk_focusNext()
		w.focus_set()
		return 'break'

	def loc(self):
		return self._location.get()
	
	def keyw(self):
		"Return list of keywords"
		t = self._keywords.get()
		if not string.strip(t):
			return []
		else:
			keys = map(string.strip, string.split(t, ','))
		return keys
	def keywtext(self):
		return self._keywords.get()

	def text(self):
		return self._puff
	
	def puff(self):
		text = self._puff.get(1.0, 'end')
		# For some reason, the Text widget puts an extra \012 on the end
		# of this
		if text and text[-1] == '\012':
			text = text[:-1]
		if text and text[-1] == '\012':
			text = text[:-1]
		if text and text[-1] != '\012':
			text = text + '\012'
		return text
	
	def clear(self):
		self.clear_loc()
		self.clear_keyw()
		self._puff.delete(1.0, 'end')
	
	def clear_text(self):
		self._puff.delete(1.0, 'end')
	
	def clear_loc(self):
		self._location.component('entry').delete(0, 'end')

	def clear_keyw(self):
		self._keywords.component('entry').delete(0, 'end')

	def set_loc(self, text):
		self.clear_loc()
		self._location.component('entry').insert('end', text)
	
	def set_keyw(self, text):
		self.clear_keyw()
		self._keywords.component('entry').insert('end', text)
	
	def set_puff_focus(self, *args):
		self._puff.component('text').focus_set()
	def set_loc_focus(self, *args):
		self._location.component('entry').focus_set()
	def set_keyw_focus(self, *args):
		self._keywords.component('entry').focus_set()

# Create a toplevel clone of the master puffwidget
class PuffWin(Toplevel):
	def __init__(self, master):
		Toplevel.__init__(self)
		self._master = master
		# Make myself transient
		if hasattr(self, 'wm_transient'):
			self.wm_transient(master)
		elif hasattr(self, 'transient'):
			self.transient(master)
		# Buttons
		butframe = Frame(self)
		butframe.pack(side=TOP, expand=0, fill=X, padx=2, pady=2)
		self._sendbutton = Button(butframe, text='Send (C-Enter)',
			command=self.sendbutton, takefocus=0)
		self._sendbutton.pack(side=LEFT, expand=0, fill=X)
		self._abortbutton = Button(butframe, text='Abort (A-c)',
			command=self.destroy, takefocus=0)
		self._abortbutton.pack(side=LEFT, expand=0, fill=X)

		# Puff widget
		self._puffwidget = PuffWidget(self,
			loctext=master._puffwidget.loc(),
			keywtext = master._puffwidget.keywtext(),
			pufftext = master._puffwidget.puff(), height=10, width=60)
		self._puffwidget.pack(side=BOTTOM, fill=BOTH, expand=1, padx=2,
			pady=2)

		# Hotkeys
		self._puffwidget.bind('<Control-Return>', self.control_return)
		self.bind('<Alt-c>', lambda e, s=self:
			s._abortbutton.invoke())
		self._puffwidget.set_puff_focus()
	
	# This is more than just invoking the send button; it also has to
	# return 'break' so the return doesn't get processed and add a newline
	# to the message.
	def control_return(self, event):
		self.sendbutton()
		return 'break'

	def sendbutton(self, *args):
		self._master.sendbutton(self._puffwidget, self.puff_sent)

	def puff_sent(self, result):
		if result:
			return
		self.destroy()
	
	def set_loc(self, *args):
		return apply(self._puffwidget.set_loc, args)
	def set_keyw(self, *args):
		return apply(self._puffwidget.set_keyw, args)
	def clear_loc(self, *args):
		return apply(self._puffwidget.clear_loc, args)
	def clear_keyw(self, *args):
		return apply(self._puffwidget.clear_keyw, args)
	def set_puff_focus(self, *args):
		return apply(self._puffwidget.set_puff_focus, args)
	def set_loc_focus(self, *args):
		return apply(self._puffwidget.set_loc_focus, args)
	def set_keyw_focus(self, *args):
		return apply(self._puffwidget.set_keyw_focus, args)
	
class ConfigPop(Toplevel):
	def __init__(self, master):
		Toplevel.__init__(self)
		if hasattr(self, 'wm_transient'):
			self.wm_transient(master)
		elif hasattr(self, 'transient'):
			self.transient(master)
		self._master = master

		# OK/Cancel buttons
		butframe = Frame(self)
		butframe.pack(side='bottom', expand=1, fill=X)
		ok = Button(butframe, text="OK", command=self.ok)
		ok.pack(side=LEFT, expand=1, fill=X)
		cancel = Button(butframe, text="Cancel", command=self.destroy)
		cancel.pack(side=LEFT, expand=1, fill=X)
	
		f = Frame(self)
		f.pack(side='left', expand=1, fill=BOTH)
		g = Frame(self)
		g.pack(side='right', expand=1, fill=BOTH)

		# Puff sender
		self.sender = Pmw.EntryField(f, labelpos='w',
			value=Config.SENDER, label_text = 'Puff sender:')
		self.sender.pack(side=TOP, expand=0, fill=X, padx=2, pady=2)

		# Signer
		self.signer = Pmw.EntryField(f, labelpos='w',
			value=Config.SIGNER, label_text = 'Puff signer:')
		self.signer.pack(side=TOP, expand=0, fill=X, padx=2, pady=2)

		# Puff history
		self.scrollback = Pmw.EntryField(f,
			labelpos='w', value=Config.SCROLLBACK,
			label_text = 'Puff history:',
			validate={'validator':'integer', 'min':1, 'max':1000})
		self.scrollback.pack(side=TOP, expand=0, fill=X, padx=2, pady=2)

		# Puff persist interval
		self.persist = Pmw.EntryField(f,
			labelpos='w', value=Config.PERSIST_INTERVAL,
			label_text = 'Save puff history (secs)',
			validate={'validator':'integer', 'min':0})
		self.persist.pack(side=TOP, expand=0, fill=X, padx=2, pady=2)

		# Cc self on private puffs
		self.cc_var = BooleanVar()
		self.cc_var.set(Config.CC_SELF)
		cc = Checkbutton(f, text='Send copy of private puffs to self',
			variable=self.cc_var, anchor='w')
		cc.pack(side=TOP, expand=0, fill=X, padx=2, pady=2)

		# Presence status
		self.presence_var = BooleanVar()
		self.presence_var.set(Config.BROADCAST_PRESENCE)
		self.presence = Checkbutton(f, text='Broadcast presence status',
			variable=self.presence_var, onval=1, offval=0, anchor='w')
		self.presence.pack(fill=X, expand=1, padx=2, pady=2)

		# Map/unmap puffbuttonbar
		self.map_var = BooleanVar()
		self.map_var.set(Config.MAP_PUFFBUTTONS)
		self.map_var.trace('w', self.toggle_mapbuttons)
		button = Checkbutton(f, text='Show puff button bar',
			variable=self.map_var, onval=1, offval=0, anchor='w')
		button.pack(fill=BOTH, expand=1, padx=2, pady=2)

		# Map/unmap keywords prompt
		self.keyw_var = BooleanVar()
		self.keyw_var.set(Config.KEYWORD_PROMPT)
		self.keyw_var.trace('w', self.toggle_keywords)
		button = Checkbutton(f, text='Prompt for puff keywords',
			variable=self.keyw_var, onval=1, offval=0, anchor='w')
		button.pack(fill=BOTH, expand=1, padx=2, pady=2)

		# Whether to display thumbnails
		self.thumb_var = BooleanVar()
		self.thumb_var.set(Config.SHOW_THUMBS)
		button = Checkbutton(f, text='Display thumbnail images',
			variable = self.thumb_var, onval=1, offval=0, anchor='w')
		button.pack(fill=BOTH, expand=1, padx=2, pady=2)

		# URL command
		group = Pmw.Group(g, tag_text='URL command')
		group.pack(side=TOP, fill=BOTH, expand=1, padx=2, pady=2)
		self.urlcommand = Entry(group.interior(), width=30)
		self.urlcommand.insert(0, Config.URLLOAD)
		self.urlcommand.pack(side=LEFT, expand=1, fill=X, padx=2, pady=2)

		# Thumbnail URL
		group = Pmw.Group(g, tag_text='URL to my thumbnail')
		group.pack(side=TOP, fill=BOTH, expand=1, padx=2, pady=2)
		self.thumburl = Entry(group.interior(), width=30)
		self.thumburl.insert(0, Config.THUMBNAIL_URL)
		self.thumburl.pack(side=LEFT, expand=1, fill=X, padx=2, pady=2)
 
		# Highlighted puff color
		group = Pmw.Group(g, tag_text='Puff highlight color')
		group.pack(side=TOP, fill=BOTH, expand=1, padx=2, pady=2)
		self.newcolor = Config.HIGHLIGHT
		self.colorbutton = Button(group.interior(), text='Choose color',
			background=Config.HIGHLIGHT, command=self.getcolor)
		self.colorbutton.pack(expand=0, fill=X)

		# Save width on exit
		self.save_width_var = BooleanVar()
		self.save_width_var.set(Config.SAVE_PANE_WIDTH)
		button = Checkbutton(g, text='Save window width on exit',
			variable=self.save_width_var, onval=1, offval=0, anchor='w')
		button.pack(fill=BOTH, expand=1, padx=2, pady=2)

		# Autocomplete category strings
		self.autocomplete_var = BooleanVar()
		self.autocomplete_var.set(Config.AUTOCOMPLETE)
		button = Checkbutton(g, text='Autocomplete location strings',
			variable=self.autocomplete_var, onval=1, offval=0,
			anchor='w')
		button.pack(fill=BOTH, expand=1, padx=2, pady=2)


	def getcolor(self):
		color = tkColorChooser.askcolor(initialcolor=self.newcolor)[1]
		if color is not None:
			self.newcolor = color
			self.colorbutton['background'] = self.newcolor

	def ok(self, *args):
		# TODO: check for valid scrollback value
		Config.BROADCAST_PRESENCE = self.presence_var.get()
		Config.SENDER = self.sender.get()
		new_signer = self.signer.get()
		if new_signer != Config.SIGNER:
			Config.SIGNER = self.signer.get()
			self._master.create_akd_server(reconnect=0)
		Config.PERSIST_INTERVAL = string.atoi(self.persist.get())
		Config.CC_SELF = self.cc_var.get()
		Config.SCROLLBACK = string.atoi(self.scrollback.get())
		Config.HIGHLIGHT = self.newcolor
		Config.SAVE_PANE_WIDTH = self.save_width_var.get()
		Config.URLLOAD = self.urlcommand.get()
		Config.MAP_PUFFBUTTONS = self.map_var.get()
		Config.KEYWORD_PROMPT = self.keyw_var.get()
		Config.SHOW_THUMBS = self.thumb_var.get()
		Config.THUMBNAIL_URL = self.thumburl.get()
		Config.AUTOCOMPLETE = self.autocomplete_var.get()

		# Updates caused by these changes
		self._master.update_highlight()
		self._master.update_autocomplete()

		# Save configuration and go away
		Config.save(FUGUDIR)
		self.destroy()
	
	def toggle_mapbuttons(self, *args):
		if self.map_var.get():
			self._master.puffbuts.pack(side=TOP, fill=X, expand=0,
				padx=2, before=self._master._puffwidget)
		else:
			self._master.puffbuts.forget()

	def toggle_keywords(self, *args):
		if self.keyw_var.get():
			self._master.show_keywords()
		else:
			self._master.hide_keywords()

class LogWin(Toplevel):
	def __init__(self, master=None):
		Toplevel.__init__(self)
		self._master = master
		self.protocol('WM_DELETE_WINDOW', self.withdraw)

		l = Label(self, text='Gale notices and warnings:', anchor='w')
		l.pack(side=TOP, expand=0, fill=X)

		self.text = Pmw.ScrolledText(self)
		self.text.pack(side=TOP, expand=1, fill=BOTH)
		self.text.configure(text_state='disabled')

		c = Button(self, text='Close', command=self.withdraw)
		c.pack(side=TOP, expand=0, fill=X)

	def add_message(self, msg):
		self.text.configure(text_state='normal')
		self.text.insert('end', msg + '\n')
		# delete old messages
		while int(float(self.text.index('end'))) >= LOG_MESG_BUFFER + 3:
			self.text.delete('1.0', '2.0')
		self.text.see('end')
		self.text.configure(text_state='disabled')

class ScreenPop(Toplevel):
	def __init__(self, master=None):
		Toplevel.__init__(self)
		if master:
			if hasattr(self, 'wm_transient'):
				self.wm_transient(master)
			elif hasattr(self, 'transient'):
				self.transient(master)
		self.protocol('WM_DELETE_WINDOW', self.withdraw)
		self._master = master
		self.oldsel = None

		close = Button(self, text='Close', command=self.close)
		close.pack(side=BOTTOM, expand=0, fill=X, padx=2, pady=2)

		f = Frame(self)
		f.pack(side=LEFT, expand=1, fill=BOTH)
		label = Label(f, text='Screens:', anchor='w')
		label.pack(side=TOP, fill=X, expand=0, padx=2, pady=2)
		listbox = Pmw.ScrolledListBox(f, selectioncommand=self.select,
			listbox_selectmode='single', listbox_takefocus=0,
			listbox_exportselection=0)
		listbox.pack(side=LEFT, fill=BOTH, expand=1, padx=2, pady=2)
		self.listbox = listbox

		for c in Config.SUBLIST:
			listbox.insert('end', c['name'])
		
		# Manipulation of screen list
		g = Frame(f)
		g.pack(side=LEFT, expand=0, fill=BOTH)
		addbut = Button(g, text='New screen', command=self.new)
		addbut.pack(side=TOP, expand=0, fill=X, padx=2, pady=2)
		cpbut = Button(g, text='Copy screen', command=self.copy)
		cpbut.pack(side=TOP, expand=0, fill=X, padx=2, pady=2)
		delbut = Button(g, text='Delete screen', command=self.delete)
		delbut.pack(side=TOP, expand=0, fill=X, padx=2, pady=2)
		upbut = Button(g, text='Move up', command=self.up)
		upbut.pack(side=TOP, expand=0, fill=X, padx=2, pady=2)
		downbut = Button(g, text='Move down', command=self.down)
		downbut.pack(side=TOP, expand=0, fill=X, padx=2, pady=2)

		# Options per screen
		# Name and subscription
		h = Frame(self, relief='groove', borderwidth=2)
		h.pack(side=TOP, expand=1, fill=BOTH, padx=2, pady=2)
		i = Frame(h)
		i.pack(side=TOP, expand=0, fill=X, padx=2, pady=2)
		label = Label(i, text='Name:', anchor='w')
		label.pack(side=LEFT, expand=0, fill=X)
		self.namevar = StringVar()
		self.name_change_cb = self.namevar.trace('w', self.name_change)
		name = Entry(i, textvariable=self.namevar)
		name.pack(side=LEFT, expand=0, fill=X)
		group = Pmw.Group(h, tag_text = 'Subscribe to:')
		group.pack(side=TOP, expand=1, fill=BOTH, padx=2, pady=2)
		self.sub = Pmw.ScrolledText(group.interior(), text_width=40,
			text_height=5, text_wrap='word',
			text_font=FONTS['TEXT_FONT'])
		self.sub.pack(side=TOP, expand=1, fill=BOTH, padx=2, pady=2)
		# Whether to save puff history
		self.savevar = BooleanVar()
		save = Checkbutton(h, text='Save puff history on exit',
			variable=self.savevar, anchor='w')
		save.pack(side=TOP, expand=0, fill=X, padx=2, pady=2)
		# Whether to send return receipts
		self.recptvar = BooleanVar()
		recpt = Checkbutton(h, text='Send return receipts to private puffs',
			variable=self.recptvar, anchor='w')
		recpt.pack(side=TOP, expand=0, fill=X, padx=2, pady=2)
		# Log sent puffs
		LOG_CHOICES = ['Nothing', 'Log private to screen',
			'Log all to screen']
		self.log = Pmw.OptionMenu(h,
			label_text='Sent puff handling:',
			labelpos = 'w',
			items=LOG_CHOICES)
		self.log.pack(side=TOP, expand=0, fill=X, padx=2, pady=2)

		# On new puff
		group = Pmw.Group(h, tag_text = 'On new puff:')
		group.pack(side=TOP, expand=0, fill=X, padx=2, pady=2)
		f = group.interior()
		f.columnconfigure(0, weight=1)
		f.columnconfigure(1, weight=1)
		f.rowconfigure(0, weight=1)
		f.rowconfigure(1, weight=1)
		f.rowconfigure(2, weight=1)

		self.beepvar = BooleanVar()
		beep = Checkbutton(group.interior(), text='Beep',
			variable=self.beepvar, anchor='w')
		beep.grid(row=0, column=0, padx=2, pady=2, sticky='we')

		self.highlightvar = BooleanVar()
		highlight = Checkbutton(group.interior(), text='Highlight button',
			variable=self.highlightvar, anchor='w')
		highlight.grid(row=0, column=1, padx=2, pady=2, sticky='we')

		self.liftvar = BooleanVar()
		lift = Checkbutton(group.interior(), text='Lift screen to top',
			variable=self.liftvar, anchor='w')
		lift.grid(row=1, column=0, padx=2, pady=2, sticky='we')

		self.deiconvar = BooleanVar()
		deicon = Checkbutton(group.interior(), text='Deiconify window',
			variable=self.deiconvar, anchor='w')
		deicon.grid(row=1, column=1, padx=2, pady=2, sticky='we')

		self.cmdvar = BooleanVar()
		cmdgrp = Pmw.Group(group.interior(), tag_text = 'Run command:',
			tag_pyclass=Checkbutton, tag_variable=self.cmdvar,
			tag_command=self.toggle_cmd)
		cmdgrp.grid(row=2, column=0, columnspan=2,
			padx=2, pady=2, sticky='we')
		self.cmd = Entry(cmdgrp.interior())
		self.cmd.pack(fill=X, padx=2, pady=2)
		self.cmd['state'] = 'disabled'

		# Initialize listbox selection
		self.listbox.selection_set(0)
		self.select()
	
	def set_screen(self, index):
		self.listbox.component('listbox').selection_clear(0, 'end')
		self.listbox.selection_set(index)
		self.select()
	
	def name_change(self, name1, name2, op):
		index = self.get_sel_as_index()
		conf = self.get_sel_as_conf()
		name = self.namevar.get()
		self.listbox.delete(index)
		self.listbox.insert(index, name)
		self.listbox.component('listbox').selection_clear(0, 'end')
		self.listbox.selection_set(index)
		conf['name'] = name
		self._master.change_button_name(index, name)
		self._master.regen_screenmenu()
	
	def toggle_cmd(self):
		if self.cmdvar.get():
			self.cmd['state'] = 'normal'
		else:
			self.cmd['state'] = 'disabled'

	def up(self):
		index = self.get_sel_as_index()
		if index is None: return
		if index == 0:
			self.bell()
			return
		sel = Config.SUBLIST[index]
		self.save_current_values(sel)
		self.listbox.delete(index)
		self.listbox.insert(index-1, sel['name'])
		self.listbox.component('listbox').selection_clear(0, 'end')
		self.listbox.selection_set(index-1)
		Config.SUBLIST[index:index+1] = []
		Config.SUBLIST.insert(index-1, sel)
		self.select()
		self._master.move_button_up(index)
		self._master.regen_screenmenu()
	
	def down(self):
		index = self.get_sel_as_index()
		if index is None: return
		if index == len(self.listbox.get(0, 'end'))-1:
			self.bell()
			return
		sel = Config.SUBLIST[index]
		self.save_current_values(sel)
		self.listbox.delete(index)
		self.listbox.insert(index+1, sel['name'])
		self.listbox.component('listbox').selection_clear(0, 'end')
		self.listbox.selection_set(index+1)
		Config.SUBLIST[index:index+1] = []
		Config.SUBLIST.insert(index+1, sel)
		self.select()
		self._master.move_button_down(index)
		self._master.regen_screenmenu()

	def new(self):
		new = connection.Connection('New screen', 'pub@%s' %
			pygale.gale_domain())
		Config.SUBLIST.append(new)
		text = self._master.make_text_widget()
		new.setwidget(self._master, text)
		new.connect()
		text.insert(1.0, new['sub'] + '\n')
		self._master.add_button(new)
		self._master.regen_screenmenu()
		self.listbox.insert('end', new['name'])
		self.listbox.component('listbox').selection_clear(0, 'end')
		self.listbox.selection_set('end')
		self.select()
	
	def copy(self):
		sel = self.get_sel_as_conf()
		if sel is None: return
		new = connection.Connection(sel['name'] + ' copy', sel['sub'])
		Config.SUBLIST.append(new)
		text = self._master.make_text_widget()
		text.insert(1.0, new['sub'] + '\n')
		new.setwidget(self._master, text)
		new.connect()
		self._master.add_button(new)
		self._master.regen_screenmenu()
		self.listbox.insert('end', new['name'])
		self.listbox.component('listbox').selection_clear(0, 'end')
		self.listbox.selection_set('end')
		self.select()
	
	def delete(self):
		index = self.get_sel_as_index()
		delwindow = Config.SUBLIST[index]
		if len(Config.SUBLIST) == 1:
			m = tkMessageBox.showerror(
				title="Error: can't delete last screen",
				parent=self,
				message='At least one screen must be defined.')
			return

		# Upcall into parent if this is currently displayed
#		print 'Checking for currently displayed window'
#		print 'Delwindow is', delwindow
#		print 'current screen index:', self._master._curscreen
#		print 'current screen:', Config.SUBLIST[self._master._curscreen]
		if self._master.is_curscreen(delwindow):
			if index == 0:
				switchindex = 1
			else:
				switchindex = index - 1
#			print 'Asking master to switch to screen', switchindex
			self._master.switch_screen(switchindex)
#			print 'curscreen is now', self._master._curscreen
#		else:
#			print '... Not currently displayed'

		Config.SUBLIST[index].disconnect()
		del Config.SUBLIST[index]
		self.listbox.delete(index)
		self.listbox.component('listbox').selection_clear(0, 'end')
		self._master.del_button(index)
		self._master.regen_screenmenu()
		if index >= len(self.listbox.get(0, 'end')):
			index = len(self.listbox.get(0, 'end')) - 1
		self.listbox.selection_set(index)
		self.select()
	
	def get_sel_as_index(self):
		selection = map(string.atoi, self.listbox.curselection())
		if len(selection) == 0: return None
		return selection[0]

	def get_sel_as_conf(self):
		selindex = self.get_sel_as_index()
		if selindex is None: return None
		return Config.SUBLIST[selindex]

	def save_current_values(self, conf):
		# Save old values
		reconnect = 0
		if conf['sub'] != self.sub.get(1.0, 'end-1c'):
			reconnect = 1
		conf['name'] = self.namevar.get()
		conf['sub'] = self.sub.get(1.0, 'end-1c')
		conf['beep'] = self.beepvar.get()
		conf['highlight'] = self.highlightvar.get()
		conf['lift'] = self.liftvar.get()
		conf['deiconify'] = self.deiconvar.get()
		conf['savepuffs'] = self.savevar.get()
		conf['send_receipts'] = self.recptvar.get()
		if Pmw.version() > '0.8.1':
			conf['logsent'] = \
				self.log.index(self.log.getcurselection())
		else:
			conf['logsent'] = self.log.index(self.log.get())
		if self.cmdvar.get():
			conf['command'] = self.cmd.get()
		else:
			conf['command'] = None
		if reconnect:
			conf.resubscribe()

	def select(self):
		sel = self.get_sel_as_conf()
		if sel is None: return
		if self.oldsel is not None and self.oldsel != sel:
			self.save_current_values(self.oldsel)
		self.namevar.trace_vdelete('w', self.name_change_cb)
		self.namevar.set(sel['name'])
		self.name_change_cb = self.namevar.trace('w', self.name_change)
		self.sub.delete(1.0, 'end')
		self.sub.insert('end', sel['sub'])
		self.beepvar.set(sel['beep'])
		self.highlightvar.set(sel['highlight'])
		self.liftvar.set(sel['lift'])
		self.deiconvar.set(sel['deiconify'])
		self.savevar.set(sel['savepuffs'])
		self.recptvar.set(sel['send_receipts'])
		self.log.invoke(sel['logsent'])
		self.cmd['state'] = 'normal'
		self.cmd.delete(0, 'end')
		if sel['command'] is None:
			self.cmdvar.set(0)
			self.cmd['state'] = 'disabled'
		else:
			self.cmdvar.set(1)
			self.cmd.insert('end', sel['command'])
		self.oldsel = sel
	
	def close(self):
		sel = self.get_sel_as_conf()
		if sel is not None:
			self.save_current_values(sel)
		if len(Config.SUBLIST) == 0:
			m = tkMessageBox.showerror(
				title='Error: no screens',
				parent=self,
				message='At least one screen must be defined.')
			return

		name_collisions = {}
		for conn in Config.SUBLIST:
			if name_collisions.has_key(conn['name']):
				m = tkMessageBox.showerror(
					title='Error: duplicate screen names',
					parent=self,
					message='No two screens may have the same name.')
				return
			name_collisions[conn['name']] = 1
		
		# Save configuration and go away
		Config.save(FUGUDIR)
		self.withdraw()

def usage():
	print 'Fugu version %s' % FUGU_VERSION
	print 'usage: %s [-d]' % sys.argv[0]
	print 'flags: -d               Enable debugging features'
	sys.exit(0)

def main():
	pygale.init()
	sys.exitfunc = pygale.shutdown

	# Command line options
	try:
		opts, args = getopt.getopt(sys.argv[1:], 'dhD:')
	except getopt.GetoptError:
		usage()
	global DEBUG

	for (opt, val) in opts:
		if opt == '-d':
			DEBUG = DEBUG + 1
			if DEBUG > 3: DEBUG = 3
		elif opt == '-h':
			usage()
		elif opt == '-D':
			try:
				module = eval(val)
			except NameError:
				print 'Unknown module:', val
			else:
				if not DEBUG:
					DEBUG = 1
				module.DEBUG = DEBUG

	# Fugu directory
	global FUGUDIR
	FUGUDIR = pygale.gale_env.get('FUGU_DIR',
		os.path.join(userinfo.home_dir, '.gale'))
	thumbcache.FUGUDIR = FUGUDIR
	global Config
	Config = config.ConfigClass()
	# Load configuration
	Config.load(FUGUDIR)
	Config.load_pufflog(FUGUDIR)
	# Propagate configuration everywhere it has to go
	thumbcache.Config = Config
	connection.Config = Config
	root = Tk(None, None, "Fugu")
	Pmw.initialise(root)
	if DEBUG and sys.version >= '2.0':
		import gc
		print 'Setting debugging GC'
		gc.set_debug(gc.DEBUG_LEAK)
	m = GaleWin(root)
	m.mainloop()

if __name__ == '__main__':
	main()

#vim:ts=5:sw=5:sts=5:
