import re import datetime, dateutil.tz import gtk, gobject from threading import RLock 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): gtk.ScrolledWindow.__init__(self) self.data_lock = RLock() self.updated_once = False self.list_name = list_name self.single_tweet = single_tweet self.num_entries = num_entries if self.single_tweet is not None: self.num_entries = 1 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) for i in range(0, self.num_entries): self.tweets.append(TweetBox()) 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('show-user', self.on_show_user) self.tweets[i].hide() 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 self.updated_once is False: self.updated_once = True # If this is our first load of this list, don't treat anything as new! if self.last_tweet_read is None: try: self.last_tweet_read = statuses[0].id 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 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 updated_once(self): return self.updated_once 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_show_user(self, widget, data): self.emit('show-user', data) 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): print 'debug: set_following(): ' + str(following) with self.data_lock: self.following = following def set_verified(self, verified): print 'debug: set_verified(): ' + str(verified) with self.data_lock: self.verified = verified def get_is_user(self): return self.is_user ### 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("show-user", TweetPane, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)) class TweetBox(gtk.VBox): ''' GUI for displaying one tweet and associated buttons Also stores the data necessary for replying or retweeting (id, screen name) ''' def __init__(self): gtk.VBox.__init__(self) self.screen_name = None self.id = None self.in_reply_to_id = None self.in_reply_to_screen_name = None self.init_widgets() def init_widgets(self): ## Build the header self.header = gtk.Button() label_eb = gtk.EventBox() label_eb.add(self.header) self.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 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) self.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) # Build the buttons button_box_align = gtk.Alignment() button_box_align.set_padding(0, 15, 0, 0) button_box = gtk.HBox() self.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) 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): 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 self.header.set_label(user.name + " (" + user.screen_name + ") " + timestring) # and the text new_text = status.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) self.text.set_markup(new_text) # If this is in reply to something, set appropriate label if self.in_reply_to_screen_name: self.reply_to_button.set_label('in reply to ' + self.in_reply_to_screen_name) def clear_status(self): self.header.set_label('') self.text.set_markup('') self.screen_name = None self.id = None self.set_read(True) 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.emit('reply') def on_retweet_clicked(self, widget): self.emit('retweet') def on_in_reply_to_clicked(self, widget): self.emit('in-reply-to', {'id': self.in_reply_to_id, 'name': self.in_reply_to_screen_name}) def on_user_clicked(self, widget): self.emit('show-user', self.screen_name) def on_mouse_clicked(self, widget, event): if event.button == 1: self.set_read(True) # fixme: call on_url_clicked if there is an active uri # Apparently, this must wait until pygtk 2.18 def on_url_clicked(self, widget, event): # fixme: we're catching this signal just to debug why it doesn't get emitted # seems to be related to EventBox? print 'debug: on_url_clicked()' # 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("in-reply-to", 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 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, ())