Browse Source

More american spelling; added an avatar cache to be shared between all

the grids. Messages are displayed properly now (although the interface
doesn't respond properly when messages are selected.)
master
Julio Biason 15 years ago
parent
commit
e2c2379191
  1. 125
      mitterlib/ui/helpers/gdk_avatarcache.py
  2. 37
      mitterlib/ui/helpers/gtk_messagegrid.py
  3. 143
      mitterlib/ui/ui_pygtk.py

125
mitterlib/ui/helpers/gdk_avatarcache.py

@ -0,0 +1,125 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Mitter, a micro-blogging client with multiple interfaces.
# Copyright (C) 2007-2010 Mitter Contributors
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import logging
import urllib2
import gtk
# ----------------------------------------------------------------------
# Constants
# ----------------------------------------------------------------------
_log = logging.getLogger('ui.helpers.avatarcache')
# ----------------------------------------------------------------------
# The cache class
# ----------------------------------------------------------------------
class AvatarCache(object):
"""An avatar cache, storing GDK pixbufs references."""
def __init__(self, connection, default, timeout=None):
"""Initialization of the cache.
connection:
A :class:`ThreadManager`, which will be used as thread pool to
request the avatars.
default:
A gdk_pixbuf to be used as placeholder while the real avatar is
being downloaded.
timeout:
Request timeout when downloading the avatars."""
self._connection = connection
self._default = default
self._cache = {}
self._timeout = timeout
def get(self, owner, avatar, url=None):
"""Return the avatar from the cache or, if the avatar isn't available
yet, returns the default avatar and queue the download of it.
owner:
The object requesting the avatar. Must have a "queue_draw()"
method, which will be called once the avatar is available. Can be
None.
avatar:
The identification of the avatar. Can be any string.
url:
URL to download the avatar. If not passed, *avatar* **must** be a
valid URL."""
if avatar in self._cache:
return self._cache[avatar]
if not url:
url = avatar
self._connection.add_work(self._post_avatar_download,
self._exception_avatar_download,
self._download_pic,
avatar,
url,
owner)
# add the default avatar in the cache just to avoid a new request for
# it. It will be overwritten anyway later.
self._cache[avatar] = self._default
return self._default
# ------------------------------------------------------------
# Network functions
# ------------------------------------------------------------
def _download_pic(self, avatar, url, owner):
"""Download a picture from the web. Can be used in a thread."""
request = urllib2.Request(url=url)
timeout = self._timeout
_log.debug('Starting request of %s (timeout %ds)' % (
url, timeout))
response = urllib2.urlopen(request, timeout=timeout)
data = response.read()
_log.debug('Request completed')
return (avatar, owner, data)
def _post_avatar_download(self, widget, data):
"""Called after the data from the picture is available."""
(avatar, owner, data) = data
loader = gtk.gdk.PixbufLoader()
loader.write(data)
loader.close()
user_pic = loader.get_pixbuf()
user_pic = user_pic.scale_simple(48, 48, gtk.gdk.INTERP_BILINEAR)
self._cache[avatar] = user_pic
if owner:
owner.queue_draw()
return
def _exception_avatar_download(self, widget, exception):
"""Called in case we have a problem downloading an user avatar."""
_log.debug('Exception trying to get an avatar.')
_log.debug(str(exception))
return

37
mitterlib/ui/helpers/gtk_messagegrid.py

@ -22,6 +22,7 @@ import gtk
import logging import logging
import pango import pango
import re import re
import gettext
from cgi import escape as html_escape from cgi import escape as html_escape
@ -30,7 +31,7 @@ import timesince
# ---------------------------------------------------------------------- # ----------------------------------------------------------------------
# Constants for the message # Constants for the message
# ---------------------------------------------------------------------- # ----------------------------------------------------------------------
MESSAGE_FORMAT = ('%(favourite_star)s' MESSAGE_FORMAT = ('%(favorite_star)s'
'<b>%(full_name)s</b> ' '<b>%(full_name)s</b> '
'<small>(%(username)s' '<small>(%(username)s'
'%(message_type)s' '%(message_type)s'
@ -47,6 +48,13 @@ URL_RE = re.compile(r'(https?://[^\s\n\r]+)', re.I)
# ---------------------------------------------------------------------- # ----------------------------------------------------------------------
_log = logging.getLogger('ui.pygtk.messagegrid') _log = logging.getLogger('ui.pygtk.messagegrid')
# ----------------------------------------------------------------------
# I18n bits
# ----------------------------------------------------------------------
t = gettext.translation('ui_pygtk', fallback=True)
_ = t.gettext
N_ = t.ngettext
# ---------------------------------------------------------------------- # ----------------------------------------------------------------------
# MessageGrid class # MessageGrid class
# ---------------------------------------------------------------------- # ----------------------------------------------------------------------
@ -71,10 +79,10 @@ class MessageGrid(gtk.ScrolledWindow, gobject.GObject):
self.emit('count-changed', messages) self.emit('count-changed', messages)
return return
def __init__(self): def __init__(self, avatar_cache):
super(MessageGrid, self).__init__() super(MessageGrid, self).__init__()
self.avatars = None self.avatars = avatar_cache
self.link_color = '#0000ff' self.link_color = '#0000ff'
self.unread_char = '(o)' self.unread_char = '(o)'
self.favorite_char = '(F)' self.favorite_char = '(F)'
@ -128,7 +136,7 @@ class MessageGrid(gtk.ScrolledWindow, gobject.GObject):
data = store.get_value(position, 0) data = store.get_value(position, 0)
pic = data.author.avatar pic = data.author.avatar
cell.set_property('pixbuf', self.avatars[pic]) cell.set_property('pixbuf', self.avatars.get(self, pic))
return return
@ -164,10 +172,10 @@ class MessageGrid(gtk.ScrolledWindow, gobject.GObject):
else: else:
message_values['read_status'] = '' message_values['read_status'] = ''
if data.favourite: if data.favorite:
message_values['favourite_star'] = (self.unfavourite_char) message_values['favorite_star'] = (self.unfavorite_char)
else: else:
message_values['favourite_star'] = (self.favourite_char) message_values['favorite_star'] = (self.favorite_char)
info = [] info = []
if data.reposted_by: if data.reposted_by:
@ -186,6 +194,10 @@ class MessageGrid(gtk.ScrolledWindow, gobject.GObject):
message_values['message_type'] = ''.join(info) message_values['message_type'] = ''.join(info)
markup = MESSAGE_FORMAT % (message_values) markup = MESSAGE_FORMAT % (message_values)
# This log is massive, but it can help when the grid doesn't show any
# messages (usually when there is something broken with the markup.)
#_log.debug('Markup:\n%s' % (markup))
cell.set_property('markup', markup) cell.set_property('markup', markup)
return return
@ -205,10 +217,10 @@ class MessageGrid(gtk.ScrolledWindow, gobject.GObject):
data = model.get_value(iter, 0) data = model.get_value(iter, 0)
data.read = True data.read = True
self._delete_action.set_property('sensitive', data.deletable) #self._delete_action.set_property('sensitive', data.deletable)
self._reply_action.set_property('sensitive', data.replyable) #self._reply_action.set_property('sensitive', data.replyable)
self._repost_action.set_property('sensitive', data.repostable) #self._repost_action.set_property('sensitive', data.repostable)
self._favourite_action.set_property('sensitive', data.favoritable) #self._favorite_action.set_property('sensitive', data.favoritable)
return 0 return 0
@ -333,6 +345,9 @@ class MessageGrid(gtk.ScrolledWindow, gobject.GObject):
iter = model.iter_next(iter) iter = model.iter_next(iter)
return return
#-----------------------------------------------------------------------
# Public functions
#-----------------------------------------------------------------------
def update(self, messages): def update(self, messages):
"""Update the grid with new messages.""" """Update the grid with new messages."""
self._grid.freeze_notify() self._grid.freeze_notify()

143
mitterlib/ui/ui_pygtk.py

@ -17,13 +17,12 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import gtk
import gobject import gobject
import gtk
gobject.threads_init() gobject.threads_init()
import logging import logging
import re
import urllib2 import urllib2
import webbrowser import webbrowser
import gettext import gettext
@ -32,6 +31,7 @@ from mitterlib.ui.helpers.image_helpers import find_image
from mitterlib.ui.helpers.gtk_threading import ThreadManager from mitterlib.ui.helpers.gtk_threading import ThreadManager
from mitterlib.ui.helpers.gtk_updatebox import UpdateBox from mitterlib.ui.helpers.gtk_updatebox import UpdateBox
from mitterlib.ui.helpers.gtk_messagegrid import MessageGrid from mitterlib.ui.helpers.gtk_messagegrid import MessageGrid
from mitterlib.ui.helpers.gdk_avatarcache import AvatarCache
from mitterlib.constants import gpl_3, version from mitterlib.constants import gpl_3, version
# ---------------------------------------------------------------------- # ----------------------------------------------------------------------
@ -88,9 +88,31 @@ class Interface(object):
) )
self._statusbar = self._create_statusbar() self._statusbar = self._create_statusbar()
# the messages grid
messages = MessageGrid(self._avatars)
messages.link_color = self._options[self.NAMESPACE]['link_color']
messages.unread_char = self._options[self.NAMESPACE]['unread_char']
messages.favorite_char = (
self._options[self.NAMESPACE]['favorite_char'])
messages.unfavorite_char = (
self._options[self.NAMESPACE]['unfavorite_char'])
messages.protected_char = (
self._options[self.NAMESPACE]['protected_char'])
# replies grid
replies = MessageGrid(self._avatars)
replies.link_color = self._options[self.NAMESPACE]['link_color']
replies.unread_char = self._options[self.NAMESPACE]['unread_char']
replies.favorite_char = (
self._options[self.NAMESPACE]['favorite_char'])
replies.unfavorite_char = (
self._options[self.NAMESPACE]['unfavorite_char'])
replies.protected_char = (
self._options[self.NAMESPACE]['protected_char'])
self._main_tabs = gtk.Notebook() self._main_tabs = gtk.Notebook()
self._main_tabs.insert_page(MessageGrid(), gtk.Label('Messages')) self._main_tabs.insert_page(messages, gtk.Label('Messages'))
self._main_tabs.insert_page(MessageGrid(), gtk.Label('Replies')) self._main_tabs.insert_page(replies, gtk.Label('Replies'))
update_box = gtk.VBox() update_box = gtk.VBox()
update_box.set_property('border_width', 2) update_box.set_property('border_width', 2)
@ -116,8 +138,6 @@ class Interface(object):
self._statusbar.show_all() self._statusbar.show_all()
box.show() box.show()
self._message_count_updated()
# now that all elements are created, connect the signals # now that all elements are created, connect the signals
main_window.connect('destroy', self._quit_app) main_window.connect('destroy', self._quit_app)
main_window.connect('delete-event', self._quit_app) main_window.connect('delete-event', self._quit_app)
@ -266,12 +286,12 @@ class Interface(object):
self._repost_action.connect('activate', self._repost_message) self._repost_action.connect('activate', self._repost_message)
action_group.add_action_with_accel(self._repost_action, '<Ctrl>p') action_group.add_action_with_accel(self._repost_action, '<Ctrl>p')
self._favourite_action = gtk.Action('Favourite', _('_Favorite'), self._favorite_action = gtk.Action('Favourite', _('_Favorite'),
_('Toggle the favorite status of a message'), _('Toggle the favorite status of a message'),
gtk.STOCK_ABOUT) gtk.STOCK_ABOUT)
self._favourite_action.set_property('sensitive', False) self._favorite_action.set_property('sensitive', False)
self._favourite_action.connect('activate', self._favourite_message) self._favorite_action.connect('activate', self._favorite_message)
action_group.add_action_with_accel(self._favourite_action, '<Ctrl>f') action_group.add_action_with_accel(self._favorite_action, '<Ctrl>f')
# view actions # view actions
view_messages_action = gtk.Action('Messages', _('_Messages'), view_messages_action = gtk.Action('Messages', _('_Messages'),
@ -367,25 +387,6 @@ class Interface(object):
self._reply_message_id = None self._reply_message_id = None
return return
def _message_count_updated(self):
"""Update the elements in the interface that should change in case of
changes in the message count."""
child = self._main_tabs.get_nth_page(0)
self._main_tabs.set_tab_label_text(child, _('Messages (%d)') %
(self._new_message_count))
child = self._main_tabs.get_nth_page(1)
self._main_tabs.set_tab_label_text(child, _('Replies (%d)') %
(self._new_replies_count))
if self._statusicon:
if (self._new_message_count + self._new_replies_count) == 0:
self._statusicon.set_from_pixbuf(self._images['icon'])
else:
self._statusicon.set_from_pixbuf(
self._images['new-messages'])
return
# ------------------------------------------------------------ # ------------------------------------------------------------
# Widget callback functions # Widget callback functions
@ -528,22 +529,22 @@ class Interface(object):
message) message)
return return
def _favourite_message(self, widget, user_data=None): def _favorite_message(self, widget, user_data=None):
"""Toggle the favourite status of a message.""" """Toggle the favorite status of a message."""
(grid, counter) = self._grids[self._main_tabs.get_current_page()] (grid, counter) = self._grids[self._main_tabs.get_current_page()]
(model, iter) = grid.get_selection().get_selected() (model, iter) = grid.get_selection().get_selected()
message = model.get_value(iter, 0) message = model.get_value(iter, 0)
self._favourite_info = (grid, iter) self._favorite_info = (grid, iter)
if message.favourite: if message.favorite:
display = _('Removing message from %s from favorites...') display = _('Removing message from %s from favorites...')
else: else:
display = _('Marking message from %s as favorite...') display = _('Marking message from %s as favorite...')
self._update_statusbar(display % (message.author.username)) self._update_statusbar(display % (message.author.username))
self._threads.add_work(self._post_favourite_message, self._threads.add_work(self._post_favorite_message,
self._exception_favourite_message, self._exception_favorite_message,
self._connection.favourite, self._connection.favorite,
message) message)
return return
@ -665,7 +666,7 @@ class Interface(object):
self._delete_action.set_property('sensitive', False) self._delete_action.set_property('sensitive', False)
self._reply_action.set_property('sensitive', False) self._reply_action.set_property('sensitive', False)
self._repost_action.set_property('sensitive', False) self._repost_action.set_property('sensitive', False)
self._favourite_action.set_property('sensitive', False) self._favorite_action.set_property('sensitive', False)
return return
def _new_message(self, widget, user_data=None): def _new_message(self, widget, user_data=None):
@ -746,24 +747,6 @@ class Interface(object):
_log.debug('Request completed') _log.debug('Request completed')
return (url, data) return (url, data)
### Results from the picture request
def _post_download_pic(self, widget, data):
"""Called after the data from the picture is available."""
(url, data) = data
loader = gtk.gdk.PixbufLoader()
loader.write(data)
loader.close()
user_pic = loader.get_pixbuf()
user_pic = user_pic.scale_simple(48, 48, gtk.gdk.INTERP_BILINEAR)
self._avatars[url] = user_pic
page = self._main_tabs.get_current_page()
grid = self._main_tabs.get_nth_page(page)
grid.queue_draw()
return
### Results from the avatar download request ### Results from the avatar download request
def _post_avatar_pic(self, widget, data): def _post_avatar_pic(self, widget, data):
"""Called after the data from the picture of the user's avatar is """Called after the data from the picture of the user's avatar is
@ -843,23 +826,23 @@ class Interface(object):
error_win.hide() error_win.hide()
return return
### Results from the favourite call ### Results from the favorite call
def _post_favourite_message(self, widget, data): def _post_favorite_message(self, widget, data):
"""Called when the message was favourited successfully.""" """Called when the message was favorited successfully."""
_log.debug('Favourite status changed.') _log.debug('Favourite status changed.')
(grid, iter) = self._favourite_info (grid, iter) = self._favorite_info
message = grid.get_model().get_value(iter, 0) message = grid.get_model().get_value(iter, 0)
if message.favourite: if message.favorite:
display = _('Message unfavorited.') display = _('Message unfavorited.')
else: else:
display = _('Message favorited.') display = _('Message favorited.')
self._update_statusbar(display) self._update_statusbar(display)
message.favourite = not message.favourite message.favorite = not message.favorite
self._favourite_info = None self._favorite_info = None
return return
def _exception_favourite_message(self, widget, exception): def _exception_favorite_message(self, widget, exception):
"""Called when the message couldn't be favourited.""" """Called when the message couldn't be favorited."""
_log.debug('Favourite error.') _log.debug('Favourite error.')
_log.debug(str(exception)) _log.debug(str(exception))
@ -879,13 +862,6 @@ class Interface(object):
"""Start the interface. `connection` is the :class:`Networks` object """Start the interface. `connection` is the :class:`Networks` object
with all the available networks. `options` is the :class:`ConfigOpt` with all the available networks. `options` is the :class:`ConfigOpt`
object with the configuration to run Mitter.""" object with the configuration to run Mitter."""
self._connection = connection
self._options = options
self._avatars = {}
self._pic_queue = set()
# Load images # Load images
unknown_pixbuf = find_image('unknown.png') unknown_pixbuf = find_image('unknown.png')
if unknown_pixbuf: if unknown_pixbuf:
@ -900,6 +876,9 @@ class Interface(object):
self._images['logo'] = gtk.gdk.pixbuf_new_from_file( self._images['logo'] = gtk.gdk.pixbuf_new_from_file(
find_image('mitter-big.png')) find_image('mitter-big.png'))
self._connection = connection
self._options = options
# icons (app and statusicon) # icons (app and statusicon)
self._images['icon'] = gtk.gdk.pixbuf_new_from_file( self._images['icon'] = gtk.gdk.pixbuf_new_from_file(
find_image('mitter.png')) find_image('mitter.png'))
@ -912,9 +891,7 @@ class Interface(object):
# interthread communication. # interthread communication.
self._delete_info = None self._delete_info = None
self._reply_message_id = None self._reply_message_id = None
self._favourite_info = None self._favorite_info = None
self._new_message_count = 0 # TODO: Turn this into a @property
self._new_replies_count = 0 # TODO: Turn this into a @property
return return
@ -929,11 +906,15 @@ class Interface(object):
else: else:
self._statusicon = None self._statusicon = None
self._threads = ThreadManager()
self._avatars = AvatarCache(
self._threads,
self._images['avatar'],
self._options['NetworkManager']['timeout'])
self._main_window = self._create_main_window() self._main_window = self._create_main_window()
self._main_window.show() self._main_window.show()
self._threads = ThreadManager()
# get a user avatar. We need that for the updatebox. # get a user avatar. We need that for the updatebox.
user = self._connection.user() user = self._connection.user()
self._threads.add_work(self._post_avatar_pic, self._threads.add_work(self._post_avatar_pic,
@ -1007,7 +988,7 @@ class Interface(object):
is_cmd_option=False) is_cmd_option=False)
options.add_option( options.add_option(
group=self.NAMESPACE, group=self.NAMESPACE,
option='link_colour', option='link_color',
help='Color of links in the interface', help='Color of links in the interface',
type='str', type='str',
metavar='COLOR', metavar='COLOR',
@ -1032,17 +1013,17 @@ class Interface(object):
is_cmd_option=False) is_cmd_option=False)
options.add_option( options.add_option(
group=self.NAMESPACE, group=self.NAMESPACE,
option='unfavourite_char', option='unfavorite_char',
help='String to be used to indicate a message is not ' \ help='String to be used to indicate a message is not ' \
'marked as favourite.', 'marked as favorite.',
metavar='CHAR', metavar='CHAR',
default='&#9733;', default='&#9733;',
is_cmd_option=False) is_cmd_option=False)
options.add_option( options.add_option(
group=self.NAMESPACE, group=self.NAMESPACE,
option='favourite_char', option='favorite_char',
help='String to be used to indicate a message is marked ' \ help='String to be used to indicate a message is marked ' \
'as favourite.', 'as favorite.',
metavar='CHAR', metavar='CHAR',
default='&#9734;', default='&#9734;',
is_cmd_option=False) is_cmd_option=False)

Loading…
Cancel
Save