import re import datetime, dateutil.tz import gtk, gobject from threading import RLock from avcache import AvCache class TweetPane(gtk.ScrolledWindow): ''' Box that holds all the TweetBoxes for a given feed This box will not update itself, the parent should do that. It will connect num_entries listeners to its parent's on_reply() and on_retweet() It also gets some data from its parent, including num_entries ''' def __init__(self, list_name, num_entries=20, single_tweet=None, is_user=False, conversation=False): gtk.ScrolledWindow.__init__(self) self.list_name = list_name self.single_tweet = single_tweet self.conversation = conversation self.num_entries = num_entries self.is_user = is_user self.following = False self.verified = False self.tab_label = CloseTabLabel(self.list_name) self.message = gtk.Label('Loading...') # These handle determining which tweets are unread self.last_tweet_read = None self.latest_tweet = None self.num_new_tweets = 0 self.tweets = [] self.init_widgets() def init_widgets(self): self.tab_label.connect('label-clicked', self.set_tweets_read_callback) tweet_box = gtk.VBox() viewport = gtk.Viewport() # Build us some labels... tweet_box.pack_start(self.message) if self.is_user: self.user_box = UserBox() self.user_box.connect('at-clicked', self.on_at_clicked) self.user_box.connect('follow-clicked', self.on_follow_clicked) tweet_box.pack_start(self.user_box) for i in range(0, self.num_entries): self.tweets.append(TweetBox(self.conversation, self.is_user)) tweet_box.pack_start(self.tweets[i], expand=False) self.tweets[i].connect('reply', self.on_tweet_reply) self.tweets[i].connect('retweet', self.on_retweet) self.tweets[i].connect('in-reply-to', self.on_tweet_reply_to) self.tweets[i].connect('conversation', self.on_tweet_conversation) self.tweets[i].connect('show-user', self.on_show_user) self.tweets[i].connect('tweet-read', self.set_tweets_read_callback) viewport.add(tweet_box) # Several different actions should mark the tweets as 'read' self.connect('focus', self.set_tweets_read_callback) viewport.connect('button-press-event', self.set_tweets_read_callback) self.connect('scroll-event', self.set_tweets_read_callback) self.connect('scroll-child', self.set_tweets_read_callback) self.add(viewport) self.set_policy(gtk.POLICY_NEVER, gtk.POLICY_ALWAYS) self.show_all() for tweet in self.tweets: tweet.hide() def update_window(self, statuses): if statuses is None: self.message.set_label('An error occurred while fetching data') self.message.show() for i in range(0, self.num_entries): self.tweets[i].hide() return self.message.hide() # If this is our first load of this list, don't treat anything as new! if self.last_tweet_read is None: try: ids = [status.id for status in statuses] ids.sort() ids.reverse() self.last_tweet_read = ids[0] except IndexError: self.last_tweet_read = 0 # Keep count of the new tweets for posting a status message self.num_new_tweets = 0 for i in range(0, self.num_entries): read = True if i < len(statuses): if statuses[i].id > self.last_tweet_read: self.num_new_tweets += 1 read = False self.tweets[i].set_status(statuses[i], read) self.tweets[i].show() else: self.tweets[i].clear_status() self.tweets[i].hide() if len(statuses) == 0: self.message.set_label('There is no data to display') try: self.latest_tweet = statuses[0].id except IndexError: self.latest_tweet = 0 self.update_tab_label() # Update the user_box with profile icon, name, etc... # Thread calling this should have the gtk lock... def update_user_info(self, user): if self.is_user: self.user_box.update_info(user) # Update the label with the number of unread tweets def update_tab_label(self): pane_text = self.list_name if self.num_new_tweets > 0: pane_text += ' (' + str(self.num_new_tweets) + ')' self.tab_label.set_label_text(pane_text) def get_list_name(self): return self.list_name def set_tweets_read(self): self.last_tweet_read = self.latest_tweet self.num_new_tweets = 0 self.update_tab_label() def set_tweets_read_callback(self, event, arg1=None, arg2=None): self.set_tweets_read() def get_tab_label(self): return self.tab_label def get_single_tweet(self): return self.single_tweet def get_conversation(self): return self.conversation def on_tweet_reply(self, widget): self.emit('tweet-reply', {'screen_name': widget.screen_name, 'id': widget.id}) def on_retweet(self, widget): self.emit('tweet-retweet', {'id': widget.id}) def on_tweet_reply_to(self, widget, data): self.emit('tweet-in-reply-to', data) def on_tweet_conversation(self, widget, data): self.emit('tweet-conversation', data) def on_show_user(self, widget, data): self.emit('show-user', data) def get_is_user(self): return self.is_user def on_at_clicked(self, widget, data): self.emit('at-clicked', data) def on_follow_clicked(self, widget, data): self.emit('follow-clicked', data) ### end class TweetPane # signals for TweetPane gobject.signal_new("tweet-reply", TweetPane, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)) gobject.signal_new("tweet-retweet", TweetPane, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)) gobject.signal_new("tweet-in-reply-to", TweetPane, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)) gobject.signal_new("tweet-conversation", TweetPane, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)) gobject.signal_new("show-user", TweetPane, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)) gobject.signal_new("follow-clicked", TweetPane, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)) gobject.signal_new("at-clicked", TweetPane, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)) class TweetBox(gtk.HBox): ''' GUI for displaying one tweet and associated buttons Also stores the data necessary for replying or retweeting (id, screen name) ''' def __init__(self, conversation=False, is_user=False): gtk.HBox.__init__(self) self.screen_name = None self.id = None self.in_reply_to_id = None self.in_reply_to_screen_name = None # Lets the tweetbox know if it is part of a conversation or not self.conversation = conversation self.is_user = is_user self.init_widgets() def init_widgets(self): # Build the image if not self.is_user: self.avatar = gtk.Image() self.avatar.set_alignment(0.0, 0.0) self.pack_start(self.avatar, expand=False, fill=False) self.avatar.hide() # Everything else goes in a VBox beside the image text_box = gtk.VBox() self.pack_start(text_box) ## Build the header self.header = gtk.Button() label_eb = gtk.EventBox() label_eb.add(self.header) text_box.pack_start(label_eb) # Set the header's properties label_eb.modify_text(gtk.STATE_NORMAL,gtk.gdk.color_parse("#ffffff")) label_eb.modify_bg(gtk.STATE_NORMAL,gtk.gdk.color_parse("#8888ff")) self.header.set_relief(gtk.RELIEF_NONE) self.header.set_alignment(0.0, 0.0) # Handle the header being clicked if self.is_user: self.header.set_sensitive(False) else: self.header.connect('clicked', self.on_user_clicked) ## Build the text self.text = gtk.Label() text_align = gtk.Alignment() text_align.add(self.text) self.text_eb = gtk.EventBox() self.text_eb.add(text_align) text_box.pack_start(self.text_eb) # Set the text's properties text_align.set_padding(2, 5, 10, 5) self.text.set_alignment(0.0, 0.0) self.text.set_selectable(True) self.text.set_line_wrap(True) if gtk.gtk_version[0] > 2 or (gtk.gtk_version[0] == 2 and gtk.gtk_version[1] >= 18): self.text.connect('activate-link', self.on_url_clicked) self.text.connect('button-press-event', self.on_mouse_clicked) self.text_eb.connect('button-press-event', self.on_mouse_clicked) # Build the buttons button_box_align = gtk.Alignment() button_box_align.set_padding(0, 15, 0, 0) button_box = gtk.HBox() text_box.pack_start(button_box) self.reply_to_button = gtk.Button("") self.reply_to_button.set_relief(gtk.RELIEF_NONE) button_box.pack_start(self.reply_to_button, expand=False) self.reply_to_button.connect("clicked", self.on_in_reply_to_clicked) self.conversation_button = gtk.Button("(conversation)") self.conversation_button.set_relief(gtk.RELIEF_NONE) button_box.pack_start(self.conversation_button, expand=False) self.conversation_button.connect("clicked", self.on_conversation_clicked) reply_button = gtk.Button("Reply") reply_button.set_relief(gtk.RELIEF_HALF) button_box.pack_end(reply_button, expand=False) reply_button.connect("clicked", self.on_reply_clicked) retweet_button = gtk.Button("Retweet") retweet_button.set_relief(gtk.RELIEF_HALF) button_box.pack_end(retweet_button, expand=False) retweet_button.connect("clicked", self.on_retweet_clicked) def set_status(self, status, read=True): # To avoid leftover data when reusing self.clear_status() # Set avatar if not self.is_user: try: with AvCache().lock: self.avatar.set_from_pixbuf(AvCache().map[status.user.screen_name]) self.avatar.show() except KeyError: self.avatar.hide() self.set_read(read) timezone = dateutil.tz.gettz() time_format = "%Y.%m.%d %H:%M:%S %Z" # Get the user object user = status.user # Get user's data for retweeting / replying self.screen_name = user.screen_name self.id = status.id self.in_reply_to_id = status.in_reply_to_status_id self.in_reply_to_screen_name = status.in_reply_to_screen_name # ... and a formatted timestamp timestamp = datetime.datetime.strptime(status.created_at, "%a %b %d %H:%M:%S +0000 %Y") timestamp = timestamp.replace(tzinfo=dateutil.tz.gettz('UTC')) timestring = timestamp.astimezone(timezone).strftime(time_format) # Set the header if self.is_user: header_text = timestring else: header_text = user.name + " (" + user.screen_name + ") " + timestring self.header.set_label(header_text) # and the text new_text = status.text new_text = re.sub(r'&([^;]*?)( |$)', r'&\1\2', new_text) new_text = re.sub(r'"([^;]*?)( |$)', r'"\1\2', new_text) if gtk.gtk_version[0] > 2 or (gtk.gtk_version[0] == 2 and gtk.gtk_version[1] >= 18): new_text = re.sub(r"(http://.*?)( |$)", r'\1\2', new_text) new_text = re.sub(r'@(.*?)( |$)', r'@\1\2', new_text) self.text.set_markup(new_text) # If this is in reply to something, set appropriate label if not self.conversation and self.in_reply_to_screen_name and self.in_reply_to_id: self.reply_to_button.set_label('in reply to ' + self.in_reply_to_screen_name) self.conversation_button.show() def clear_status(self): self.header.set_label('') self.text.set_markup('') self.screen_name = None self.id = None self.set_read(True) self.reply_to_button.set_label('') self.conversation_button.hide() if not self.is_user: self.avatar.hide() def set_read(self, read=True): if read: self.text_eb.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse("#f2f1f0")) else: self.text_eb.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse("#dbffdb")) def on_reply_clicked(self, widget): self.set_read() self.emit('reply') def on_retweet_clicked(self, widget): self.set_read() self.emit('retweet') def on_in_reply_to_clicked(self, widget): self.set_read() self.emit('in-reply-to', {'id': self.in_reply_to_id, 'name': self.in_reply_to_screen_name}) def on_conversation_clicked(self, widget): self.set_read() self.emit('conversation', {'id': self.id, 'name': 'conversation'}) def on_user_clicked(self, widget): self.set_read() self.emit('show-user', self.screen_name) def on_mouse_clicked(self, widget, event): if event.button == 1: self.set_read(True) def on_url_clicked(self, widget, uri): self.set_read() if re.match(r'@', uri): self.emit('show-user', re.sub(r'@', '', uri)) return True # end class TweetBox # signals for TweetBox gobject.signal_new("reply", TweetBox, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()) gobject.signal_new("retweet", TweetBox, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()) gobject.signal_new("tweet-read", TweetBox, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()) gobject.signal_new("in-reply-to", TweetBox, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)) gobject.signal_new("conversation", TweetBox, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)) gobject.signal_new("show-user", TweetBox, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)) class UserBox(gtk.VBox): def __init__(self): gtk.VBox.__init__(self) self.data_lock = RLock() self.user_name = None self.following = False self.verified = False self.init_widgets() def init_widgets(self): self.name_label = gtk.Label() self.avatar = gtk.Image() self.follow_button = gtk.Button() at_button = gtk.Button('@') self.follow_label = gtk.Label('You are following this user') self.verified_label = gtk.Label('Verified account') text_col = gtk.VBox() text_col.pack_start(self.name_label, expand=False) text_col.pack_start(self.follow_label, expand=False) text_col.pack_start(self.verified_label, expand=False) info_row = gtk.HBox() info_row.pack_start(self.avatar, expand=False) info_row.pack_start(text_col, expand=False) button_row = gtk.HBox() button_row.pack_start(self.follow_button, expand=False) button_row.pack_start(at_button, expand=False) self.pack_start(info_row, expand=False) self.pack_start(button_row, expand=False) at_button.connect('clicked', self.on_at_clicked) self.follow_button.connect('clicked', self.on_follow_clicked) self.show_all() self.verified_label.hide() self.follow_label.hide() def update_info(self, user): self.user_name = user.screen_name self.name_label.set_text(user.name + ' (' + self.user_name + ')') try: with AvCache().lock: self.avatar.set_from_pixbuf(AvCache().map[user.screen_name]) self.avatar.show() except KeyError: self.avatar.hide() with self.data_lock: self.verified = user.verified if self.verified: self.verified_label.show() else: self.verified_label.hide() def on_follow_clicked(self, event): if self.following: follow = False # destroy the friendship else: follow = True self.emit('follow-clicked', follow) def on_at_clicked(self, widget): self.emit('at-clicked', self.user_name) def get_following(self): with self.data_lock: return self.following def get_verified(self): with self.data_lock: return self.verified def set_following(self, following): with self.data_lock: self.following = following if following: self.follow_button.set_label('Unfollow') self.follow_label.show() else: self.follow_button.set_label('Follow') self.follow_label.hide() # end class UserBox # signals for UserBox gobject.signal_new("follow-clicked", UserBox, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)) gobject.signal_new("at-clicked", UserBox, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)) class CloseTabLabel(gtk.EventBox): ''' This class holds a label and a button with an 'I' in it. This button causes the CloseTabLabel to emit a clicked signal ''' def __init__(self, name=None): gtk.EventBox.__init__(self) self.init_widgets(name) # This code modified from create_custom_tab in: # http://www.daa.com.au/pipermail/pygtk/2006-April/012216.html # # My version of this code is a little heinous, but at least it is # isolated to its own class def init_widgets(self, name): #create a custom tab for notebook containing a #label and a button with STOCK_ICON tabBox = gtk.HBox(False, 2) tabButton=gtk.Button() tabButton.connect('clicked', self.on_clicked) self.label = gtk.Label(name) #Add a picture on a button iconBox = gtk.HBox(False, 0) image = gtk.Image() image.set_from_stock(gtk.STOCK_CLOSE,gtk.ICON_SIZE_MENU) gtk.Button.set_relief(tabButton,gtk.RELIEF_NONE) settings = gtk.Widget.get_settings(tabButton) (w,h) = gtk.icon_size_lookup_for_settings(settings,gtk.ICON_SIZE_MENU) gtk.Widget.set_size_request(tabButton, w + 4, h + 4); iconBox.pack_start(image, True, False, 0) tabButton.add(iconBox) tabBox.pack_start(self.label, False) tabBox.pack_start(tabButton, False) self.connect('button-press-event', self.on_button_press) # needed, otherwise even calling show_all on the notebook won't # make the hbox contents appear. tabBox.show_all() self.add(tabBox) def set_label_text(self, new_text): self.label.set_text(new_text) def on_clicked(self, event): self.emit('close-clicked') def on_button_press(self, event, direction): self.emit('label-clicked') ### end class CloseTabLabel # signals for CloseTabLabel gobject.signal_new("close-clicked", CloseTabLabel, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()) gobject.signal_new("label-clicked", CloseTabLabel, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ())