import utils from utils import _u import string import gtk import gobject import pango from sort import * from pyneheaders import * from boxtypes import superbox import time from time import localtime from time import strftime import pynei18n import ptk.misc_widgets # used in threading, and made global for the sake of laze CHILDREN = HEAD_LENGTH PARENT = HEAD_LENGTH+1 # Columns COL_PIXBUF = 0 COL_SUBJECT = 1 COL_TOFROM = 2 COL_DATE = 3 COL_MSGID = 4 COL_WEIGHT = 5 COL_COLOUR = 6 def model_path_next(model, path): """ return a path one step down the tree from this, stepping into subtrees depth first. """ iter = model.get_iter(path) if model.iter_has_child(iter): return path+(0,) # Go back to parent and consider child_num = path[-1] path = path[:-1] while 1: # parent path if len(path): iter = model.get_iter(path) num = model.iter_n_children(iter) else: num = model.iter_n_children(None) if child_num < num-1: return path+(child_num+1,) elif len(path): # no more children. parent again child_num = path[-1] path = path[:-1] if len(path) == 0: # on the root node num = model.iter_n_children(None) if child_num < num-1: return (child_num+1,) else: return None else: return None def model_path_prev(model, path): """ return a path one step up the tree from this, stepping into subtrees depth first. """ while 1: # if this is the first child we decend if path[-1] == 0: if len(path) == 1: # top return None return path[:-1] # otherwise go onto previous child else: kidnum = path[-1] path = path[:-1]+(kidnum-1,) break # zoom to depth while 1: iter = model.get_iter(path) num = model.iter_n_children(iter) if num: path = path+(num-1,) else: return path # message_tree and folder_tree are best used attached to # gtk.ScrolledWindow objects. class message_tree(gtk.TreeView): """ A TreeView for showing NNTP or email messages with threading code and that stuff... """ def __init__(self, parent_win, width): gtk.TreeView.__init__(self) cell = gtk.CellRendererText() pix = gtk.CellRendererPixbuf() self.get_selection().set_mode(gtk.SELECTION_MULTIPLE) self.subject_column = gtk.TreeViewColumn(_("Subject")) self.subject_column.pack_start(pix, expand=False) self.subject_column.pack_start(cell, expand=True) self.subject_column.set_spacing(4) self.subject_column.add_attribute(pix, "pixbuf", COL_PIXBUF) self.subject_column.add_attribute(cell, "text", COL_SUBJECT) self.subject_column.add_attribute(cell, "weight", COL_WEIGHT) self.subject_column.add_attribute(cell, "foreground", COL_COLOUR) self.subject_column.set_clickable(True) self.subject_column.set_resizable(True) self.subject_column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED) self.subject_column.set_fixed_width(width/2) self.append_column(self.subject_column) self.tofrom_column = gtk.TreeViewColumn(_("From"), cell, text=COL_TOFROM) self.tofrom_column.set_resizable(True) self.tofrom_column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED) self.tofrom_column.set_fixed_width(width/4) self.tofrom_column.set_clickable(True) self.append_column(self.tofrom_column) self.date_column = gtk.TreeViewColumn(_("Date"), cell, text=COL_DATE) self.date_column.set_resizable(True) self.date_column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED) self.date_column.set_fixed_width(width/4) self.date_column.set_clickable(True) self.append_column(self.date_column) self.set_model(gtk.TreeStore(gobject.TYPE_OBJECT, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_INT, gobject.TYPE_STRING)) self.cur_object = None # current mailbox shown #self.set_expander_style(gtk.CTREE_EXPANDER_SQUARE) #self.set_line_style(gtk.CTREE_LINES_DOTTED) self.button_press_function = None self.key_press_function = None self.parent_win = parent_win self.user = parent_win.user def change_sort_method(w, column, self=self): # Change sorting method according to column head clicked on oldmethod = self.user.sort_type # invert if same if oldmethod[0] == column: self.user.sort_type = (column, 1-oldmethod[1]) else: self.user.sort_type = (column, 0) # force update box = self.cur_object self.cur_object = None self.update(box, self.parent_win.filter_msgview) self.subject_column.connect("clicked", change_sort_method, 0) self.tofrom_column.connect("clicked", change_sort_method, 1) self.date_column.connect("clicked", change_sort_method, 2) def dnd_drag_data_get(w, context, selection_data, info, time, self=self): # give folder.uid/message-id selected = str(self.cur_object.uid)+"/"+repr(self.get_selected_msg_ids()) selection_data.set(selection_data.target, 8, selected) self.connect("drag_data_get", dnd_drag_data_get) # setup DND. Only move at the moment. no copy targets = [ ("pyne_msgid", 0, 0) ] self.drag_source_set(gtk.gdk.BUTTON1_MASK|gtk.gdk.BUTTON3_MASK, targets, gtk.gdk.ACTION_MOVE) def click_tree_item(w, event, self=self): """ Get id of clicked message and pass to user_press_function """ mousebutton = event.button msg_id = None # Absolute widget coords of click x, y = event.x, event.y# + w.get_vadjustment().get_value() selected = w.get_path_at_pos(int(x), int(y)) if selected == None: return #selected_message = self.get_model().on_get_iter(selected[0]) iter = self.get_model().get_iter(selected[0]) msg_id = self.get_model().get_value(iter, COL_MSGID) # Call secondary event handler if self.button_press_function != None: self.button_press_function(self, event, msg_id, iter) def _key_press(w, event, self=self): # Get message ID of selected message # Note this will very soon become out of date # if up/down cursors were hit msg_ids = self.get_selected_msg_ids() self.get_selected_msg_ids() if len(msg_ids) != 0: msg_id = msg_ids[0] else: msg_id = None # Filthy user function... if self.key_press_function != None: self.key_press_function(self, event, msg_id) self.connect("key_press_event", _key_press) self.connect("button_press_event", click_tree_item) # make clicking the expander open whole thread done_thingies = [] def _expand_recursive(widget, iter, path): # Avoid recursive eating your own tonsils problem. Beautiful ;-) if not path in done_thingies: done_thingies.append(path) self.collapse_row(path) self.expand_row(path, True) done_thingies.remove(path) self.connect("row-expanded", _expand_recursive) def get_iter_for_msgid(self, msg_id): """ This is pretty rank. Perhaps we should cache a msg_id to path dictionary. """ found_iter = [None] def _findmsgid(liststore, path, iter): # Add message id if it is an expanded node if msg_id == self.get_model().get_value(iter, COL_MSGID): found_iter[0] = iter return True self.get_model().foreach(_findmsgid) return found_iter[0] def update_node(self, user, msg_opts, msg_id=None, iter=None): """ The msg.opts of this node have changed. Update to reflect this. Either there should be a given node, or we search for one by msg_id. iter should be a GtkTreeIter thingy. """ # search for the iter if none is given if iter==None: iter = self.get_iter_for_msgid(msg_id) if iter == None: return isread = (msg_opts & MSG_ISREAD) == MSG_ISREAD if (msg_opts & MSG_NO_BODY): flags = "000%d" % ((msg_opts & MSG_ISMARKED)==MSG_ISMARKED) else: isreplied = (msg_opts & MSG_ISREPLIED) == MSG_ISREPLIED ismarked = (msg_opts & MSG_ISMARKED) == MSG_ISMARKED flags = "1%d%d%d" % (isread, isreplied, ismarked) icon = user.msg_icons[flags] text_col = user.__dict__[user.msg_cols[flags]] self.get_model().set(iter, COL_PIXBUF, icon.get_pixbuf()) self.get_model().set(iter, COL_COLOUR, text_col) # bold if unread if isread or (msg_opts & MSG_NO_BODY): self.get_model().set(iter, COL_WEIGHT, pango.WEIGHT_NORMAL) else: self.get_model().set(iter, COL_WEIGHT, pango.WEIGHT_BOLD) def get_selected_msg_ids(self): """ Return list of selected message-ids """ msg_ids = [] def _get_msgid(widget, path, iter): msg_id = self.get_model().get_value(iter, COL_MSGID) msg_ids.append(msg_id) if self.cur_object == None: return [] self.get_selection().selected_foreach(_get_msgid) return msg_ids def connect_button_press_event(self, function): """ Allow "button_press_event"s to be passed to a secondary function for things like right click menus and doing more than just remembering what is selected :-) """ self.button_press_function = function def connect_key_press_event(self, function): """ As above for keypresses... """ self.key_press_function = function def select_next(self, prev=0): # Find the first selected node model = self.get_model() selection = self.get_selection() node = [ None ] def _unselect(model, path, iter, node=node): if node[0] == None: if prev: node[0] = model_path_prev(model, path) else: node[0] = model_path_next(model, path) selection.unselect_iter(iter) selection.selected_foreach(_unselect) node = node[0] if node == None: # start from the beginning node = model.get_iter_first() if node == None: return node = model.get_path(node) # expand up to it for i in xrange(len(node)-1, 0, -1): self.expand_row(node[:-i], False) iter = model.get_iter(node) selection.select_iter(iter) msg_id = model.get_value(iter, COL_MSGID) self.scroll_to_cell(node, self.subject_column, True, 0.5, 0.5) # Open it self.parent_win.open_message(iter, msg_id) def update(self, object, filter, clear=0): """ Thread messages in mailbox/nntpbox 'object'. """ #t = time.time() if object == None and self.cur_object == None: return if clear == 1 and object == self.cur_object: # Folder we have viewing has been nuked self.get_model().clear() self.cur_object = None return user = self.user # Remember expanded messages expanded = [] # paths to expand once we know expand_paths = [] if self.cur_object == object: def _addexpanded(liststore, path, iter): # Add message id if it is an expanded node if self.row_expanded(path): expanded.append(self.get_model().get_value(iter, COL_MSGID)) # Redrawing same folder. Remember what nodes are expanded. self.get_model().foreach(_addexpanded) elif self.cur_object: # New folder. Zoom to top self.scroll_to_point(0, 0) # remember selected nodes selected = [] select_paths = [] def _addselected(liststore, path, iter): selected.append(self.get_model().get_value(iter, COL_MSGID)) self.get_selection().selected_foreach(_addselected) # Wipe current model self.get_model().clear() # Remember what mailbox is being viewed self.cur_object = object # name the to/from column appropriately if object.uid == "sent" or object.uid == "outbox": self.tofrom_column.set_title(_("To")) else: self.tofrom_column.set_title(_("From")) # build cache of actual message objects r_messages = [] object.num_unread = 0 for x in object.messages: msg = object.load_header(x) if msg != None: r_messages.append(msg) if not (msg[HEAD_OPTS] & MSG_ISREAD): object.num_unread += 1 else: print "Missing headers message ",x # Sort sort_type, sort_polarity = user.sort_type if object.opts & superbox.OPT_THREAD: # objects to be threaded. sort backwards r_messages.sort(sort_methods[sort_type][1-sort_polarity]) else: r_messages.sort(sort_methods[sort_type][sort_polarity]) # transfer back to object.messages object.messages = [] for x in r_messages: object.messages.append(x[HEAD_MESSAGE_ID]) # Apply filters if filter: # backwards so we can remove for i in xrange(len(r_messages)-1, -1, -1): opts = r_messages[i][HEAD_OPTS] if (filter & FILTER_READ) and (opts & MSG_ISREAD): # Filter out read messages del r_messages[i] continue elif (filter & FILTER_UNREAD) and not (opts & MSG_ISREAD): # Filter out unread messages del r_messages[i] continue elif (filter & FILTER_PARTIAL) and (opts & MSG_NO_BODY): # Filter out partially downloaded (headers only) messages del r_messages[i] continue elif (filter & FILTER_FULL) and not (opts & MSG_NO_BODY): # filter out fully downloaded messages del r_messages[i] continue elif (filter & FILTER_UNMARKED) and not (opts & MSG_ISMARKED): del r_messages[i] continue elif (filter & FILTER_MARKED) and (opts & MSG_ISMARKED): del r_messages[i] continue # name the to/from column correctly #if object.uid == "sent" or object.uid == "outbox": # self.set_column_title(1, _("To")) #else: # self.set_column_title(1, _("From")) def make_node(headers): # format: ( headers, [children] ) if headers != None: node = list(headers) node.append([]) else: node = [ 0, None, None, None, None, "", None, None, [] ] return node ########################################### BUILD def make_thread(messages, msg_dict, ref_cache, expanded): """ {fast} threading routine. 'expanded' is a list of message-ids of expanded nodes. """ #__t = time.time() # supernode, empty thread = make_node(None) node_cache = {} for x in range(0, len(messages)): refs = ref_cache[x] msg = messages[x] t = thread[CHILDREN] for depth in xrange(0, len(refs)): # has the message-id been added to the # tree yet? if node_cache.has_key(refs[depth]): node = node_cache[refs[depth]] t = node[CHILDREN] else: # if the message's headers are downloaded if msg_dict.has_key(refs[depth]): node = make_node(msg_dict[refs[depth]]) t.insert(0, node) t = node[CHILDREN] node_cache[refs[depth]] = node # else create empty node (dated to this message) else: node = make_node(None) node[HEAD_DATE] = msg[HEAD_DATE] t.insert(0, node) t = node[CHILDREN] node_cache[refs[depth]] = node # mark depth 0 message as unread if any are unread above it :-] # mark as replied if there are replied messages in this thread. # makes finding threads you posted to nice and easy. if depth == 0 and node[HEAD_MESSAGE_ID] != None: if not (msg[HEAD_OPTS] & MSG_ISREAD): node[HEAD_OPTS] = node[HEAD_OPTS] & (~MSG_ISREAD) if msg[HEAD_OPTS] & MSG_ISREPLIED: node[HEAD_OPTS] = node[HEAD_OPTS] | MSG_ISREPLIED if msg[HEAD_OPTS] & MSG_ISMARKED: node[HEAD_OPTS] = node[HEAD_OPTS] | MSG_ISMARKED tree_store = self.get_model() #print "PART 1:", time.time() - __t, # build thread [recursively] :-) def build_tree(this_node, parent, depth): msg = this_node if msg[HEAD_MESSAGE_ID] != None: # chose icon by 'read' status if (msg[HEAD_OPTS]&MSG_NO_BODY): icon = user.msg_icons["000%d" % ((msg[HEAD_OPTS]&MSG_ISMARKED)==MSG_ISMARKED)] text_weight = pango.WEIGHT_NORMAL text_col = user.col_mnobody else: isread = (msg[HEAD_OPTS]&MSG_ISREAD) == MSG_ISREAD isreplied = (msg[HEAD_OPTS]&MSG_ISREPLIED) == MSG_ISREPLIED ismarked = (msg[HEAD_OPTS]&MSG_ISMARKED) == MSG_ISMARKED flags = "1%d%d%d" % (isread, isreplied, ismarked) icon = user.msg_icons[flags] text_col = user.__dict__[user.msg_cols[flags]] # make unread stuff bold if isread: text_weight = pango.WEIGHT_NORMAL else: text_weight = pango.WEIGHT_BOLD iter = tree_store.append(parent) tree_store.set(iter, COL_PIXBUF, icon.get_pixbuf(), COL_SUBJECT, _u(msg[HEAD_SUBJECT]), COL_TOFROM, _u(utils.split_address(msg[HEAD_FROM_TO])[0]), COL_DATE, time.strftime("%d %b %Y %H:%M:%S", time.localtime(msg[HEAD_DATE])), COL_MSGID, _u(msg[HEAD_MESSAGE_ID]), COL_WEIGHT, text_weight, COL_COLOUR, text_col) if msg[HEAD_MESSAGE_ID] in expanded: # It should be expanded expand_paths.append(tree_store.get_path(iter)) if msg[HEAD_MESSAGE_ID] in selected: # It was selected before redraw select_paths.append(tree_store.get_path(iter)) else: # if the message is a dummy only # (possibly) show at base of thread if depth > 0: # if it has only 1 child don't show it obviously if len(this_node[CHILDREN]) < 2: iter = parent else: try: # otherwise get name from nearest 'real' message _this_node = this_node while msg[HEAD_MESSAGE_ID] == None: _this_node = _this_node[CHILDREN][0] msg = _this_node # uncached message icon icon = user.msg_icons["0000"] iter = tree_store.append(parent) tree_store.set(iter, COL_PIXBUF, icon.get_pixbuf(), COL_SUBJECT, _u(msg[HEAD_SUBJECT]), COL_TOFROM, "", COL_DATE, time.strftime("%d %b %Y %H:%M:%S", time.localtime(msg[HEAD_DATE])), COL_MSGID, _u(msg[HEAD_MESSAGE_ID]), COL_WEIGHT, pango.WEIGHT_NORMAL, COL_COLOUR, user.__dict__[user.msg_cols["0000"]]) #], 0, icon, mask, icon, mask, False, False) #self.node_set_row_data(node, None) except IndexError, e: # Messages with screwed up references (ie. 2 # copies of one message-id) may cause index # error in '_this_node = _this_node[CHILDREN][0]' node = parent iter = parent else: # move onto it's children iter = parent depth = depth + 1 # recurse into each child for x in this_node[CHILDREN]: build_tree(x, iter, depth) # recursively build thread #__t = time.time() build_tree(thread, None, 0) #print " PART 2:", time.time() - __t, return tree_store if object.opts & superbox.OPT_THREAD: # Newsgroups or mailboxes explicitly requesting # threading of message should be ref_cache = [] d_messages = {} for x in r_messages: # if there are references if x[HEAD_REFERENCES] != None: # a = references + " " + message-id a = x[HEAD_REFERENCES]+" "+x[HEAD_MESSAGE_ID] else: a = x[HEAD_MESSAGE_ID] a = tuple(string.split(a)) ref_cache.append(a) # a dictionary of message headers indexed by message-ids d_messages[x[HEAD_MESSAGE_ID]] = x model_tree = make_thread(r_messages, d_messages, ref_cache, expanded) else: # otherwise no threading of messages model_tree = self.get_model() for msg in r_messages: if (msg[HEAD_OPTS]&MSG_NO_BODY): flags = "000%d" % ((msg[HEAD_OPTS]&MSG_ISMARKED)==MSG_ISMARKED) icon = user.msg_icons[flags] text_col = user.__dict__[user.msg_cols[flags]] text_weight = pango.WEIGHT_NORMAL else: isread = (msg[HEAD_OPTS]&MSG_ISREAD) == MSG_ISREAD isreplied = (msg[HEAD_OPTS]&MSG_ISREPLIED) == MSG_ISREPLIED ismarked = (msg[HEAD_OPTS]&MSG_ISMARKED) == MSG_ISMARKED flags = "1%d%d%d" % (isread, isreplied, ismarked) icon = user.msg_icons[flags] text_col = user.__dict__[user.msg_cols[flags]] # make unread stuff bold if isread: text_weight = pango.WEIGHT_NORMAL else: text_weight = pango.WEIGHT_BOLD iter = model_tree.append(None) model_tree.set(iter, COL_PIXBUF, icon.get_pixbuf(), COL_SUBJECT, _u(msg[HEAD_SUBJECT]), COL_TOFROM, _u(utils.split_address(msg[HEAD_FROM_TO])[0]), COL_DATE, time.strftime("%d %b %Y %H:%M:%S", time.localtime(msg[HEAD_DATE])), COL_MSGID, _u(msg[HEAD_MESSAGE_ID]), COL_WEIGHT, text_weight, COL_COLOUR, text_col) if msg[HEAD_MESSAGE_ID] in selected: # It was selected before redraw select_paths.append(model_tree.get_path(iter)) # Expand previously expanded nodes for path in expand_paths: self.expand_row(path, False) # Select previously selected nodes for path in select_paths: self.get_selection().select_path(path) #print "Done ("+str(time.time()-t)+" secs)" # Smelly little 'search for string in messages' box class search_box(gtk.Window): def __init__(self, user, pynewin): self.user = user self.pynewin = pynewin # create pretty find boxy thing gtk.Window.__init__(self) self.set_transient_for(pynewin) self.set_title(_("Pyne - Find message")) box = gtk.VBox() self.add(box) box.show() # Find: [ something ] text entry box1 = gtk.HBox() box.pack_start(box1, expand=False) box1.set_border_width(5) box1.show() label = gtk.Label(_("Find: ")) box1.pack_start(label, expand=False) label.show() self.find_entry = gtk.Entry() box1.pack_start(self.find_entry) self.find_entry.show() # Settings box2 = gtk.HBox() box2.set_border_width(5) box.pack_start(box2, expand=False) box2.show() self.case_sensitive_cb = gtk.CheckButton(_("Case Sensitive")) box2.pack_start(self.case_sensitive_cb, expand=False) self.case_sensitive_cb.show() # Buttons buttonbox = ptk.misc_widgets.MyButtonBox() box.pack_start(buttonbox, expand=False) buttonbox.show() button = gtk.Button(stock="gtk-find") button.connect("clicked", self.do_find) button.show() buttonbox.pack_end(button, expand=False) self.show() def do_find(self, _button): selection = self.pynewin.message_list.get_selection() model = self.pynewin.message_list.get_model() # Find the first selected node to start search from start = [ None ] def _unselect(model, path, iter, start=start): if start[0] == None: start[0] = model_path_next(model, path) selection.unselect_iter(iter) selection.selected_foreach(_unselect) start = start[0] if start == None: # start from the beginning start = model.get_iter_first() if start == None: return start = model.get_path(start) path = start while path: iter = model.get_iter(path) msg_id = model.get_value(iter, COL_MSGID) msg = self.pynewin.opened_folder.load_article(msg_id) find_me = self.find_entry.get_text() if self.case_sensitive_cb.get_active(): index = string.find(msg.parts_text[0], find_me) else: # all munched to lower case index = string.find(string.lower(msg.parts_text[0]), string.lower(find_me)) if index != -1: # expand up to it for i in xrange(len(path)-1, 0, -1): self.pynewin.message_list.expand_row(path[:-i], False) selection.select_iter(iter) self.pynewin.message_list.scroll_to_cell(path, self.pynewin.message_list.subject_column, True, 0.5, 0.5) return path = model_path_next(model, path) ptk.misc_widgets.InfoBox (_("Pyne Search"), _("Reached end of search"), gtk.STOCK_OK, parent_win=self)