#!/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-.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('', self.drop_presence) self.presence_box.component('entry').bind('', self.drop_presence) # These two don't work yet self.presence_box.component('entry').bind('', self.tabForward, add=1) #self.presence_box.component('listbox').bind('', # 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('', self.url_store) text.component('text').bind('', 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('', self.page_up) text.component('text').bind('', 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', '', lambda e, s=self, t=text: s.set_text_cursor(t, 'hand2')) text.tag_bind('URL', '', lambda e, s=self, t=text: s.set_text_cursor(t)) text.tag_bind('URL', '', self.url_store) text.tag_bind('URL', '', 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('', self.control_return) self._puffwidget.bind('', lambda e, s=self: s.clearpuff()) self._puffwidget.bind('', lambda e, s=self: s._detachbutton.invoke()) self._puffwidget.bind('', lambda e, s=self: s.reply()) self._puffwidget.bind('', lambda e, s=self: s.reply_to_sender()) self._puffwidget.bind('', lambda e, s=self: s.key_up(), text=0) self._puffwidget.bind('', lambda e, s=self: s.key_down(), text=0) self._puffwidget.bind('', self.author_search) self._puffwidget.bind('', lambda e, s=self: s.master.iconify()) self._puffwidget.bind('', self.search_pop) self._puffwidget.bind('', self.home) self._puffwidget.bind('', self.end) # Focus presence widget self._puffwidget.bind('', lambda e, s=self: s.presence_box.component('entry').focus_set()) # These bindings only work in the entry fields self._puffwidget.bind('', self.next_thread, text=0) self._puffwidget.bind('', self.prev_thread, text=0) self._puffwidget.bind('', self.page_up, text=0) self._puffwidget.bind('', self.page_down, text=0) # Mouse button bindings have to be bound to the master frame self.master.bind('', self.page_up) self.master.bind('', self.page_down) # Alt-up/down always move the puff cursor self._puffwidget.bind('', self.key_up) self._puffwidget.bind('', self.key_down) self._puffwidget.bind('', self.page_up) self._puffwidget.bind('', self.page_down) # Cut the reference to the cursor self._puffwidget.bind('', self.cite_puff) # This next line causes problems; it binds to all entry widgets, # which it oughtn't do #self.master.bind_class('Entry', '', 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', '', self.ret_break, add=1) # self.master.bind_class('Text', '', self.ret_break, add=1) # self.master.bind_class('Text', '', self.ret_break, add=1) # self.master.bind_class('Text', '', self.ret_break, add=1) # self.master.bind_class('Text', '', self.ret_break, add=1) # self.master.bind_class('Text', '', self.ret_break, add=1) # self.master.bind_class('Text', '', self.ret_break, add=1) # self.master.bind_class('Text', '', self.ret_break, add=1) # self.master.bind_class('Text', '', self.ret_break, add=1) # Switching between screens with C-N where 1 <= N <= 9 for i in range(1, 10): self._puffwidget.bind('' % 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('', lambda e, s=self, p=self._location: s.drop_presence(p)) self._location.component('entry').bind('', lambda e, s=self, p=self._location: s.drop_presence(p)) self._location.component('entry').bind('', 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('', lambda e, s=self, p=self._keywords: s.drop_presence(p)) self._keywords.component('entry').bind('', lambda e, s=self, p=self._keywords: s.drop_presence(p)) self._keywords.component('entry').bind('', 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('', 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('', '<>', loc=0) self.bind_generate('', '<>', 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('', self.control_return) self.bind('', 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: