Source

chirpy / widgets.py

from datetime import datetime, timedelta
from hashlib import sha1
from wx.lib import delayedresult
from gui.generic import ChirpyHTML
from data.models import toDT
from data.config import AVATAR_DIR
import cStringIO
import os
import re
import urllib2
import wx

TWEET_SIZE = (420, 130)
AT_REPLY_RE = re.compile('@(\w+)')
HASH_TAG_RE = re.compile('(#\w+)')
URL_RE = re.compile('((https?|ftp)://([-\w\.]+)+(:\d+)?(/([\w/_\-\.]*(\?\S+)?)?)?)')

class TweetFrame(wx.Frame):
    parent = None

    template = """
<html>
<body bgcolor="%(bg)s" text="%(fg)s" link="%(fg)s" vlink="%(fg)s">
<table border="0">
<tr><td valign="top"><img src="%(avatar)s" height="60" width="60"></td>
<td valign="top">
    <font size="3"><b>
        <a href="http://twitter.com/%(user)s">%(user)s</a>
    </b></font><br>
    <font size="1">Posted %(posted)s ago from %(source)s %(in_reply)s</font><br>
    <font size="2">%(tweet)s</font>
</td></tr></table>
</body>
</html>"""
    def __init__(self, parent=None):
        size = TWEET_SIZE
        style = wx.NO_BORDER | wx.FRAME_NO_TASKBAR | wx.STAY_ON_TOP# | wx.FRAME_TOOL_WINDOW
        wx.Frame.__init__(self, parent, -1, size=size, style=style)
        self.foreground = parent.GetForegroundColour().GetAsString()
        self.background = parent.GetBackgroundColour().GetAsString()
        self.SetParent(parent)

        self.tweet_text = ChirpyHTML(self, -1)

        self.timer = wx.Timer(self, -1)
        self.Bind(wx.EVT_TIMER, self.ForceHide)
        self.Bind(wx.EVT_ENTER_WINDOW, self.ResetTimer)
        self.Bind(wx.EVT_LEAVE_WINDOW, self.Hide)

        self.tweet_text.SetSize(TWEET_SIZE)

    def DisplayAt(self, pos, tweet, user, avatar):
        self.ResetTimer()

        # position the tweet
        self.SetPosition(pos)
        text = tweet.text

        # turn URLs into links
        text = URL_RE.sub(r'<a href="\1">\1</a>', text)

        # link to users in @replies
        text = AT_REPLY_RE.sub(r'@<a href="http://twitter.com/\1">\1</a>', text)

        # link hashtags
        text = HASH_TAG_RE.sub(r'<a href="http://twitter.com/search?=\1">\1</a>', text)

        posted = toDT(tweet.created_at)
        now = datetime.utcnow()
        delta = now - posted

        pluralize = lambda val: val != 1 and 's' or ''

        if delta.days:
            ago = '%i day%s' % (delta.days, pluralize(delta.days))
        else:
            seconds = delta.seconds
            hours = seconds / 3600
            seconds -= hours * 60
            minutes = seconds / 60
            seconds -= minutes * 60

            hp = pluralize(hours)
            mp = pluralize(minutes)
            sp = pluralize(seconds)

            if hours > 0:
                ago = '%i hour%s, %i minute%s' % (hours, hp, minutes, mp)
            elif minutes > 5:
                ago = '%i minute%s' % (minutes, mp)
            else:
                ago = '%i minute%s, %i second%s' % (minutes, mp, seconds, sp)

        params = {
            'bg': self.background,
            'fg': self.foreground,
            'user': tweet.user.screen_name or tweet.user.name,
            'posted': ago,
            'avatar': avatar,
            'tweet': text,
            'source': tweet.source,
            'reply_user': tweet.in_reply_to_screen_name,
            'reply_status': tweet.in_reply_to_status_id,
            'in_reply': ''
        }

        if tweet.in_reply_to_screen_name and tweet.in_reply_to_status_id:
            reply = 'in reply to <a href="http://twitter.com/%(reply_user)s/status/%(reply_status)s">%(reply_user)s</a>'
            params['in_reply'] = reply % params

        self.tweet_text.SetPage(self.template % params)
        self.Show(True)

    def ForceHide(self, evt=None):
        self.timer.Stop()
        self.Hide(force=True)

    def Hide(self, force=False):
        if force:
            super(TweetFrame, self).Hide()
        else:
            self.timer.Start(500)

    def ResetTimer(self, evt=None):
        self.timer.Stop()

    def SetParent(self, parent):
        if parent and not self.parent:
            self.parent = parent
            self.SetBackgroundColour(self.background)
            self.SetForegroundColour(self.foreground)

