Browse Source

Grids are now their own object. This should make things easier (although

code is a complete mess at this point.)
master
Julio Biason 14 years ago
parent
commit
0cb152a2d9
  1. 364
      mitterlib/ui/helpers/gtk_messagegrid.py
  2. 391
      mitterlib/ui/ui_pygtk.py

364
mitterlib/ui/helpers/gtk_messagegrid.py

@ -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(_(' &#8212; <i>reposted by %s</i>') %
(data.reposted_by.username))
if data.parent_owner:
info.append(_(' &#8212; <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

391
mitterlib/ui/ui_pygtk.py

@ -1,7 +1,7 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Mitter, a multiple-interface client for microblogging services.
# 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
@ -28,11 +28,10 @@ import urllib2
import webbrowser
import gettext
from cgi import escape as html_escape
from mitterlib.ui.helpers.image_helpers import find_image
from mitterlib.ui.helpers.gtk_threading import ThreadManager
from mitterlib.ui.helpers.gtk_updatebox import UpdateBox
from mitterlib.ui.helpers.gtk_messagegrid import MessageGrid
from mitterlib.constants import gpl_3, version
from mitterlib.ui.helpers import timesince
@ -48,21 +47,6 @@ t = gettext.translation('ui_pygtk', fallback=True)
_ = t.gettext
N_ = t.ngettext
# ----------------------------------------------------------------------
# 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>')
# ----------------------------------------------------------------------
# Mitter interface object
# ----------------------------------------------------------------------
@ -105,16 +89,8 @@ class Interface(object):
self._statusbar = self._create_statusbar()
self._main_tabs = gtk.Notebook()
self._grids = []
(grid_widget, grid) = self._create_grid('_new_message_count')
self._main_tabs.insert_page(grid_widget, gtk.Label('Messages'))
self._grids.append((grid, '_new_messages_count'))
(replies_widget, replies) = self._create_grid(
'_new_replies_count')
self._main_tabs.insert_page(replies_widget, gtk.Label('Replies'))
self._grids.append((replies, '_new_replies_count'))
self._main_tabs.insert_page(MessageGrid(), gtk.Label('Messages'))
self._main_tabs.insert_page(MessageGrid(), gtk.Label('Replies'))
update_box = gtk.VBox()
update_box.set_property('border_width', 2)
@ -151,47 +127,6 @@ class Interface(object):
return main_window
def _create_grid(self, counter):
"""Add the displaying grid."""
# Store NetworkData objects only
grid_store = gtk.ListStore(object)
grid_store.set_sort_column_id(0, gtk.SORT_ASCENDING)
grid_store.set_sort_func(0, self._order_datetime)
grid = gtk.TreeView(grid_store)
grid.set_property('headers-visible', False)
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', gtk.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)
grid.append_column(user_column)
grid.append_column(message_column)
grid.set_resize_mode(gtk.RESIZE_IMMEDIATE)
grid.connect('cursor-changed', self._message_selected)
grid.connect('button-press-event', self._click_message)
grid.connect('popup-menu', self._message_popup) # Menu button
grid.connect('cursor-changed', self._mark_message_read, counter)
scrolled_window = gtk.ScrolledWindow()
scrolled_window.set_policy(gtk.POLICY_NEVER, gtk.POLICY_ALWAYS)
scrolled_window.add(grid)
return (scrolled_window, grid)
def _create_menu_and_toolbar(self):
"""Create the main menu and the toolbar."""
@ -318,7 +253,8 @@ class Interface(object):
action_group.add_action_with_accel(self._delete_action, 'Delete')
self._reply_action = gtk.Action('Reply', _('_Reply'),
_("Send a response to someone's else message"), gtk.STOCK_REDO)
_("Send a response to someone's else message"),
gtk.STOCK_REDO)
self._reply_action.set_property('sensitive', False)
self._reply_action.connect('activate', self._reply_message)
action_group.add_action_with_accel(self._reply_action, '<Ctrl>r')
@ -399,90 +335,6 @@ class Interface(object):
about_window.run()
about_window.hide()
# ------------------------------------------------------------
# Cell rendering functions
# ------------------------------------------------------------
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._options[self.NAMESPACE]['link_colour'])
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._options[self.NAMESPACE]['unread_char'])
else:
message_values['read_status'] = ''
if data.favourite:
message_values['favourite_star'] = (
self._options[self.NAMESPACE]['unfavourite_char'])
else:
message_values['favourite_star'] = (
self._options[self.NAMESPACE]['favourite_char'])
info = []
if data.reposted_by:
info.append(_(' &#8212; <i>reposted by %s</i>') %
(data.reposted_by.username))
if data.parent_owner:
info.append(_(' &#8212; <i>in reply to %s</i>') %
(data.parent_owner.username))
if data.protected:
message_values['protected_status'] = (
self._options[self.NAMESPACE]['protected_char'])
else:
message_values['protected_status'] = ''
message_values['message_type'] = ''.join(info)
markup = MESSAGE_FORMAT % (message_values)
cell.set_property('markup', markup)
return
def _window_to_tray(self, statusicon, user_data=None):
"""Minimize/display main window (as in minimize to tray.)"""
if self._main_window.get_property('visible'):
self._main_window.hide()
else:
self._main_window.show()
return
# ------------------------------------------------------------
# Helper functions
# ------------------------------------------------------------
@ -515,41 +367,6 @@ class Interface(object):
self._reply_message_id = None
return
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 = []
link = self._connection.link(message)
if link:
network = self._connection.name(message.network)
items.append((_('Open on %s') % (network), 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 _message_count_updated(self):
"""Update the elements in the interface that should change in case of
changes in the message count."""
@ -565,32 +382,36 @@ class Interface(object):
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
self._statusicon.set_from_pixbuf(
self._images['new-messages'])
def _fill_store(self, store, data):
"""Using the results from the request, fill a gtk.Store."""
for message in data:
message.read = False
pic = message.author.avatar
if not pic in self._avatars:
# set the user avatar to the default image, so it won't get
# queued again. Once downloaded, the _post_download_pic will
# update the image and force a redraw.
self._avatars[pic] = self._images['avatar']
self._threads.add_work(self._post_download_pic,
self._exception_download_pic,
self._download_pic,
pic)
store.prepend([message])
store.sort_column_changed()
return
# ------------------------------------------------------------
# Widget callback functions
# ------------------------------------------------------------
def _clear_posts(self, widget):
"""Clear the posts in the currently selected tab."""
page = self._main_tabs.get_current_page()
child = self._main_tabs.get_nth_page(page)
child.clear_posts()
return
def _mark_all_read(self, widget):
"""Mark all messages as read in the currently selected tab."""
page = self._main_tabs.get_current_page()
child = self._main_tabs.get_nth_page(page)
child.mark_all_read()
return
def _window_to_tray(self, statusicon, user_data=None):
"""Minimize/display main window (as in minimize to tray.)"""
if self._main_window.get_property('visible'):
self._main_window.hide()
else:
self._main_window.show()
return
def _update_status(self, widget):
"""Update your status."""
if not self._update_field.get_property('visible'):
@ -649,99 +470,12 @@ class Interface(object):
def _grid_resize(self, widget, requisition, data=None):
"""Called when the window is resized. We use it to set the proper
word-wrapping in the message column."""
for grid in self._grids:
(grid, counter) = grid
model = grid.get_model()
if len(model) == 0:
# don't try to update if there are no data to be updated
return
(win_width, win_height) = self._main_window.get_size()
column = grid.get_column(1)
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 _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 _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)
self._delete_action.set_property('sensitive',
self._connection.can_delete(data))
self._reply_action.set_property('sensitive',
self._connection.can_reply(data))
self._repost_action.set_property('sensitive',
self._connection.can_repost(data))
self._favourite_action.set_property('sensitive',
self._connection.can_favourite(data))
return 0
def _clear_posts(self, widget, user_data=None):
"""Clear the list of posts from the grid."""
page = self._main_tabs.get_current_page()
(grid, counter) = self._grids[page]
grid.get_model().clear()
setattr(self, counter, 0)
self._message_count_updated()
return
def _mark_all_read(self, widget, user_data=None):
"""Mark all messages as read."""
page = self._main_tabs.get_current_page()
(grid, counter) = self._grids[page]
model = 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)
# update the counters
set(attr, self, counter, 0)
self._message_count_updated()
(win_width, win_height) = self._main_window.get_size()
total_tabs = self._main_tabs.get_n_pages()
for page in range(0, total_tabs):
child = self._main_tabs.get_nth_page(page)
child.update_window_size(win_width)
return
def _delete_message(self, widget, user_data=None):
@ -915,57 +649,6 @@ class Interface(object):
self._options.save()
return True
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 _open_url(self, widget, user_data=None):
"""Opens an URL (used mostly from popup menu items.)"""
webbrowser.open_new_tab(user_data)
return
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
# Do it generically, so we can reuse this function to both grids
counter = getattr(self, user_data)
setattr(self, user_data, counter - 1)
message.read = True
self._message_count_updated()
return True
def _change_tab(self, widget, user_data=None):
"""Change the notebook tab to display a differnt tab."""
if not user_data:
@ -1245,8 +928,8 @@ class Interface(object):
return
def __call__(self):
"""Call function; displays the interface. This method should appear on
every interface."""
"""Call function; displays the interface. This method should
appear on every interface."""
if self._options[self.NAMESPACE]['statusicon']:
self._statusicon = gtk.StatusIcon()

Loading…
Cancel
Save