#!/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 _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 _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 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() self._grid.queue_draw() return 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