class TweetButton(wx.BitmapButton):
    frame = None
    avatar = None

    def __init__(self, parent, tweet, user):
        if not TweetButton.frame:
            TweetButton.frame = TweetFrame(parent)

        self.parent = parent
        self.tweet = tweet
        self.user = user
        self.menu = None
        self.is_new = True

        # for the flashing button
        self.flash = self.parent.config.appearance__new
        self.normal = self.parent.GetBackgroundColour()

        zoom = self.parent.config.layout__zoom
        size = zoom * self.parent.parent.DEFAULT_SIZE
        self.button_size = (size, size)
        wx.BitmapButton.__init__(self, parent, -1, self.GetAvatar(),
                                 size=self.button_size, style=wx.NO_3D)
        self.timer = wx.Timer(self, -1)
        self.SetThemeEnabled(False)

        self.Bind(wx.EVT_BUTTON, self.OnClick)
        self.Bind(wx.EVT_ENTER_WINDOW, self.OnMouseOver)
        self.Bind(wx.EVT_LEAVE_WINDOW, self.OnMouseOut)
        self.Bind(wx.EVT_MOTION, self.GetGrandParent().OnMouseMove)
        self.Bind(wx.EVT_MOUSEWHEEL, self.GetGrandParent().OnScroll)
        self.Bind(wx.EVT_RIGHT_DOWN, self.GetGrandParent().OnRightDown)
        self.Bind(wx.EVT_RIGHT_UP, self.GetGrandParent().OnRightUp)
        self.Bind(wx.EVT_TIMER, self.ToggleBackground)
        self.Bind(wx.EVT_MENU, self.OnMenu)

        self.timer.Start(500)

    def GetFilenameForAvatar(self, url=None):
        if not url:
            url = self.tweet.user.profile_image_url
        return os.path.join(AVATAR_DIR, sha1(url).hexdigest())

    def GetAvatar(self):
        """
        Attempts to retrieve the user's avatar from Twitter.
        """
        if not self.avatar:
            try:
                url = self.tweet.user.profile_image_url
                filename = self.GetFilenameForAvatar()

                if not os.access(filename, os.R_OK):
                    conn = urllib2.urlopen(url)
                    data = conn.read()
                    conn.close()

                    out = open(filename, 'wb')
                    out.write(data)
                    out.close()

                self.avatar = self.GetImageFromFile(filename)
            except Exception, ex:
                #print 'exception!', ex
                self.avatar = self.GetDefaultAvatar()

        return self.avatar

    def GetDefaultAvatar(self):
        return self.GetImageFromFile('resources/twitter-bird-1.png')

    def GetImageFromFile(self, filename):
        #print 'loading image from file', filename
        return self.GetImageFromData(open(filename, 'rb').read())

    def GetImageFromData(self, data):
        stream = cStringIO.StringIO(data)
        return wx.BitmapFromImage(wx.ImageFromStream(stream))

    def UpdateFavoriteItems(self):
        self.favorite.Enable(not self.tweet.favorited)
        self.unfavorite.Enable(self.tweet.favorited)

    def GetMenu(self):
        """
        Returns a menu that provides the user with options pertaining to this
        tweet
        """
        if not self.menu:
            self.menu = wx.Menu()

            self.menu.Append(wx.ID_INDENT, 'Retweet')
            self.menu.Append(wx.ID_REDO, '@Reply')

            self.favorite = wx.MenuItem(self.menu, wx.ID_ADD, 'Favorite')
            self.unfavorite = wx.MenuItem(self.menu, wx.ID_REMOVE, 'Unfavorite')
            self.menu.AppendItem(self.favorite)
            self.menu.AppendItem(self.unfavorite)

            self.user_menu = wx.Menu()
            self.user_menu.Append(wx.ID_HOME, 'Homepage')
            self.user_menu.Append(wx.ID_DELETE, 'Unfollow')

            self.menu.AppendSeparator()

            self.menu.AppendMenu(-1,
                                 self.tweet.user.screen_name,
                                 self.user_menu)

            self.menu.AppendSeparator()

            self.menu.Append(wx.ID_NEW, 'New Tweet')

        self.UpdateFavoriteItems()

        return self.menu

    def OnClick(self, evt=None):
        """
        Displays a menu with various options relating to this tweet
        """
        self.SetNotNew()
        TweetButton.frame.Hide()
        self.PopupMenu(self.GetMenu())

    def OnMenu(self, evt):
        action = {
            wx.ID_REDO: self.Reply,
            wx.ID_INDENT: self.Retweet,
            wx.ID_ADD: self.Favorite,
            wx.ID_REMOVE: self.Favorite,
        }.get(evt.GetId())

        if action:
            action(evt)

    def Reply(self, evt=None):
        self.GetGrandParent().Reply(self.tweet, self.user)

    def Retweet(self, evt=None):
        self.GetGrandParent().Retweet(self.tweet, self.user)

    def Favorite(self, evt=None):
        self.GetGrandParent().Favorite(self.tweet, self.user)

    def OnMouseOver(self, evt=None):
        """
        Displays the tweet
        """
        self.SetNotNew()
        TweetButton.frame.DisplayAt(self.DetermineTweetLocation(), self.tweet,
                                    self.user, self.GetFilenameForAvatar())

    def OnMouseOut(self, evt=None):
        """
        Hides the tweet
        """
        self.SetNotNew()
        TweetButton.frame.Hide()

    def DetermineTweetLocation(self):
        """
        Determines where the tweet window should appear.
        """
        maxLeft, maxTop = wx.GetDisplaySize()
        curLeft, curTop = self.GetScreenPositionTuple()
        left, top = 0, 0

        if self.parent.config.layout__orientation == wx.VERTICAL:
            if maxLeft - TWEET_SIZE[0] <= curLeft:
                left = curLeft - TWEET_SIZE[0]
            else:
                left = curLeft + self.button_size[0]

            if maxTop - TWEET_SIZE[1] <= curTop:
                top = curTop - TWEET_SIZE[1] + self.button_size[0]
            else:
                top = curTop
        else:
            if maxLeft - TWEET_SIZE[0] <= curLeft:
                left = curLeft - TWEET_SIZE[0] + self.button_size[0]
            else:
                left = curLeft

            if maxTop - TWEET_SIZE[1] <= curTop:
                top = curTop - TWEET_SIZE[1]
            else:
                top = curTop + self.GetSizeTuple()[1]

        return (left, top)

    def SetNotNew(self):
        self.timer.Stop()
        self.SetBackgroundColour(self.normal)
        self.is_new = False

        try:
            # remove this button from the list of new tweets
            self.parent.new_tweets.remove(self)
            self.SetBitmapLabel(self.GetAvatar())
        except ValueError:
            pass

    def ToggleBackground(self, evt=None):
        if self.is_new:
            if self.parent.config.behavior__flash_new:
                bg = self.GetBackgroundColour() == self.flash and self.normal or self.flash
            else:
                bg = self.flash

            # this douchehackery is thanks to Winders...
            if bg == self.flash:
                avatar = self.GetDefaultAvatar()
            else:
                avatar = self.GetAvatar()

            self.SetBitmapLabel(avatar)
            self.SetBackgroundColour(bg)

