[27576] in Source-Commits
xdsc commit: Initial checkin of Gtk rewrite
daemon@ATHENA.MIT.EDU (Jonathan D Reed)
Mon Feb 3 15:59:26 2014
Date: Mon, 3 Feb 2014 15:59:18 -0500
From: Jonathan D Reed <jdreed@MIT.EDU>
Message-Id: <201402032059.s13KxI7P010517@drugstore.mit.edu>
To: source-commits@MIT.EDU
https://github.com/mit-athena/xdsc/commit/fa8c9f2975c7bc705b3ef7d142261acc41f7b45a
commit fa8c9f2975c7bc705b3ef7d142261acc41f7b45a
Author: Jonathan Reed <jdreed@mit.edu>
Date: Sat Nov 23 12:35:48 2013 -0500
Initial checkin of Gtk rewrite
- Rewrite xdsc in Python/Gtk3
- Provide the Glade UI file for the new xdsc
- Provide an icon for xdsc (stolen from diswww)
xdsc.py | 1080 +++++++++++++++++++++++++++++++++++++++++++++
xdsc.ui | 1362 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
xdsc_icon.gif | Bin 0 -> 471 bytes
3 files changed, 2442 insertions(+), 0 deletions(-)
diff --git a/xdsc.py b/xdsc.py
new file mode 100755
index 0000000..e816671
--- /dev/null
+++ b/xdsc.py
@@ -0,0 +1,1080 @@
+#!/usr/bin/python
+
+import errno
+import logging
+import os
+import pwd
+import re
+import shutil
+import signal
+import socket
+import subprocess
+import sys
+import tempfile
+import time
+from email.mime.text import MIMEText
+from optparse import OptionParser
+from xml.etree import ElementTree
+
+from gi.repository import Gtk, GObject, Pango, GLib, Gdk
+
+import discuss
+
+dslogger = logging.getLogger('xdsc.discuss')
+uilogger = logging.getLogger('xdsc.ui')
+
+DEFAULT_UI_FILE="/usr/share/debathena-xdsc/xdsc.ui"
+DEFAULT_ICON_FILE="/usr/share/debathena-xdsc/xdsc_icon.gif"
+DEFAULT_DISCUSS_EDITOR="gedit"
+DEFAULT_TIMEOUT=3
+
+def gtk_tree_model_get_iter_last(tree_model):
+ """
+ Convenience function to return a GtkTreeIter on the last row of the model.
+ """
+ path_row = 0
+ if len(tree_model):
+ path_row = len(tree_model) - 1
+ tree_path = Gtk.TreePath.new_from_string(str(path_row))
+ tree_iter = tree_model.get_iter(tree_path)
+
+class DiscussWrapper:
+ """
+ Abstraction for the discuss interface.
+ """
+
+ reply_action = "Replying to {trn.meeting.short_name}[{trn.current}]..."
+ new_action = "New transaction in '{mtg.long_name}'..."
+
+ reply_header = "\n\nOn {date}, {trn.author} ({signature}) said in " \
+ "{trn.meeting.short_name}[{trn.current}]:\n"
+
+ transaction_header = """\
+### xdsc: Enter your transaction on the lines below these instructions.
+### xdsc: When you're done, save the file, and quit this editor.
+### xdsc: An empty file will abort the operation and no post will be created.
+### xdsc: These instructions will be removed before posting.
+### xdsc:
+### xdsc: {action}
+### xdsc:
+### xdsc: Begin your transaction on the line below this one.
+
+"""
+
+ def __init__(self, timeout):
+ self.meetingcache = {}
+ self.connectioncache = {}
+ self.timeout = timeout
+ self.rcfile = discuss.RCFile()
+ self.meetings = self.rcfile.entries
+
+ def _get_connection(self, host):
+ if host not in self.connectioncache:
+ conn = discuss.Client(host, timeout=self.timeout)
+ self.connectioncache[host] = conn
+ return self.connectioncache[host]
+
+ def get_meeting(self, name):
+ """
+ Load the meeting, cache it, and return it.
+ N.B. We can't cache connections (discuss.Client) otherwise
+ hilarity ensues.
+ """
+ dslogger.debug('get_meeting %s', name)
+ location = self.rcfile.lookup(name)
+ if location not in self.meetingcache:
+ mtg = discuss.Meeting(self._get_connection(location[0]),
+ location[1])
+ try:
+ mtg.check_update(0)
+ except discuss.client.DiscussError as e:
+ # Because transaction 0 could have been deleted.
+ # But we can't actually check mtg.lowest because if
+ # the meeting doesn't exist, load_info() will fail.
+ if e.code != discuss.constants.NO_SUCH_TRN:
+ raise
+ mtg.load_info()
+ self.meetingcache[location] = mtg
+ return self.meetingcache[location]
+
+ def add_meeting(self, host, path):
+ cli = discuss.Client(host)
+ mtg = discuss.Meeting(cli, path)
+ self.rcfile.add(mtg)
+ self.rcfile.save()
+
+ def delete_meeting(self, meeting):
+ mtg = self.rcfile.lookup(meeting)
+ if mtg is None:
+ raise ValueError(
+ '"{0}" is not in your .meetings file.'.format(meeting)
+ )
+ del self.rcfile.entries[mtg]
+ self.rcfile.save()
+ self.rcfile.recache()
+
+ def _last_read_transaction(self, meeting_obj):
+ lookup = self.rcfile.lookup(meeting_obj.long_name)
+ entry = self.rcfile.entries[lookup]
+ return entry['last_transaction']
+
+ def meeting_has_changed(self, meeting_obj):
+ last_trans = self._last_read_transaction(meeting_obj)
+ return meeting_obj.check_update(last_trans)
+
+ def touch_meeting(self, meeting_obj, trn_obj):
+ self.rcfile.touch(meeting_obj.id, trn_obj.current)
+
+ def get_transaction(self, meeting_obj, trn_num, updateLast=True):
+ dslogger.debug("Retrieving %s[%d]", meeting_obj.short_name, trn_num)
+ if trn_num < meeting_obj.lowest or trn_num > meeting_obj.highest:
+ raise ValueError("Transaction number out of range for meeting.")
+ trn_obj = meeting_obj.get_transaction(trn_num)
+ if updateLast:
+ self.touch_meeting(meeting_obj, trn_obj)
+ return trn_obj
+
+ def find_next_valid_transaction(self, meeting_obj, backwards=False):
+ start_from = self._last_read_transaction(meeting_obj)
+ if not backwards and start_from >= meeting_obj.last:
+ self.go_to_transaction(meeting_obj.last)
+ if backwards and start_from <= meeting_obj.first:
+ self.go_to_transaction(meeting_obj.first)
+ end = meeting_obj.first if backwards else meeting_obj.last
+ increment = -1 if backwards else 1
+ for t in xrange(start_from + increment,
+ end + increment,
+ increment):
+ dslogger.debug("Trying transaction #%d in %s",
+ t, meeting_obj.long_name)
+ try:
+ return self.get_transaction(meeting_obj, t)
+ except discuss.DiscussError as err:
+ if err.code in (discuss.constants.DELETED_TRN,
+ discuss.constants.NO_SUCH_TRN):
+ continue
+ else:
+ raise
+
+ @staticmethod
+ def format_transaction_for_list(trn):
+ assert trn is not None
+ line = u" [{trn.number}]{flags} {num_lines:>6}" \
+ u" {date} {author:<16} {subject:.60}"
+ markup = line.format(trn=trn,
+ flags='F' if trn.flags else u' ',
+ author=trn.author.split('@')[0],
+ num_lines="({0})".format(trn.num_lines),
+ subject=unicode(trn.subject, errors='replace'),
+ date=trn.date_entered.strftime("%m/%d/%y %H:%M"))
+ return GLib.markup_escape_text(markup.encode('UTF-8', errors='ignore'))
+
+ @staticmethod
+ def format_transaction(trn):
+ assert trn is not None
+ text = u''
+ header = u"[{trn.number}]{flags} {trn.author} ({signature}) " \
+ u"{trn.meeting.long_name} {date} ({trn.num_lines} lines)\n" \
+ u"Subject: {subject}\n"
+ footer = u"--[{trn.number}]--\n\n"
+ text += header.format(trn=trn,
+ flags='F' if trn.flags else ' ',
+ signature=unicode(trn.signature,
+ errors='replace'),
+ subject=unicode(trn.subject, errors='replace'),
+ date=trn.date_entered.strftime("%m/%d/%y %H:%M"))
+ text += unicode(trn.get_text(), errors='replace')
+ text += footer.format(trn=trn)
+ return text
+
+class Xdsc:
+ # A mapping of widget id to font description string
+ default_fonts_map = {'help_textview': 'Courier 8',
+ 'transaction_textview': 'Courier 8',
+ 'upper_treeview': 'Courier 8'}
+
+ # A mapping of menus to the buttons they pop down from
+ # for positioning.
+ menubuttons_map = {'configure_menu': 'configure_button',
+ 'mode_menu': 'mode_button',
+ 'show_menu': 'show_button',
+ 'goto_menu': 'goto_button',
+ 'enter_menu': 'enter_button',
+ 'write_menu': 'write_button'}
+
+ def __init__(self, ui_file, icon_file, dsc_wrapper):
+ self.builder = Gtk.Builder()
+ try:
+ self.builder.add_from_file(ui_file)
+ except GLib.GError as e:
+ sys.exit("Unable to load UI file: " + str(e))
+ # GtkBuilder still scribbles over the id/name properties
+ # We could get away without this by doing things in the handlers
+ # like "if self.builder.get_object(name) is widget: [...]" but meh
+ for object_id in [x.get('id') for x in
+ ElementTree.parse(ui_file).findall('.//object')]:
+ if isinstance(self.builder.get_object(object_id), Gtk.Widget):
+ self.builder.get_object(object_id).set_name(object_id)
+ # Set some font defaults
+ for widget_id in self.default_fonts_map:
+ pangofont = Pango.FontDescription(self.default_fonts_map[widget_id])
+ self.builder.get_object(widget_id).override_font(pangofont)
+ self.builder.connect_signals(self)
+ self.main_window = self.builder.get_object('xdsc_main_window')
+ try:
+ self.main_window.set_icon_from_file(icon_file)
+ except GLib.GError as e:
+ print >>sys.stderr, "Failed to set icon file for window: ", e
+ self.upper_treeview = self.builder.get_object('upper_treeview')
+ self.meeting_liststore = self.builder.get_object("meeting_list_store")
+ self.trans_liststore = self.builder.get_object("transaction_list_store")
+ # The 'Show' button is not valid in meeting mode
+ self.builder.get_object('show_button').set_sensitive(False)
+ self.discuss = dsc_wrapper
+ dlg = Gtk.MessageDialog(self.main_window, 0,
+ Gtk.MessageType.INFO,
+ Gtk.ButtonsType.NONE,
+ "Loading meeting information, please wait...")
+ dlg.show()
+ while Gtk.events_pending():
+ Gtk.main_iteration()
+ self.check_meetings()
+ self.current_meeting = None
+ self.current_transaction = None
+ self.update_meeting_list()
+ dlg.destroy()
+ self.upper_treeview.grab_focus()
+
+ def check_meetings(self):
+ """
+ Ensure that the long names in the file match what the server
+ thinks they are. They can -- and do -- change. The short
+ names cannot change without the path changing, which would
+ render the meeting useless. This also has the benefit that
+ we visit every meeting and remove invalid values.
+ """
+ update = False
+ for m in self.discuss.meetings:
+ if self.discuss.meetings[m]['deleted']:
+ continue
+ try:
+ mtg = self.discuss.get_meeting(m)
+ except socket.timeout as e:
+ self.msg_dialog("Unable to attend '{0}' on '{1}' ({2}).\n"
+ "It will be flagged as 'deleted' in your "
+ "meetings file. You may wish to clean up the "
+ "file later.",
+ self.discuss.meetings[m]['displayname'],
+ self.discuss.meetings[m]['hostname'],
+ e, warn=True)
+ self.discuss.meetings[m]['deleted'] = True
+ update = True
+ continue
+ except discuss.client.DiscussError as e:
+ dslogger.debug('%s: %s', ':'.join(m), e)
+ if e.code == discuss.constants.NO_SUCH_MTG:
+ self.msg_dialog("Meeting '{0}' on '{1}' no longer exists. "
+ "It will be flagged as 'deleted' in your "
+ "meetings file. You may wish to clean up "
+ "the file later.",
+ self.discuss.meetings[m]['displayname'],
+ self.discuss.meetings[m]['hostname'],
+ e, warn=True)
+ self.discuss.meetings[m]['deleted'] = True
+ update = True
+ continue
+ # We catch these errors when populating the liststore
+ except discuss.rpc.ProtocolError as e:
+ dslogger.debug('%s: %s', ':'.join(m), e)
+ # We catch these errors when populating the liststore
+ continue
+ if mtg.long_name != self.discuss.meetings[m]['names'][0]:
+ dslogger.debug("Updating long name from '%s' to '%s'",
+ self.discuss.meetings[m]['names'][0],
+ mtg.long_name)
+ update = True
+ self.discuss.meetings[m]['names'][0] = mtg.long_name
+ if update:
+ self.discuss.rcfile.save()
+ self.discuss.rcfile.recache()
+
+ def quit(self):
+ """
+ Quit the application, ideally saving the RC file.
+ """
+ self.discuss.rcfile.save()
+ Gtk.main_quit()
+
+ def in_transaction_mode(self):
+ """
+ Return True if in transaction mode, else meeting mode
+ """
+ return self.upper_treeview.get_model() is self.trans_liststore
+
+ def change_meeting(self, meeting_obj):
+ if self.current_meeting is meeting_obj:
+ return True
+ self.trans_liststore.clear()
+ self.builder.get_object('transaction_buffer').set_text('')
+ self.builder.get_object('next_chain_button').set_sensitive(False)
+ self.builder.get_object('prev_chain_button').set_sensitive(False)
+ self.builder.get_object('write_button').set_sensitive(False)
+ self.current_meeting = meeting_obj
+ last_trans = self.discuss._last_read_transaction(self.current_meeting)
+ try:
+ self.current_transaction = self.discuss.get_transaction(
+ self.current_meeting, last_trans)
+ except (ValueError, discuss.DiscussError) as e:
+ dslogger.debug('Error: %s', e)
+ errdetail = "Something bad happened."
+ if isinstance(e, ValueError):
+ errdetail = "The last read transaction ({0}) in {1} was " \
+ "outside the range for the meeting."
+ elif e.code == discuss.constants.DELETED_TRN:
+ errdetail = "The last read transaction ({0}) in {1} has "\
+ "been deleted."
+ elif e.code == discuss.constants.NO_SUCH_TRN:
+ errdetail = "The last read transaction ({0}) in {1} does "\
+ "not exist."
+ err = errdetail + \
+ " Your current transaction will be updated " \
+ "to the next unread one, or the last transaction in " \
+ "the meeting if there are no more unread transactions."
+ self.msg_dialog(err, last_trans,
+ self.current_meeting.long_name, info=True)
+ self.display_transaction(
+ self.discuss.find_next_valid_transaction(
+ self.current_meeting), by_object=True)
+ self.builder.get_object('enter_reply').set_sensitive(
+ 'a' in self.current_meeting.access_modes)
+ self.builder.get_object('enter_new_transaction').set_sensitive(
+ 'w' in self.current_meeting.access_modes)
+ self.builder.get_object('enter_button').set_sensitive(
+ self.builder.get_object('enter_reply').get_sensitive() or
+ self.builder.get_object('enter_new_transaction').get_sensitive())
+ self.update_status_label(mtg=self.current_meeting)
+
+ def update_meeting_list(self):
+ uilogger.debug('Clearing liststore')
+ self.meeting_liststore.clear()
+ uilogger.debug('Cleared!')
+ meetings = self.discuss.meetings
+ for m in meetings:
+ if meetings[m]['deleted']:
+ continue
+ try:
+ mtg = self.discuss.get_meeting(m)
+ except (discuss.rpc.ProtocolError,
+ discuss.client.DiscussError,
+ socket.timeout) as e:
+ self.msg_dialog("Error while attending {0}:\n{1}\n\n"
+ "The meeting will be temporarily removed from "
+ "your list of meetings.",
+ meetings[m]['displayname'],
+ e)
+ continue
+ updated = False
+ display_name = ', '.join(meetings[m]['names'])
+ if self.discuss.meeting_has_changed(mtg):
+ updated = True
+ display_name = "<b>%s</b>" % (display_name,)
+ self.meeting_liststore.append((display_name,
+ mtg))
+ uilogger.debug('%d meetings loaded', len(self.meeting_liststore))
+ for widget_id in ('down_button', 'up_button', 'mode_transactions',
+ 'next_button', 'prev_button', 'goto_button',
+ 'enter_button', 'write_button'):
+ self.builder.get_object(widget_id).set_sensitive(
+ len(self.meeting_liststore) > 0)
+ if len(self.meeting_liststore) < 1:
+ self.msg_dialog("No meetings to display.", warn=True)
+ else:
+ next_unread_meeting_path = self._find_unread_meetings()
+ if next_unread_meeting_path is not None:
+ self.upper_treeview.set_cursor(next_unread_meeting_path, None)
+ else:
+ self.upper_treeview.set_cursor(Gtk.TreePath.new_first())
+ self.msg_dialog("Nothing more to read.", info=True)
+
+ def remove_temporary_file(self, filename):
+ try:
+ os.unlink(filename)
+ except OSError as e:
+ self.msg_dialog("Unable to remove '{0}': {1}", filename, e)
+
+ def post_reply(self, replying_to=None):
+ editor = os.getenv('DISCUSS_EDITOR', DEFAULT_DISCUSS_EDITOR)
+ filename = None
+ try:
+ with tempfile.NamedTemporaryFile(prefix='xdsc', delete=False) as f:
+ filename = f.name
+ action = DiscussWrapper.new_action.format(
+ mtg=self.current_meeting)
+ if replying_to is not None:
+ action = DiscussWrapper.reply_action.format(trn=replying_to)
+ f.write(DiscussWrapper.transaction_header.format(action=action))
+ if replying_to is not None:
+ f.write(DiscussWrapper.reply_header.format(
+ date=replying_to.date_entered.strftime(
+ "%m/%d/%y %H:%M"),
+ signature=unicode(replying_to.signature,
+ errors='replace'),
+ trn=replying_to))
+ f.write("> ")
+ f.write(replying_to.get_text().replace("\n", "\n> "))
+ except IOError as e:
+ self.msg_dialog("Unable to create temporary file '{0}': {1}",
+ filename, e)
+ return None
+ if filename is None:
+ # Shouldn't happen.
+ self.msg_dialog("Could not determine temporary file name!")
+ return None
+ rv = None
+ try:
+ rv = subprocess.call([editor, filename])
+ except OSError as e:
+ self.msg_dialog("Unable to launch editor ({0}): {1}",
+ editor, e)
+ self.remove_temporary_file(filename)
+ return None
+ if rv != 0:
+ if self.msg_dialog("Your editor indicated an error (exited with "
+ "non-zero status). That's probably bad. "
+ "Should I delete the temporary file ({0})?",
+ filename, question=True):
+ self.remove_temporary_file(filename)
+ return None
+ body = ''
+ try:
+ with open(filename, 'r') as f:
+ body = ''.join([l for l in f.readlines()
+ if not l.startswith('### xdsc:')])
+ except IOError as e:
+ self.msg_dialog("Unable to read temporary file '{0}': {1}",
+ filename, e)
+ return None
+ if len(body.strip()) == 0:
+ self.msg_dialog("Empty transaction detected. Posting aborted.")
+ self.remove_temporary_file(filename)
+ return None
+ dlg = self.builder.get_object('enter_transaction_dlg')
+ self.builder.get_object('enter_transaction_ok').set_sensitive(False)
+ subj_entry = self.builder.get_object('enter_transaction_subject')
+ sig_entry = self.builder.get_object('enter_transaction_signature')
+ try:
+ default_sig = pwd.getpwuid(os.getuid()).pw_gecos.split(',')[0]
+ except:
+ default_sig = 'Unknown User'
+ sig_entry.set_text(default_sig)
+ if replying_to is not None:
+ subj = u"Re: " + unicode(replying_to.subject, errors='replace')
+ subj_entry.set_text(subj)
+ else:
+ subj_entry.set_text('')
+ subj_entry.grab_focus()
+ response = dlg.run()
+ dlg.hide()
+ if response == Gtk.ResponseType.CANCEL:
+ if self.msg_dialog('Delete temporary file ({0})?',
+ filename, question=True):
+ self.remove_temporary_file(filename)
+ else:
+ new_trn = self.current_meeting.post(body,
+ subj_entry.get_text().strip(),
+ sig_entry.get_text().strip(),
+ 0 if replying_to is None
+ else replying_to.current)
+ self.current_meeting.load_info(force=True)
+ self.display_transaction(new_trn, by_object=True)
+ self.remove_temporary_file(filename)
+
+ def can_send_email(self):
+ """
+ Sanity-check the obvious failures when sending e-mail. This is not
+ designed to be foolproof, it's designed to prevent dumb typos.
+ """
+ to = self.builder.get_object('send_email_to').get_text().strip()
+ sender = self.builder.get_object('send_email_from').get_text().strip()
+ subj = self.builder.get_object('send_email_subject').get_text().strip()
+ return len(to) > 0 and len(subj) > 0 and len(sender) > 0 \
+ and '@' in to and '@' in sender
+
+ def send_email_validate(self, widget):
+ self.builder.get_object('send_email_ok').set_sensitive(
+ self.can_send_email())
+
+ def enter_transaction_validate(self, widget):
+ subj = self.builder.get_object(
+ 'enter_transaction_subject').get_text().strip()
+ sig = self.builder.get_object(
+ 'enter_transaction_signature').get_text().strip()
+ self.builder.get_object('enter_transaction_ok').set_sensitive(
+ len(subj) > 0 and len(sig) > 0)
+
+ def transaction_entry_changed(self, widget):
+ self.builder.get_object('goto_transaction_dialog_ok').set_sensitive(
+ len(widget.get_text().strip()) > 0)
+
+ def transaction_entry_insert_text(self, widget, text, text_len,
+ pointer, data=None):
+ if text_len == 0:
+ return True
+ if re.search(r'\D', text):
+ widget.error_bell()
+ widget.set_icon_from_stock(Gtk.EntryIconPosition.SECONDARY,
+ 'gtk-dialog-warning')
+ widget.set_icon_activatable(Gtk.EntryIconPosition.SECONDARY,
+ False)
+ widget.set_icon_tooltip_text(Gtk.EntryIconPosition.SECONDARY,
+ "Only numbers are allowed")
+ widget.stop_emission('insert-text')
+ else:
+ widget.set_icon_from_stock(Gtk.EntryIconPosition.SECONDARY,
+ None)
+ return True
+
+ def msg_dialog(self, *args, **kwargs):
+ errortext = args[0]
+ if len(args) > 1:
+ errortext = errortext.format(*args[1:])
+ buttons = Gtk.ButtonsType.OK
+ if kwargs.get('fatal', False):
+ buttons = Gtk.ButtonsType.CLOSE
+ dialogtype = Gtk.MessageType.ERROR
+ if kwargs.get('warn', False):
+ dialogtype = Gtk.MessageType.WARNING
+ elif kwargs.get('info', False):
+ dialogtype = Gtk.MessageType.INFO
+ elif kwargs.get('question', False):
+ dialogtype = Gtk.MessageType.QUESTION
+ buttons = Gtk.ButtonsType.YES_NO
+ dialog = Gtk.MessageDialog(self.main_window, 0,
+ dialogtype, buttons, errortext)
+ dialog.set_title(kwargs.get('title', 'Xdsc'))
+ response = dialog.run()
+ dialog.destroy()
+ if buttons == Gtk.ButtonsType.YES_NO:
+ return response == Gtk.ResponseType.YES
+ else:
+ return None
+
+ def transactions_callback(self, cur, total, left):
+ self.update_status_label(remaining=left)
+ while Gtk.events_pending():
+ Gtk.main_iteration()
+
+ def load_more_transactions(self, num):
+ trn = self.trans_liststore.get_value(
+ self.trans_liststore.get_iter_first(), 1)
+ uilogger.debug("loading %d more transaction(s)", num)
+ for _ in xrange(0, num):
+ if trn.prev == 0:
+ uilogger.debug("Hit start of meeting")
+ break
+ trn = self.current_meeting.get_transaction(trn.prev)
+ self.trans_liststore.prepend(
+ (DiscussWrapper.format_transaction_for_list(trn), trn))
+ uilogger.debug("prepending %d", trn.current)
+ while Gtk.events_pending():
+ Gtk.main_iteration()
+
+ def _select_transaction_by_num(self, trn_num,
+ backwards=False, start_path=None):
+ uilogger.debug("selecting %d by number", trn_num)
+ ls = self.trans_liststore
+ tree_iter = ls.get_iter_first()
+ if start_path is not None:
+ tree_iter = ls.get_iter(start_path)
+ elif backwards:
+ tree_iter = gtk_tree_model_get_iter_last(ls)
+ walk_function = ls.iter_previous if backwards else ls.iter_next
+ while tree_iter is not None:
+ if ls.get_value(tree_iter, 1).current == trn_num:
+ self.upper_treeview.set_cursor(ls.get_path(tree_iter), None)
+ break
+ tree_iter = walk_function(tree_iter)
+
+ def display_transaction(self, num_or_obj, by_object=False):
+ try:
+ if by_object:
+ trn = num_or_obj
+ else:
+ trn = self.discuss.get_transaction(self.current_meeting,
+ num_or_obj)
+ text = DiscussWrapper.format_transaction(trn)
+ self.mark_current_meeting_as_changed()
+ self.current_transaction = trn
+ self.builder.get_object('next_chain_button').set_sensitive(
+ trn.nref != 0)
+ self.builder.get_object('prev_chain_button').set_sensitive(
+ trn.pref != 0)
+ self.builder.get_object('write_button').set_sensitive(True)
+ self.builder.get_object('transaction_buffer').set_text(text)
+ self.update_status_label(mtg=self.current_meeting, trn=trn)
+ self.builder.get_object('transaction_textview').grab_focus()
+ except (ValueError, discuss.DiscussError) as e:
+ self.msg_dialog(e)
+
+ def _find_unread_meetings(self, backwards=False, start_path=None):
+ tree_iter = self.meeting_liststore.get_iter_first()
+ if start_path is not None:
+ tree_iter = self.meeting_liststore.get_iter(start_path)
+ elif backwards:
+ tree_iter = gtk_tree_model_get_iter_last(self.meeting_liststore)
+ walk_function = self.meeting_liststore.iter_next
+ if backwards:
+ walk_function = self.meeting_liststore.iter_previous
+ while walk_function(tree_iter) is not None:
+ tree_iter = walk_function(tree_iter)
+ if self.discuss.meeting_has_changed(
+ self.meeting_liststore.get_value(tree_iter, 1)):
+ return self.meeting_liststore.get_path(tree_iter)
+ return None
+
+ def mark_current_meeting_as_changed(self):
+ tree_iter = self.meeting_liststore.get_iter_first()
+ while tree_iter is not None:
+ markup = self.meeting_liststore.get_value(tree_iter, 0)
+ mtg = self.meeting_liststore.get_value(tree_iter, 1)
+ if mtg is self.current_meeting:
+ if '<b>' in markup and not self.discuss.meeting_has_changed(
+ self.current_meeting):
+ self.meeting_liststore.set_value(
+ tree_iter, 0, ', '.join(
+ (self.current_meeting.long_name,
+ self.current_meeting.short_name)))
+ break
+ tree_iter = self.meeting_liststore.iter_next(tree_iter)
+
+ def update_status_label(self, **kwargs):
+ if 'remaining' in kwargs:
+ text = "Retrieving headers, please wait... ({0} remaining)"
+ text = text.format(kwargs['remaining'])
+ elif 'mtg' in kwargs:
+ text = "Reading {mtg.long_name} [{mtg.first}-{mtg.last}]"
+ if 'trn' in kwargs:
+ text += ", #{trn.current}"
+ text = text.format(**kwargs)
+ self.builder.get_object("status_label").set_text(text)
+
+ # Event handlers
+
+ def xdsc_main_window_delete_event(self, widget, event, data=None):
+ if self.msg_dialog("Quit the application?", question=True):
+ self.quit()
+ else:
+ # Returning "True" inhibits the event
+ return True
+
+ def font_size_keypress_event(self, widget, event, data=None):
+ if event.state & Gdk.ModifierType.CONTROL_MASK:
+ if event.keyval == Gdk.KEY_plus or event.keyval == Gdk.KEY_KP_Add:
+ pangofont = widget.get_style().font_desc
+ if pangofont.get_size() // Pango.SCALE >= 16:
+ widget.error_bell()
+ return True
+ pangofont.set_size(pangofont.get_size() + Pango.SCALE)
+ uilogger.debug('Increasing font size of %s to %d (%d pt)',
+ widget.get_name(),
+ pangofont.get_size(),
+ pangofont.get_size() // Pango.SCALE)
+ widget.override_font(pangofont)
+ elif event.keyval == Gdk.KEY_minus or \
+ event.keyval == Gdk.KEY_KP_Subtract:
+ pangofont = widget.get_style().font_desc
+ if pangofont.get_size() // Pango.SCALE <= 6:
+ widget.error_bell()
+ return True
+ pangofont.set_size(pangofont.get_size() - Pango.SCALE)
+ uilogger.debug('Decreasing font size of %s to %d (%d pt)',
+ widget.get_name(),
+ pangofont.get_size(),
+ pangofont.get_size() // Pango.SCALE)
+ widget.override_font(pangofont)
+
+ def upper_treeview_cursor_changed(self, tree_view):
+ uilogger.debug("tree_view_cursor_changed_handler")
+ tree_row = tree_view.get_cursor()[0]
+ uilogger.debug("tree_row=%s", tree_row)
+ if tree_row is None:
+ return True
+ model = tree_view.get_model()
+ tree_iter = model.get_iter(tree_row)
+ if tree_iter is None:
+ uilogger.debug("tree_iter is None, shouldn't happen")
+ return True
+ if self.in_transaction_mode():
+ self.display_transaction(model.get_value(tree_iter, 1),
+ by_object=True)
+ else:
+ self.change_meeting(model.get_value(tree_iter, 1))
+ return True
+
+ def upper_treeview_move_cursor(self, tree_view, gtk_movement_step,
+ direction, data=None):
+ uilogger.debug("move_cursor %s %s %d", gtk_movement_step,
+ tree_view.get_cursor()[0], direction)
+ # Extend the liststore as we move up
+ if direction != -1:
+ return True
+ tree_path = tree_view.get_cursor()[0]
+ if tree_path != Gtk.TreePath.new_first():
+ return True
+ # We can't use Meeting.transactions() here because the range is unknown.
+ # Basically, we want to load the previous 10 transactions. Some meetings
+ # have huge gaps (hundreds of transactions) where spam was deleted.
+ trn = self.current_transaction
+ if trn.prev == 0:
+ tree_view.error_bell()
+ return True
+ if gtk_movement_step == Gtk.MovementStep.DISPLAY_LINES:
+ self.load_more_transactions(1)
+ return True
+ elif gtk_movement_step == Gtk.MovementStep.PAGES:
+ # Figure out how many rows the view is currently displaying
+ # and load that many more.
+ (start_row, end_row) = [path.get_indices()[0] for path in
+ self.upper_treeview.get_visible_range()]
+ uilogger.debug("Visible rows from %d to %d", start_row, end_row)
+ self.load_more_transactions(abs(end_row - start_row))
+ return True
+
+ # Menu handlers
+
+ def configure_add_meeting_activate(self, menuitem, data=None):
+ dialog = self.builder.get_object('add_meeting_dialog')
+ self.builder.get_object('add_meeting_hostname').set_text('')
+ self.builder.get_object('add_meeting_pathname').set_text(
+ '/usr/spool/discuss/')
+ if dialog.run() == Gtk.ResponseType.OK:
+ try:
+ hostName = self.builder.get_object(
+ 'add_meeting_hostname').get_text().strip()
+ pathName = self.builder.get_object(
+ 'add_meeting_pathname').get_text().strip()
+ self.discuss.add_meeting(hostName, pathName)
+ self.update_meeting_list()
+ except (ValueError, discuss.DiscussError,
+ discuss.rpc.ProtocolError) as e:
+ self.msg_dialog(str(e))
+ dialog.hide()
+
+ def configure_delete_meeting_activate(self, menuitem, data=None):
+ dialog = self.builder.get_object('delete_meeting_dialog')
+ if self.current_meeting is not None:
+ self.builder.get_object('delete_meeting_meetingname').set_text(
+ self.current_meeting.short_name)
+ if dialog.run() == Gtk.ResponseType.OK:
+ try:
+ meetingName = self.builder.get_object(
+ 'delete_meeting_meetingname').get_text().strip()
+ self.discuss.delete_meeting(meetingName)
+ self.update_meeting_list()
+ except (ValueError, discuss.DiscussError) as e:
+ self.msg_dialog(str(e))
+ dialog.hide()
+
+ def mode_transactions_activate(self, widget, data=None):
+ if self.current_meeting is None:
+ self.msg_dialog("Not currently attending a meeting.",
+ warn=True)
+ return None
+ self.builder.get_object('show_button').set_sensitive(True)
+ if len(self.trans_liststore) == 0:
+ for trn in self.current_meeting.transactions(
+ self.current_transaction.current, -1,
+ self.transactions_callback):
+ self.trans_liststore.append(
+ (DiscussWrapper.format_transaction_for_list(trn),
+ trn))
+ self.upper_treeview.set_model(self.trans_liststore)
+ self._select_transaction_by_num(self.current_transaction.current)
+ self.upper_treeview.grab_focus()
+
+ def mode_meetings_activate(self, widget, data=None):
+ self.builder.get_object('show_button').set_sensitive(False)
+ self.upper_treeview.set_model(self.meeting_liststore)
+
+ def show_unread_activate(self, menuitem, data=None):
+ self._select_transaction_by_num(
+ self.discuss._last_read_transaction(self.current_meeting))
+
+ def show_all_activate(self, menuitem, data=None):
+ # Extend the transaction_liststore with all the transactions
+ trn = self.trans_liststore.get_value(
+ self.trans_liststore.get_iter_first(), 1)
+ while trn.prev != 0:
+ trn = self.current_meeting.get_transaction(trn.prev)
+ self.trans_liststore.prepend(
+ (DiscussWrapper.format_transaction_for_list(trn), trn))
+ uilogger.debug("prepending %d", trn.current)
+ self.update_status_label(
+ remaining=trn.current - self.current_meeting.first)
+ while Gtk.events_pending():
+ Gtk.main_iteration()
+ self.update_status_label(mtg=self.current_meeting,
+ trn=self.current_transaction)
+
+ def show_back10_activate(self, menuitem, data=None):
+ uilogger.debug("Moving back 10")
+ start_path = self.upper_treeview.get_visible_range()[0]
+ visible_row = start_path.get_indices()[0]
+ uilogger.debug("topmost visible row: %s", visible_row)
+ if visible_row < 10:
+ self.load_more_transactions(10 - visible_row)
+ next_row = max(visible_row - 10, 0)
+ uilogger.debug("new visible row will be: %s", next_row)
+ self.upper_treeview.scroll_to_cell(
+ Gtk.TreePath.new_from_string(str(next_row)), None, True, 0.0, 0.0)
+
+ def goto_start_activate(self, widget, data=None):
+ # All transactions have an fref and lref. For single transactions,
+ # these are the transaciton number
+ self.display_transaction(self.current_transaction.fref)
+ if self.in_transaction_mode():
+ self.upper_treeview.get_selection().unselect_all()
+
+ def goto_end_activate(self, widget, data=None):
+ self.display_transaction(self.current_transaction.lref)
+ if self.in_transaction_mode():
+ self.upper_treeview.get_selection().unselect_all()
+
+ def goto_first_activate(self, widget, data=None):
+ self.display_transaction(self.current_meeting.first)
+ if self.in_transaction_mode():
+ self.upper_treeview.get_selection().unselect_all()
+
+ def goto_last_activate(self, widget, data=None):
+ self.display_transaction(self.current_meeting.last)
+ if self.in_transaction_mode():
+ self.upper_treeview.get_selection().unselect_all()
+
+ def goto_number_activate(self, widget, data=None):
+ dlg = self.builder.get_object('goto_transaction_dlg')
+ entry = self.builder.get_object('transaction_number_entry')
+ self.builder.get_object(
+ 'goto_transaction_dialog_ok').set_sensitive(False)
+ entry.set_text('')
+ entry.grab_focus()
+ response = dlg.run()
+ dlg.hide()
+ if response == Gtk.ResponseType.OK:
+ try:
+ number = int(entry.get_text().strip())
+ self.display_transaction(number)
+ if self.in_transaction_mode():
+ self.upper_treeview.get_selection().unselect_all()
+ except ValueError as e:
+ self.msg_dialog("'{0}' is not a valid number.",
+ entry.get_text())
+
+ def enter_reply_activate(self, widget, data=None):
+ self.post_reply(self.current_transaction)
+
+ def enter_new_transaction_activate(self, widget, data=None):
+ self.post_reply()
+
+ def write_mail_to_someone_activate(self, widget, data=None):
+ dlg = self.builder.get_object('send_email_dlg')
+ username = os.getenv('USER', None)
+ if username is not None:
+ self.builder.get_object('send_email_from').set_text(
+ '{0}@mit.edu'.format(username))
+ default_subj = "{trn.meeting.short_name}[{trn.current}] {subject}"
+ trn = self.current_transaction
+ default_subj = default_subj.format(trn=trn,
+ subject=unicode(trn.subject,
+ errors='replace'))
+ self.builder.get_object('send_email_subject').set_text(default_subj)
+ entry = self.builder.get_object('send_email_to')
+ self.builder.get_object('send_email_ok').set_sensitive(False)
+ entry.set_text('')
+ entry.grab_focus()
+ response = dlg.run()
+ dlg.hide()
+ if response == Gtk.ResponseType.OK:
+ textbuffer = self.builder.get_object('transaction_buffer')
+ start = textbuffer.get_start_iter()
+ end = textbuffer.get_end_iter()
+ msg = MIMEText(textbuffer.get_text(start, end, False))
+ msg['To'] = entry.get_text().strip()
+ msg['Subject'] = self.builder.get_object(
+ 'send_email_subject').get_text().strip()
+ msg['From'] = self.builder.get_object(
+ 'send_email_from').get_text().strip()
+ sendmail = subprocess.Popen(['/usr/sbin/sendmail', '-t'],
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ (stdout, stderr) = sendmail.communicate(msg.as_string())
+ if sendmail.returncode != 0:
+ self.msg_dialog("An error occurred while sending e-mail: {0}",
+ stderr, warn=True)
+ else:
+ self.msg_dialog("E-mail sent successfully.",
+ info=True)
+
+ def write_to_file_activate(self, widget, data=None):
+ filedialog = Gtk.FileChooserDialog("Save transaction to file...",
+ self.main_window,
+ Gtk.FileChooserAction.SAVE,
+ (Gtk.STOCK_CANCEL,
+ Gtk.ResponseType.CANCEL,
+ Gtk.STOCK_SAVE,
+ Gtk.ResponseType.OK))
+ filedialog.set_do_overwrite_confirmation(True)
+ filename = "{0}_{1}.txt".format(self.discuss.current_meeting.short_name,
+ self.current_transaction.current)
+ filedialog.set_current_name(filename)
+ response = filedialog.run()
+ filedialog.hide()
+ if response == Gtk.ResponseType.OK:
+ textbuffer = self.builder.get_object('transaction_buffer')
+ start = textbuffer.get_start_iter()
+ end = textbuffer.get_end_iter()
+ try:
+ with open(filedialog.get_filename(), 'w') as f:
+ f.write(textbuffer.get_text(start, end, False))
+ except IOError as e:
+ self.msg_dialog(e)
+
+ # Button handlers
+
+ def up_down_button_clicked(self, widget, data=None):
+ """
+ Signal handler for the 'clicked' event for the 'Up' and 'Down'
+ buttons in the top toolbar. In meeting mode, advance to the
+ next/previous meeting with unread transactions. In transaction
+ mode, advance to the next/previous transaction.
+ """
+ going_up = widget.get_name() == 'up_button'
+ if self.in_transaction_mode():
+ self.upper_treeview.emit('move-cursor',
+ Gtk.MovementStep.DISPLAY_LINES,
+ -1 if going_up else 1)
+ else:
+ curPath = self.upper_treeview.get_cursor()[0]
+ meetingPath = self._find_unread_meetings(start_path=curPath,
+ backwards = going_up)
+ if meetingPath is None:
+ self.msg_dialog("No more meetings with unread transactions.",
+ warn=True)
+ else:
+ self.upper_treeview.set_cursor(meetingPath, None)
+ return True
+
+ def update_button_clicked(self, widget, data=None):
+ """
+ Signal handler for the 'clicked' event for the 'Update' button.
+ """
+ self.update_meeting_list()
+
+ def get_menubutton_position(self, menu, toolbutton=None):
+ """
+ A GtkMenuPositionFunc callback for positioning the menus for the
+ "menubuttons" directly under the buttons themselves. Returns
+ a tuple of the x and y coordinates, and 'True' indicating it
+ should adjust the menu position if it would pop up outside the
+ screen boundaries.
+ """
+ assert isinstance(toolbutton, Gtk.ToolButton)
+ (xwin, ywin) = toolbutton.get_window().get_position()
+ allocation_rect = toolbutton.get_allocation()
+ return (xwin + allocation_rect.x,
+ ywin+allocation_rect.y+allocation_rect.height,
+ True)
+
+ def menubutton_clicked(self, widget, data=None):
+ """
+ Handler for the 'clicked' event for the "menu buttons"
+ ('Configure', 'Mode', 'Show', 'Goto', 'Enter', 'Write').
+ The XML passes the correct menu as widget data for the object.
+ """
+ assert widget.get_name() in self.menubuttons_map
+ button_id = self.menubuttons_map[widget.get_name()]
+ widget.popup(None, None, self.get_menubutton_position,
+ self.builder.get_object(button_id), 0,
+ Gtk.get_current_event_time())
+
+ def help_button_clicked(self, widget, data=None):
+ dlg = self.builder.get_object('help_dialog')
+ dlg.run()
+ dlg.hide()
+
+ def quit_button_clicked(self, widget, data=None):
+ self.quit()
+
+ def next_button_clicked(self, widget):
+ if self.current_transaction.next == 0:
+ self.msg_dialog("No more transactions")
+ else:
+ self.display_transaction(self.current_transaction.next)
+ if self.in_transaction_mode():
+ self.upper_treeview.get_selection().unselect_all()
+
+ def prev_button_clicked(self, widget):
+ if self.current_transaction.prev == 0:
+ self.msg_dialog("No more transactions")
+ else:
+ self.display_transaction(self.current_transaction.prev)
+ if self.in_transaction_mode():
+ self.upper_treeview.get_selection().unselect_all()
+
+ def next_chain_button_clicked(self, widget, data=None):
+ if self.current_transaction.nref == 0:
+ widget.error_bell()
+ return True
+ self.display_transaction(self.current_transaction.nref)
+ if self.in_transaction_mode():
+ self.upper_treeview.get_selection().unselect_all()
+
+ def prev_chain_button_clicked(self, widget, data=None):
+ if self.current_transaction.pref == 0:
+ widget.error_bell()
+ return True
+ self.display_transaction(self.current_transaction.pref)
+ if self.in_transaction_mode():
+ self.upper_treeview.get_selection().unselect_all()
+
+
+if __name__ == '__main__':
+ parser = OptionParser()
+ parser.set_defaults(ui_file=DEFAULT_UI_FILE,
+ icon_file=DEFAULT_ICON_FILE,
+ timeout=DEFAULT_TIMEOUT,
+ debug=[])
+ parser.add_option("--ui", dest="ui_file", action="store",
+ help="Specify UI file")
+ parser.add_option("--icon", dest="icon_file", action="store",
+ help="Specify icon file")
+ parser.add_option("--timeout", dest="timeout", action="store",
+ type="int", help="Default connection timeout")
+ parser.add_option("--debug", dest="debug", action="append",
+ help="Specify (multiple) debug options")
+ (options, args) = parser.parse_args()
+ if options.timeout > 5:
+ print >>sys.stderr, "A timeout value larger than 5 is not a good idea."
+ try:
+ dsc_wrapper = DiscussWrapper(options.timeout)
+ except ValueError as e:
+ dlg = Gtk.MessageDialog(None, 0, Gtk.MessageType.ERROR,
+ Gtk.ButtonsType.CLOSE,
+ "Error initializing discuss interface")
+ dlg.set_title("Xdsc")
+ dlg.format_secondary_text("\n".join(str(e).split(':', 1)))
+ dlg.run()
+ sys.exit(255)
+ if len(args):
+ parser.error("Program does not take any arguments.")
+ logging.basicConfig()
+ options.debug = [x.lower() for x in options.debug]
+ if 'ui' in options.debug:
+ uilogger.setLevel(logging.DEBUG)
+ if 'discuss' in options.debug:
+ dslogger.setLevel(logging.DEBUG)
+ # Because PyGObject does stupid things with SIGINT
+ signal.signal(signal.SIGINT, signal.SIG_DFL)
+ xdsc = Xdsc(options.ui_file, options.icon_file, dsc_wrapper)
+ Gtk.main()
+ sys.exit(0)
diff --git a/xdsc.ui b/xdsc.ui
new file mode 100644
index 0000000..f489723
--- /dev/null
+++ b/xdsc.ui
@@ -0,0 +1,1362 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <!-- interface-requires gtk+ 3.0 -->
+ <object class="GtkDialog" id="add_meeting_dialog">
+ <property name="width_request">305</property>
+ <property name="height_request">120</property>
+ <property name="can_focus">False</property>
+ <property name="title" translatable="yes">Add Meeting</property>
+ <property name="resizable">False</property>
+ <property name="window_position">center-on-parent</property>
+ <property name="type_hint">dialog</property>
+ <property name="transient_for">xdsc_main_window</property>
+ <child internal-child="vbox">
+ <object class="GtkBox" id="dialog-vbox2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child internal-child="action_area">
+ <object class="GtkButtonBox" id="dialog-action_area2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="layout_style">end</property>
+ <child>
+ <object class="GtkButton" id="cancelbutton1">
+ <property name="label">gtk-cancel</property>
+ <property name="use_action_appearance">False</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="can_default">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_action_appearance">False</property>
+ <property name="use_stock">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="okbutton1">
+ <property name="label">gtk-ok</property>
+ <property name="use_action_appearance">False</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="can_default">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_action_appearance">False</property>
+ <property name="use_stock">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="pack_type">end</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkTable" id="table1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="border_width">5</property>
+ <property name="n_rows">2</property>
+ <property name="n_columns">2</property>
+ <property name="column_spacing">5</property>
+ <property name="row_spacing">5</property>
+ <child>
+ <object class="GtkLabel" id="label1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">Host:</property>
+ </object>
+ <packing>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">Pathname:</property>
+ </object>
+ <packing>
+ <property name="top_attach">1</property>
+ <property name="bottom_attach">2</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="add_meeting_hostname">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="invisible_char">●</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="add_meeting_pathname">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="invisible_char">●</property>
+ <property name="text" translatable="yes">/usr/spool/discuss/</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">1</property>
+ <property name="bottom_attach">2</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <action-widgets>
+ <action-widget response="-6">cancelbutton1</action-widget>
+ <action-widget response="-5">okbutton1</action-widget>
+ </action-widgets>
+ </object>
+ <object class="GtkMenu" id="configure_menu">
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkMenuItem" id="configure_add_meeting">
+ <property name="use_action_appearance">False</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Add Meeting</property>
+ <signal name="activate" handler="configure_add_meeting_activate" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkMenuItem" id="configure_delete_meeting">
+ <property name="use_action_appearance">False</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Delete Meeting</property>
+ <signal name="activate" handler="configure_delete_meeting_activate" swapped="no"/>
+ </object>
+ </child>
+ </object>
+ <object class="GtkDialog" id="delete_meeting_dialog">
+ <property name="width_request">305</property>
+ <property name="can_focus">False</property>
+ <property name="title" translatable="yes">Delete Meeting</property>
+ <property name="resizable">False</property>
+ <property name="window_position">center-on-parent</property>
+ <property name="type_hint">dialog</property>
+ <property name="transient_for">xdsc_main_window</property>
+ <child internal-child="vbox">
+ <object class="GtkBox" id="vbox4">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child internal-child="action_area">
+ <object class="GtkButtonBox" id="hbuttonbox1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="layout_style">end</property>
+ <child>
+ <object class="GtkButton" id="button1">
+ <property name="label">gtk-cancel</property>
+ <property name="use_action_appearance">False</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="can_default">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_action_appearance">False</property>
+ <property name="use_stock">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="button2">
+ <property name="label">gtk-ok</property>
+ <property name="use_action_appearance">False</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="can_default">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_action_appearance">False</property>
+ <property name="use_stock">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="pack_type">end</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkTable" id="table2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="border_width">5</property>
+ <property name="n_columns">2</property>
+ <property name="column_spacing">5</property>
+ <property name="row_spacing">5</property>
+ <child>
+ <object class="GtkLabel" id="label3">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">Meeting:</property>
+ </object>
+ <packing>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="delete_meeting_meetingname">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="invisible_char">●</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <action-widgets>
+ <action-widget response="-6">button1</action-widget>
+ <action-widget response="-5">button2</action-widget>
+ </action-widgets>
+ </object>
+ <object class="GtkMenu" id="enter_menu">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkMenuItem" id="enter_reply">
+ <property name="use_action_appearance">False</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Reply</property>
+ <property name="use_underline">True</property>
+ <signal name="activate" handler="enter_reply_activate" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkMenuItem" id="enter_new_transaction">
+ <property name="use_action_appearance">False</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">New Transaction</property>
+ <property name="use_underline">True</property>
+ <signal name="activate" handler="enter_new_transaction_activate" swapped="no"/>
+ </object>
+ </child>
+ </object>
+ <object class="GtkDialog" id="enter_transaction_dlg">
+ <property name="can_focus">False</property>
+ <property name="border_width">5</property>
+ <property name="title" translatable="yes">Enter transaction...</property>
+ <property name="resizable">False</property>
+ <property name="type_hint">dialog</property>
+ <property name="transient_for">xdsc_main_window</property>
+ <child internal-child="vbox">
+ <object class="GtkBox" id="dialog-vbox9">
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">2</property>
+ <child internal-child="action_area">
+ <object class="GtkButtonBox" id="dialog-action_area9">
+ <property name="can_focus">False</property>
+ <property name="layout_style">end</property>
+ <child>
+ <object class="GtkButton" id="button5">
+ <property name="label" translatable="yes">Cancel</property>
+ <property name="use_action_appearance">False</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_action_appearance">False</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="enter_transaction_ok">
+ <property name="label" translatable="yes">Post</property>
+ <property name="use_action_appearance">False</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="can_default">True</property>
+ <property name="has_default">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_action_appearance">False</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="pack_type">end</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkGrid" id="grid2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="row_spacing">5</property>
+ <property name="column_spacing">5</property>
+ <child>
+ <object class="GtkLabel" id="label8">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">end</property>
+ <property name="label" translatable="yes">Subject:</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="enter_transaction_subject">
+ <property name="width_request">240</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="invisible_char">•</property>
+ <property name="invisible_char_set">True</property>
+ <signal name="changed" handler="enter_transaction_validate" swapped="no"/>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">0</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label9">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">end</property>
+ <property name="label" translatable="yes">Signature:</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">1</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="enter_transaction_signature">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="invisible_char">•</property>
+ <property name="invisible_char_set">True</property>
+ <signal name="changed" handler="enter_transaction_validate" swapped="no"/>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">1</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <action-widgets>
+ <action-widget response="-6">button5</action-widget>
+ <action-widget response="-5">enter_transaction_ok</action-widget>
+ </action-widgets>
+ </object>
+ <object class="GtkMenu" id="goto_menu">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkMenuItem" id="goto_number">
+ <property name="use_action_appearance">False</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Number</property>
+ <property name="use_underline">True</property>
+ <signal name="activate" handler="goto_number_activate" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkMenuItem" id="goto_first">
+ <property name="use_action_appearance">False</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">First</property>
+ <property name="use_underline">True</property>
+ <signal name="activate" handler="goto_first_activate" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkMenuItem" id="goto_last">
+ <property name="use_action_appearance">False</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Last</property>
+ <property name="use_underline">True</property>
+ <signal name="activate" handler="goto_last_activate" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkMenuItem" id="goto_start">
+ <property name="use_action_appearance">False</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Start of chain</property>
+ <property name="use_underline">True</property>
+ <signal name="activate" handler="goto_start_activate" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkMenuItem" id="goto_end">
+ <property name="use_action_appearance">False</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">End of chain</property>
+ <property name="use_underline">True</property>
+ <signal name="activate" handler="goto_end_activate" swapped="no"/>
+ </object>
+ </child>
+ </object>
+ <object class="GtkDialog" id="goto_transaction_dlg">
+ <property name="can_focus">False</property>
+ <property name="border_width">5</property>
+ <property name="title" translatable="yes">Go to transaction...</property>
+ <property name="type_hint">dialog</property>
+ <property name="transient_for">xdsc_main_window</property>
+ <child internal-child="vbox">
+ <object class="GtkBox" id="dialog-vbox5">
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">2</property>
+ <child internal-child="action_area">
+ <object class="GtkButtonBox" id="dialog-action_area5">
+ <property name="can_focus">False</property>
+ <property name="layout_style">end</property>
+ <child>
+ <object class="GtkButton" id="button4">
+ <property name="label">gtk-cancel</property>
+ <property name="use_action_appearance">False</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_action_appearance">False</property>
+ <property name="use_stock">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="goto_transaction_dialog_ok">
+ <property name="label">gtk-ok</property>
+ <property name="use_action_appearance">False</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="can_default">True</property>
+ <property name="has_default">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_action_appearance">False</property>
+ <property name="use_stock">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="pack_type">end</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="box1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkLabel" id="label4">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Transaction number:</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">3</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="transaction_number_entry">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="has_focus">True</property>
+ <property name="invisible_char">•</property>
+ <property name="activates_default">True</property>
+ <signal name="changed" handler="transaction_entry_changed" swapped="no"/>
+ <signal name="insert-text" handler="transaction_entry_insert_text" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <action-widgets>
+ <action-widget response="-6">button4</action-widget>
+ <action-widget response="-5">goto_transaction_dialog_ok</action-widget>
+ </action-widgets>
+ </object>
+ <object class="GtkTextBuffer" id="help_text_buffer">
+ <property name="text" translatable="yes">
+ What the buttons mean:
+---------------------------------------------------------------------
+ Down Enter the next meeting with unread transactions
+ Up Enter the previous meeting with unread transactions
+ Update Check for new transactions
+ Configure Change the list of meetings you attend
+ Mode Choose between listing meetings or transactions
+ Show Choose how many transactions should be listed
+ Help Display this screen
+ Quit Quit
+
+ <downarrow> Move cursor to the next line
+ <uparrow> Move cursor to the previous line
+ <return> Read the meeting or transaction the cursor is on
+---------------------------------------------------------------------
+ Next Read the next transaction in the current meeting
+ Prev Read the previous transaction in the current meeting
+ Next in chain Read the next transaction in this chain
+ Prev in chain Read the previous transaction in this chain
+ Goto Choose a specific transaction to read
+ Enter Enter a new transaction or reply to the current one
+ Write Save the current transaction to a file or mail it
+
+ <spacebar> 'do the right thing'
+ <backspace> reverse what space did
+---------------------------------------------------------------------
+You can also enter a meeting by doubleclicking on its title.
+
+The keyboard equivalent for clicking on a button is always the first
+character on the button.
+
+If a button is grayed out, this action is not possible at this time.
+For example, the 'enter' button will gray out when you do not have
+permission to enter transactions in a meeting.
+</property>
+ </object>
+ <object class="GtkDialog" id="help_dialog">
+ <property name="width_request">520</property>
+ <property name="height_request">600</property>
+ <property name="can_focus">False</property>
+ <property name="title" translatable="yes">Help</property>
+ <property name="resizable">False</property>
+ <property name="window_position">center-on-parent</property>
+ <property name="type_hint">dialog</property>
+ <property name="skip_taskbar_hint">True</property>
+ <property name="skip_pager_hint">True</property>
+ <property name="transient_for">xdsc_main_window</property>
+ <child internal-child="vbox">
+ <object class="GtkBox" id="dialog-vbox1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">2</property>
+ <child internal-child="action_area">
+ <object class="GtkButtonBox" id="dialog-action_area1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="layout_style">end</property>
+ <child>
+ <object class="GtkButton" id="closebutton1">
+ <property name="label">gtk-close</property>
+ <property name="use_action_appearance">False</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="can_default">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_action_appearance">False</property>
+ <property name="use_stock">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="pack_type">end</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkScrolledWindow" id="scrolledwindow3">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="hscrollbar_policy">always</property>
+ <property name="vscrollbar_policy">always</property>
+ <property name="shadow_type">in</property>
+ <child>
+ <object class="GtkTextView" id="help_textview">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="editable">False</property>
+ <property name="cursor_visible">False</property>
+ <property name="buffer">help_text_buffer</property>
+ <property name="accepts_tab">False</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <action-widgets>
+ <action-widget response="-7">closebutton1</action-widget>
+ </action-widgets>
+ </object>
+ <object class="GtkListStore" id="meeting_list_store">
+ <columns>
+ <!-- column-name meeting-markup -->
+ <column type="gchararray"/>
+ <!-- column-name meeting-object -->
+ <column type="PyObject"/>
+ </columns>
+ </object>
+ <object class="GtkMenu" id="mode_menu">
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkMenuItem" id="mode_transactions">
+ <property name="use_action_appearance">False</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Transactions</property>
+ <property name="use_underline">True</property>
+ <signal name="activate" handler="mode_transactions_activate" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkMenuItem" id="mode_meetings">
+ <property name="use_action_appearance">False</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Meetings</property>
+ <property name="use_underline">True</property>
+ <signal name="activate" handler="mode_meetings_activate" swapped="no"/>
+ </object>
+ </child>
+ </object>
+ <object class="GtkDialog" id="send_email_dlg">
+ <property name="can_focus">False</property>
+ <property name="border_width">5</property>
+ <property name="title" translatable="yes">Send e-mail...</property>
+ <property name="resizable">False</property>
+ <property name="type_hint">dialog</property>
+ <property name="transient_for">xdsc_main_window</property>
+ <child internal-child="vbox">
+ <object class="GtkBox" id="dialog-vbox7">
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">2</property>
+ <child internal-child="action_area">
+ <object class="GtkButtonBox" id="dialog-action_area7">
+ <property name="can_focus">False</property>
+ <property name="layout_style">end</property>
+ <child>
+ <object class="GtkButton" id="button3">
+ <property name="label" translatable="yes">Cancel</property>
+ <property name="use_action_appearance">False</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_action_appearance">False</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="send_email_ok">
+ <property name="label" translatable="yes">Send Email</property>
+ <property name="use_action_appearance">False</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="can_default">True</property>
+ <property name="has_default">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_action_appearance">False</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="pack_type">end</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkGrid" id="grid1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="row_spacing">5</property>
+ <property name="column_spacing">5</property>
+ <child>
+ <object class="GtkLabel" id="label5">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">end</property>
+ <property name="label" translatable="yes">To:</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">1</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="send_email_to">
+ <property name="width_request">240</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="has_focus">True</property>
+ <property name="invisible_char">•</property>
+ <signal name="changed" handler="send_email_validate" swapped="no"/>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">1</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label6">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">end</property>
+ <property name="label" translatable="yes">From:</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label7">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">end</property>
+ <property name="label" translatable="yes">Subject:</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">2</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="send_email_from">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="invisible_char">•</property>
+ <signal name="changed" handler="send_email_validate" swapped="no"/>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">0</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="send_email_subject">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="invisible_char">•</property>
+ <signal name="changed" handler="send_email_validate" swapped="no"/>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">2</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <action-widgets>
+ <action-widget response="-6">button3</action-widget>
+ <action-widget response="-5">send_email_ok</action-widget>
+ </action-widgets>
+ </object>
+ <object class="GtkMenu" id="show_menu">
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkMenuItem" id="unread">
+ <property name="use_action_appearance">False</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Unread</property>
+ <property name="use_underline">True</property>
+ <signal name="activate" handler="show_unread_activate" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkMenuItem" id="all">
+ <property name="use_action_appearance">False</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">All</property>
+ <property name="use_underline">True</property>
+ <signal name="activate" handler="show_all_activate" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkMenuItem" id="back10">
+ <property name="use_action_appearance">False</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Back 10</property>
+ <property name="use_underline">True</property>
+ <signal name="activate" handler="show_back10_activate" swapped="no"/>
+ </object>
+ </child>
+ </object>
+ <object class="GtkTextBuffer" id="transaction_buffer"/>
+ <object class="GtkListStore" id="transaction_list_store">
+ <columns>
+ <!-- column-name transaction-markup -->
+ <column type="gchararray"/>
+ <!-- column-name transaction-object -->
+ <column type="PyObject"/>
+ </columns>
+ </object>
+ <object class="GtkMenu" id="write_menu">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkMenuItem" id="write_to_file">
+ <property name="use_action_appearance">False</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Write to file</property>
+ <property name="use_underline">True</property>
+ <signal name="activate" handler="write_to_file_activate" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkMenuItem" id="write_mail">
+ <property name="use_action_appearance">False</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Mail to someone</property>
+ <property name="use_underline">True</property>
+ <signal name="activate" handler="write_mail_to_someone_activate" swapped="no"/>
+ </object>
+ </child>
+ </object>
+ <object class="GtkWindow" id="xdsc_main_window">
+ <property name="width_request">600</property>
+ <property name="height_request">400</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="title" translatable="yes">Xdsc</property>
+ <property name="icon_name">stock_news</property>
+ <signal name="delete-event" handler="xdsc_main_window_delete_event" swapped="no"/>
+ <child>
+ <object class="GtkVBox" id="vbox1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkVBox" id="vbox2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkToolbar" id="toolbar1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="toolbar_style">both</property>
+ <child>
+ <object class="GtkToolButton" id="down_button">
+ <property name="use_action_appearance">False</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="use_action_appearance">False</property>
+ <property name="label" translatable="yes">Down</property>
+ <property name="use_underline">True</property>
+ <property name="stock_id">gtk-go-down</property>
+ <signal name="clicked" handler="up_down_button_clicked" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="homogeneous">True</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToolButton" id="up_button">
+ <property name="use_action_appearance">False</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="use_action_appearance">False</property>
+ <property name="label" translatable="yes">Up</property>
+ <property name="use_underline">True</property>
+ <property name="stock_id">gtk-go-up</property>
+ <signal name="clicked" handler="up_down_button_clicked" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="homogeneous">True</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToolButton" id="update_button">
+ <property name="use_action_appearance">False</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="use_action_appearance">False</property>
+ <property name="label" translatable="yes">Update</property>
+ <property name="use_underline">True</property>
+ <property name="stock_id">gtk-refresh</property>
+ <signal name="clicked" handler="update_button_clicked" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="homogeneous">True</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToolButton" id="configure_button">
+ <property name="use_action_appearance">False</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="use_action_appearance">False</property>
+ <property name="label" translatable="yes">Configure</property>
+ <property name="use_underline">True</property>
+ <property name="stock_id">gtk-preferences</property>
+ <signal name="clicked" handler="menubutton_clicked" object="configure_menu" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="homogeneous">True</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToolButton" id="mode_button">
+ <property name="use_action_appearance">False</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="use_action_appearance">False</property>
+ <property name="label" translatable="yes">Mode</property>
+ <property name="use_underline">True</property>
+ <property name="stock_id">gtk-index</property>
+ <signal name="clicked" handler="menubutton_clicked" object="mode_menu" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="homogeneous">True</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToolButton" id="show_button">
+ <property name="use_action_appearance">False</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="use_action_appearance">False</property>
+ <property name="label" translatable="yes">Show</property>
+ <property name="use_underline">True</property>
+ <property name="stock_id">gtk-zoom-fit</property>
+ <signal name="clicked" handler="menubutton_clicked" object="show_menu" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="homogeneous">True</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToolButton" id="help_button">
+ <property name="use_action_appearance">False</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="use_action_appearance">False</property>
+ <property name="visible_vertical">False</property>
+ <property name="label" translatable="yes">Help</property>
+ <property name="use_underline">True</property>
+ <property name="stock_id">gtk-help</property>
+ <signal name="clicked" handler="help_button_clicked" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="homogeneous">True</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToolButton" id="quit_button">
+ <property name="use_action_appearance">False</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="use_action_appearance">False</property>
+ <property name="label" translatable="yes">Quit</property>
+ <property name="use_underline">True</property>
+ <property name="stock_id">gtk-quit</property>
+ <signal name="clicked" handler="quit_button_clicked" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="homogeneous">True</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkScrolledWindow" id="scrolledwindow2">
+ <property name="height_request">130</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="hscrollbar_policy">never</property>
+ <property name="vscrollbar_policy">always</property>
+ <property name="shadow_type">in</property>
+ <child>
+ <object class="GtkTreeView" id="upper_treeview">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="model">meeting_list_store</property>
+ <property name="headers_visible">False</property>
+ <property name="enable_search">False</property>
+ <signal name="cursor-changed" handler="upper_treeview_cursor_changed" swapped="no"/>
+ <signal name="key-press-event" handler="font_size_keypress_event" swapped="no"/>
+ <signal name="move-cursor" handler="upper_treeview_move_cursor" swapped="no"/>
+ <child internal-child="selection">
+ <object class="GtkTreeSelection" id="treeview-selection1"/>
+ </child>
+ <child>
+ <object class="GtkTreeViewColumn" id="treeviewcolumn2">
+ <property name="spacing">5</property>
+ <property name="title" translatable="yes">meeting-name</property>
+ <child>
+ <object class="GtkCellRendererText" id="meeting_name_renderer">
+ <property name="xpad">5</property>
+ </object>
+ <attributes>
+ <attribute name="markup">0</attribute>
+ </attributes>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="status_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">start</property>
+ <property name="margin_left">5</property>
+ <property name="label" translatable="yes">(Not currently attending a meeting)</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">2</property>
+ <property name="pack_type">end</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSeparator" id="separator1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="pack_type">end</property>
+ <property name="position">3</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkVBox" id="vbox3">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkToolbar" id="toolbar2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="toolbar_style">both</property>
+ <child>
+ <object class="GtkToolButton" id="next_button">
+ <property name="use_action_appearance">False</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="use_action_appearance">False</property>
+ <property name="label" translatable="yes">Next</property>
+ <property name="use_underline">True</property>
+ <property name="stock_id">gtk-media-next</property>
+ <signal name="clicked" handler="next_button_clicked" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="homogeneous">True</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToolButton" id="prev_button">
+ <property name="use_action_appearance">False</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="use_action_appearance">False</property>
+ <property name="label" translatable="yes">Previous</property>
+ <property name="use_underline">True</property>
+ <property name="stock_id">gtk-media-previous</property>
+ <signal name="clicked" handler="prev_button_clicked" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="homogeneous">True</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToolButton" id="next_chain_button">
+ <property name="use_action_appearance">False</property>
+ <property name="visible">True</property>
+ <property name="sensitive">False</property>
+ <property name="can_focus">False</property>
+ <property name="use_action_appearance">False</property>
+ <property name="label" translatable="yes">Next in chain</property>
+ <property name="use_underline">True</property>
+ <property name="stock_id">gtk-redo</property>
+ <signal name="clicked" handler="next_chain_button_clicked" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="homogeneous">True</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToolButton" id="prev_chain_button">
+ <property name="use_action_appearance">False</property>
+ <property name="visible">True</property>
+ <property name="sensitive">False</property>
+ <property name="can_focus">False</property>
+ <property name="use_action_appearance">False</property>
+ <property name="label" translatable="yes">Prev in chain</property>
+ <property name="use_underline">True</property>
+ <property name="stock_id">gtk-undo</property>
+ <signal name="clicked" handler="prev_chain_button_clicked" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="homogeneous">True</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToolButton" id="goto_button">
+ <property name="use_action_appearance">False</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="use_action_appearance">False</property>
+ <property name="label" translatable="yes">Goto</property>
+ <property name="use_underline">True</property>
+ <property name="stock_id">gtk-jump-to</property>
+ <signal name="clicked" handler="menubutton_clicked" object="goto_menu" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="homogeneous">True</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToolButton" id="enter_button">
+ <property name="use_action_appearance">False</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="use_action_appearance">False</property>
+ <property name="label" translatable="yes">Enter</property>
+ <property name="use_underline">True</property>
+ <property name="stock_id">gtk-new</property>
+ <signal name="clicked" handler="menubutton_clicked" object="enter_menu" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="homogeneous">True</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToolButton" id="write_button">
+ <property name="use_action_appearance">False</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="use_action_appearance">False</property>
+ <property name="label" translatable="yes">Write</property>
+ <property name="use_underline">True</property>
+ <property name="stock_id">gtk-save-as</property>
+ <signal name="clicked" handler="menubutton_clicked" object="write_menu" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="homogeneous">True</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkScrolledWindow" id="scrolledwindow1">
+ <property name="height_request">400</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="hscrollbar_policy">never</property>
+ <property name="vscrollbar_policy">always</property>
+ <property name="shadow_type">in</property>
+ <child>
+ <object class="GtkTextView" id="transaction_textview">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="editable">False</property>
+ <property name="wrap_mode">char</property>
+ <property name="buffer">transaction_buffer</property>
+ <signal name="key-press-event" handler="font_size_keypress_event" swapped="no"/>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+</interface>
diff --git a/xdsc_icon.gif b/xdsc_icon.gif
new file mode 100644
index 0000000..9e9bbf1
Binary files /dev/null and b/xdsc_icon.gif differ