#!/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 .
import gobject
import gtk
import logging
import pango
from cgi import escape as html_escape
# ----------------------------------------------------------------------
# String with the format to the message
# ----------------------------------------------------------------------
MESSAGE_FORMAT = ('%(favourite_star)s'
'%(full_name)s '
'(%(username)s'
'%(message_type)s'
'):'
'%(read_status)s '
'%(protected_status)s '
'\n'
'%(message)s\n'
'%(message_age)s')
# ----------------------------------------------------------------------
# Logging
# ----------------------------------------------------------------------
_log = logging.getLogger('ui.pygtk.messagegrid')
# ----------------------------------------------------------------------
# MessageGrid class
# ----------------------------------------------------------------------
class MessageGrid(gtk.ScrolledWindow, gobject.GObject):
"""Custom message grid."""
__gsignals__ = {
"count-changed": (
gobject.SIGNAL_RUN_LAST,
gobject.TYPE_NONE,
(gobject.TYPE_INT, ))} # The exception
@property
def count(self):
"""Number of unread messages."""
return self._message_count
@count.setter
def count(self, messages):
"""Update the number of unread messages."""
self._message_count = messages
self.emit('count-changed', messages)
return
def __init__(self):
super(MessageGrid, self).__init__()
self.avatars = None
self.link_color = '#0000ff'
self.unread_char = '(o)'
self.favorite_char = '(F)'
self.unfavorite_char = '( )'
self.protected_char = '(protected)'
self._message_count = 0
store = gtk.ListStore(object)
store.set_sort_column_id(0, gtk.SORT_ASCENDING)
store.set_sort_func(0, self._order_datetime)
self._grid = gtk.TreeView(store)
self._grid.set_property('headers-visible', False)
self._grid.set_rules_hint(True) # change color for each row
user_renderer = gtk.CellRendererPixbuf()
user_column = gtk.TreeViewColumn('User', user_renderer)
user_column.set_fixed_width(48) # avatar size (we resize to 48x48)
user_column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
user_column.set_cell_data_func(user_renderer,
self._cell_renderer_user)
message_renderer = gtk.CellRendererText()
message_renderer.set_property('wrap-mode', pango.WRAP_WORD)
message_renderer.set_property('wrap-width', 200)
message_column = gtk.TreeViewColumn('Message',
message_renderer)
message_column.set_cell_data_func(message_renderer,
self._cell_renderer_message)
self._grid.append_column(user_column)
self._grid.append_column(message_column)
self._grid.set_resize_mode(gtk.RESIZE_IMMEDIATE)
self._grid.connect('cursor-changed', self._message_selected)
self._grid.connect('button-press-event', self._click_message)
self._grid.connect('popup-menu', self._message_popup) # Menu button
self._grid.connect('cursor-changed', self._mark_message_read)
self.set_policy(gtk.POLICY_NEVER, gtk.POLICY_ALWAYS)
self.add(self._grid)
return
#-----------------------------------------------------------------------
# Renderers
#-----------------------------------------------------------------------
def _cell_renderer_user(self, column, cell, store, position):
"""Callback for the user column. Used to created the pixbuf of the
userpic."""
data = store.get_value(position, 0)
pic = data.author.avatar
cell.set_property('pixbuf', self.avatars[pic])
return
def _cell_renderer_message(self, column, cell, store, position):
"""Callback for the message column. We need this to adjust the markup
property of the cell, as setting it as text won't do any markup
processing."""
data = store.get_value(position, 0)
time = timesince.timesince(data.message_time)
message_values = {}
# unescape escaped entities that pango is not okay with
message_values['message'] = html_escape(data.message)
message_values['username'] = html_escape(data.author.username)
message_values['full_name'] = html_escape(data.author.name)
message_values['message_age'] = time
# highlight URLs
mask = r'\1' % (self.link_color)
message_values['message'] = URL_RE.sub(mask,
message_values['message'])
# use a different highlight for the current user
# TODO: How to handle this with several networks?
#message = re.sub(r'(@'+self.twitter.username+')',
# r'\1',
# message)
if not data.read:
message_values['read_status'] = (' ' + self.unread_char)
else:
message_values['read_status'] = ''
if data.favourite:
message_values['favourite_star'] = (self.unfavourite_char)
else:
message_values['favourite_star'] = (self.favourite_char)
info = []
if data.reposted_by:
info.append(_(' — reposted by %s') %
(data.reposted_by.username))
if data.parent_owner:
info.append(_(' — in reply to %s') %
(data.parent_owner.username))
if data.protected:
message_values['protected_status'] = (self.protected_char)
else:
message_values['protected_status'] = ''
message_values['message_type'] = ''.join(info)
markup = MESSAGE_FORMAT % (message_values)
cell.set_property('markup', markup)
return
#-----------------------------------------------------------------------
# Widget callbacks
#-----------------------------------------------------------------------
def _message_selected(self, view, user_data=None):
"""Callback when a row in the list is selected. Mostly, we'll check
if the message is from the logged user and we change de "sensitive"
property of the message actions to reflect what the user can and
cannot do."""
(model, iter) = view.get_selection().get_selected()
if not iter:
return
data = model.get_value(iter, 0)
data.read = True
self._delete_action.set_property('sensitive', data.deletable)
self._reply_action.set_property('sensitive', data.replyable)
self._repost_action.set_property('sensitive', data.repostable)
self._favourite_action.set_property('sensitive', data.favoritable)
return 0
def _click_message(self, widget, event, user_data=None):
"""Check the click on the message and, if it's a right click, call the
_message_popup function to show the popup."""
if event.button != 3:
# not right click
return False
path = widget.get_path_at_pos(event.x, event.y)
if not path:
return False
(path, _, _, _) = path
return self._url_popup(widget, path, event)
def _message_popup(self, widget, user_data=None):
"""Builds the popup with the URLs in the message."""
_log.debug('Popup')
(path, _) = widget.get_cursor()
if not path:
return True
# create a syntetic event (the popup requires one)
event = gtk.gdk.Event(gtk.gdk.BUTTON_PRESS)
event.button = 3
return self._url_popup(widget, path, event)
def _url_popup(self, widget, path, event):
"""Builds the popup with URLs in the cell pointed by *path*. Requires
the *event* that the widget received."""
iter = widget.get_model().get_iter(path)
message = widget.get_model().get_value(iter, 0)
items = []
if message.link:
items.append((_('Open on %s') % (message.network_name),
message.link))
urls = URL_RE.findall(message.message)
for url in urls:
if len(url) > 20:
title = url[:20] + '...'
else:
title = url
items.append((title, url))
if len(items) == 0:
return
popup = gtk.Menu()
for url in items:
(title, url) = url
item = gtk.MenuItem(title)
item.connect('activate', self._open_url, url)
popup.append(item)
popup.show_all()
popup.popup(None, None, None, event.button, event.time)
return True
def _mark_message_read(self, widget, user_data=None):
"""Mark a message as read when it's selected."""
(path, _) = widget.get_cursor()
if not path:
return True
iter = widget.get_model().get_iter(path)
message = widget.get_model().get_value(iter, 0)
if message.read:
return True
self.count -= 1
return True
def update(self, messages):
"""Update the grid with new messages."""
self._grid.freeze()
store = self._grid.get_model()
for message in messages:
message.read = False
store.prepend([message])
store.sort_column_changed()
self._grid.thaw()
return
def _order_datetime(self, model, iter1, iter2, user_data=None):
"""Used by the ListStore to sort the columns (in our case, "column")
by date."""
message1 = model.get_value(iter1, 0)
message2 = model.get_value(iter2, 0)
if (not message1) or \
(not message1.message_time) or \
(message1.message_time > message2.message_time):
return -1
if (not message2) or \
(not message2.message_time) or \
(message2.message_time > message1.message_time):
return 1
return 0
def clear_posts(self, widget, user_data=None):
"""Clear the list of posts from the grid."""
self._grid.get_model().clear()
self.count = 0;
return
def mark_all_read(self, widget, user_data=None):
"""Mark all messages as read."""
model = self._grid.get_model()
if len(model) == 0:
# no messages, so we don't worry (if I recall correctly,
# get_iter_first will fail if the model is empty)
return
iter = model.get_iter_first()
while iter:
message = model.get_value(iter, 0)
message.read = True
iter = model.iter_next(iter)
self.count = 0
return
def _open_url(self, widget, user_data=None):
"""Opens an URL (used mostly from popup menu items.)"""
webbrowser.open_new_tab(user_data)
return
def update_window_size(self, size):
"""Called when the window size changes, so the grid needs to change
its word-wrapping size."""
model = self._grid.get_model()
if len(model) == 0:
return
column = self._grid.get_column(1) # 0=avatar, 1=message
iter = model.get_iter_first()
path = model.get_path(iter)
width = win_width - 85 # 48 = icon size
# TODO: Find out where those 37 pixels came from and/or if they
# are platform specific.
for renderer in column.get_cell_renderers():
renderer.set_property('wrap-width', width)
while iter:
path = model.get_path(iter)
model.row_changed(path, iter)
iter = model.iter_next(iter)
return