class TweetPanel(wx.Panel):
    buttons = []
    new_tweets = []
    def __init__(self, parent, orientation=wx.HORIZONTAL):
        style = wx.NO_FULL_REPAINT_ON_RESIZE
        wx.Panel.__init__(self, parent, -1, style=style)
        self.parent = parent
        self.config = parent.config
        self.orientation = orientation

        self.SetBackgroundColour(self.config.appearance__background)
        self.SetForegroundColour(self.config.appearance__foreground)
        self.Bind(wx.EVT_MOTION, self.parent.OnMouseMove)
        self.Bind(wx.EVT_RIGHT_DOWN, self.parent.OnRightDown)
        self.Bind(wx.EVT_RIGHT_UP, self.parent.OnRightUp)

        self.sizer = wx.BoxSizer(orientation)
        self.SetSizer(self.sizer)
        self.sizer.Fit(self.parent)
        self.SetAutoLayout(True)

    def AddTweet(self, tweet, user):
        """
        Creates a new TweetButton and adds it to the panel.
        """
        btn = TweetButton(self, tweet, user)
        self.buttons.append(btn)
        self.new_tweets.append(btn)
        self.sizer.Prepend(btn, 0)

    def AddTweets(self, tweet_list, user):
        for tweet in tweet_list:
            self.AddTweet(tweet, user)
        self.sizer.Layout()

        width = height = self.parent.ZOOM_SIZE
        size = len(self.buttons) * self.parent.ZOOM_SIZE
        if self.orientation == wx.HORIZONTAL:
            width = size
        else:
            height = size
        self.SetSize((width, height))
        self.Refresh(False)
        self.parent.SetSize(self.parent.DetermineSize())