Julio Biason
15 years ago
2 changed files with 401 additions and 354 deletions
@ -0,0 +1,364 @@
|
||||
#!/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 |
Loading…
Reference in new issue