You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
365 lines
12 KiB
365 lines
12 KiB
15 years ago
|
#!/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 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'
|
||
|
'<b>%(full_name)s</b> '
|
||
|
'<small>(%(username)s'
|
||
|
'%(message_type)s'
|
||
|
')</small>:'
|
||
|
'%(read_status)s '
|
||
|
'%(protected_status)s '
|
||
|
'\n'
|
||
|
'%(message)s\n'
|
||
|
'<small>%(message_age)s</small>')
|
||
|
|
||
|
# ----------------------------------------------------------------------
|
||
|
# 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'<span foreground="%s">\1</span>' % (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'<span foreground="#FF6633">\1</span>',
|
||
|
# 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(_(' — <i>reposted by %s</i>') %
|
||
|
(data.reposted_by.username))
|
||
|
|
||
|
if data.parent_owner:
|
||
|
info.append(_(' — <i>in reply to %s</i>') %
|
||
|
(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
|