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):
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')
self.pack_start(self.name_label)
self.pack_start(self.avatar)
self.pack_start(self.follow_label)
self.pack_start(self.verified_label)
button_row = gtk.HBox()
button_row.pack_start(self.follow_button)
button_row.pack_start(at_button)
self.pack_start(self.button_row)
at_button.connect('clicked', self.on_at_clicked)
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[status.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_text('Unfollow')
self.follow_label.show()
else:
self.follow_button.set_text('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, ())