Browse Source

Moved threading classes to their own file, to reduce clutter in the main

pygtk file; created a update_box object too, to make it easier to set
the avatar and other things.
master
Julio Biason 15 years ago
parent
commit
7ad74aa714
  1. 145
      mitterlib/ui/helpers/gtk_threading.py
  2. 125
      mitterlib/ui/helpers/gtk_updatebox.py
  3. 299
      mitterlib/ui/ui_pygtk.py

145
mitterlib/ui/helpers/gtk_threading.py

@ -0,0 +1,145 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# ----------------------------------------------------------------------
# Threading related objects.
# These classes are based on the code available at http://gist.github.com/51686
# (c) 2008, John Stowers <john.stowers@gmail.com>
# ----------------------------------------------------------------------
import gobject
import threading
import logging
_log = logging.getLogger('helper.gtk_threads')
# ----------------------------------------------------------------------
class _IdleObject(gobject.GObject):
"""
Override gobject.GObject to always emit signals in the main thread
by emmitting on an idle handler
"""
def __init__(self):
gobject.GObject.__init__(self)
def emit(self, *args):
gobject.idle_add(gobject.GObject.emit, self, *args)
# ----------------------------------------------------------------------
class _WorkerThread(threading.Thread, _IdleObject):
"""
A single working thread.
"""
__gsignals__ = {
"completed": (
gobject.SIGNAL_RUN_LAST,
gobject.TYPE_NONE,
(gobject.TYPE_PYOBJECT, )), # list/networkdata
"exception": (
gobject.SIGNAL_RUN_LAST,
gobject.TYPE_NONE,
(gobject.TYPE_PYOBJECT, ))} # The exception
def __init__(self, function, *args, **kwargs):
threading.Thread.__init__(self)
_IdleObject.__init__(self)
self._function = function
self._args = args
self._kwargs = kwargs
def run(self):
# call the function
_log.debug('Thread d %s calling %s', self.getName(),
str(self._function.__name__))
args = self._args
kwargs = self._kwargs
try:
result = self._function(*args, **kwargs)
except Exception, exc: # Catch ALL exceptions
# TODO: Check if this catch all warnins too!
_log.debug('Exception inside thread: %s', str(exc))
self.emit("exception", exc)
return
_log.debug('Thread id %s completed', self.getName())
self.emit("completed", result)
return
# ----------------------------------------------------------------------
class ThreadManager(object):
"""Manages the threads."""
def __init__(self, max_threads=2):
"""Start the thread pool. The number of threads in the pool is defined
by `pool_size`, defaults to 2."""
self._max_threads = max_threads
self._thread_pool = []
self._running = []
self._thread_id = 0
return
def _remove_thread(self, widget, arg=None):
"""Called when the thread completes. We remove it from the thread list
(dictionary, actually) and start the next thread (if there is one)."""
# not actually a widget. It's the object that emitted the signal, in
# this case, the _WorkerThread object.
thread_id = widget.getName()
self._running.remove(thread_id)
_log.debug('Thread id %s completed, %d threads in the queue, ' \
'%d still running', thread_id, len(self._thread_pool),
len(self._running))
if self._thread_pool:
if len(self._running) < self._max_threads:
next = self._thread_pool.pop()
_log.debug('Dequeuing thread %s', next.getName())
self._running.append(next.getName())
next.start()
return
def add_work(self, complete_cb, exception_cb, func, *args, **kwargs):
"""Add a work to the thread list. `complete_cb` is the function to be
called with the result of the work. `exception_cb` is the function to
be called if there are any exceptions raised. Note that, once the
work is complete, one of those will be called, not both. `func` is the
function to be called in the secondary threads. `args` and `kwargs`
are parameters passed to the function."""
thread = _WorkerThread(func, *args, **kwargs)
thread_id = '%s-%s' % (self._thread_id, func.__name__)
thread.connect('completed', complete_cb)
thread.connect('completed', self._remove_thread)
thread.connect('exception', exception_cb)
thread.connect('exception', self._remove_thread)
thread.setName(thread_id)
if len(self._running) < self._max_threads:
# immediatelly start the thread
self._running.append(thread_id)
thread.start()
else:
# add the thread to the queue
running_names = ', '.join(self._running)
_log.debug('Threads %s running, adding %s to the queue',
running_names, thread_id)
self._thread_pool.append(thread)
self._thread_id += 1
return
def clear(self):
"""Clear the thread pool list. This will cause the manager to stop
working after the threads finish."""
self._thread_pool = []
self._running = []
return

125
mitterlib/ui/helpers/gtk_updatebox.py

@ -0,0 +1,125 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Mitter, a multiple-interface client for microblogging services.
# 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 gtk
import logging
_log = logging.getLogger('ui.pygtk.updatebox')
class UpdateBox(gtk.VBox):
"""Custom update box widget."""
def __init__(self, avatar, spell_check=True):
super(UpdateBox, self).__init__(False, 0)
self._reply_to = None
self._text = gtk.TextView()
self._text.set_property('wrap-mode', gtk.WRAP_WORD)
self.avatar = gtk.Image()
self.avatar.set_from_pixbuf(avatar)
status_bar = gtk.HBox(False, 0)
self._update_status = gtk.Label()
close_button = gtk.Button(stock=gtk.STOCK_CLOSE)
close_button.set_relief(gtk.RELIEF_NONE)
close_button.connect('clicked', self.hide)
status_bar = gtk.HBox(False, 0)
status_bar.pack_start(self._update_status, expand=True, fill=True,
padding=0)
status_bar.pack_start(close_button, expand=False, fill=False,
padding=0)
update_bar = gtk.HBox(False, 3)
update_bar.pack_start(self._text, expand=True, fill=True,
padding=0)
update_bar.pack_start(self.avatar, expand=False, fill=False,
padding=0)
self.pack_start(status_bar, expand=False, fill=True, padding=0)
self.pack_start(update_bar, expand=False, fill=True, padding=0)
# Spell checking the update box
if spell_check:
try:
import gtkspell
import locale
language = locale.getlocale()[0]
self.spell_check = gtkspell.Spell(self._update_text, language)
_log.debug('Spell checking turned on with language: %s' \
% (language))
except:
_log.debug('Error initializing spell checking: ' \
'spell checking disabled')
def show(self, reply_to=None):
"""Show the update box and all related widgets."""
super(UpdateBox, self).show()
self._reply_to = reply_to
self.show_all()
_log.debug('show')
return
def hide(self, caller=None):
"""Hide the update box and related widgets."""
super(UpdateBox, self).hide()
self.text = ''
self.hide_all()
return
def _count_chars(self, text_buffer):
"""Count the number of chars in the edit field and update the
label that shows the available space."""
count = len(self.text)
if self._reply_to:
suffix = _('(replying to %s)') % (
self._reply_to)
else:
suffix = ''
# TODO: gettext to properly use "characters"/"character"
text = N_('%d character %s', '%d characters %s', count) % (count,
suffix)
self._update_status.set_text(text)
return True
@property
def text(self):
"""Return the text in the update box."""
text_buffer = self._text.get_buffer()
start = text_buffer.get_start_iter()
end = text_buffer.get_end_iter()
return text_buffer.get_text(start, end, include_hidden_chars=False)
@text.setter
def text(self, text):
"""Set the textarea text."""
self._text.get_buffer().set_text(text)
return
def _set_pixbuf(self, pixbuf):
"""Update the avatar pixbuf."""
self.avatar.set_from_pixbuf(pixbuf)
# pixbuf can only be set, not get.
pixbuf = property(None, _set_pixbuf)

299
mitterlib/ui/ui_pygtk.py

@ -1,8 +1,8 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Mitter, a Maemo client for Twitter. # Mitter, a multiple-interface client for microblogging services.
# Copyright (C) 2007, 2008 Julio Biason, Deepak Sarda # Copyright (C) 2007-2010 Mitter Contributors
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
@ -23,7 +23,6 @@ import gobject
gobject.threads_init() gobject.threads_init()
import logging import logging
import threading
import re import re
import urllib2 import urllib2
import webbrowser import webbrowser
@ -32,10 +31,11 @@ import gettext
from cgi import escape as html_escape from cgi import escape as html_escape
from mitterlib.ui.helpers.image_helpers import find_image 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.constants import gpl_3, version from mitterlib.constants import gpl_3, version
from mitterlib.ui.helpers import timesince from mitterlib.ui.helpers import timesince
# Constants # Constants
_log = logging.getLogger('ui.pygtk') _log = logging.getLogger('ui.pygtk')
@ -63,156 +63,6 @@ MESSAGE_FORMAT = ('%(favourite_star)s'
'<small>%(message_age)s</small>') '<small>%(message_age)s</small>')
# ----------------------------------------------------------------------
# Helper Functions (not related to objects or that don't need direct access to
# the objects contents.)
# ----------------------------------------------------------------------
def _buffer_text(text_buffer):
"""Return the content of a gtk.TextBuffer."""
start = text_buffer.get_start_iter()
end = text_buffer.get_end_iter()
text = text_buffer.get_text(start, end, include_hidden_chars=False)
return text
# ----------------------------------------------------------------------
# Threading related objects.
# These classes are based on the code available at http://gist.github.com/51686
# (c) 2008, John Stowers <john.stowers@gmail.com>
# ----------------------------------------------------------------------
class _IdleObject(gobject.GObject):
"""
Override gobject.GObject to always emit signals in the main thread
by emmitting on an idle handler
"""
def __init__(self):
gobject.GObject.__init__(self)
def emit(self, *args):
gobject.idle_add(gobject.GObject.emit, self, *args)
class _WorkerThread(threading.Thread, _IdleObject):
"""
A single working thread.
"""
__gsignals__ = {
"completed": (
gobject.SIGNAL_RUN_LAST,
gobject.TYPE_NONE,
(gobject.TYPE_PYOBJECT, )), # list/networkdata
"exception": (
gobject.SIGNAL_RUN_LAST,
gobject.TYPE_NONE,
(gobject.TYPE_PYOBJECT, ))} # The exception
def __init__(self, function, *args, **kwargs):
threading.Thread.__init__(self)
_IdleObject.__init__(self)
self._function = function
self._args = args
self._kwargs = kwargs
def run(self):
# call the function
_log.debug('Thread d %s calling %s', self.getName(),
str(self._function.__name__))
args = self._args
kwargs = self._kwargs
try:
result = self._function(*args, **kwargs)
except Exception, exc: # Catch ALL exceptions
# TODO: Check if this catch all warnins too!
_log.debug('Exception inside thread: %s', str(exc))
self.emit("exception", exc)
return
_log.debug('Thread id %s completed', self.getName())
self.emit("completed", result)
return
class _ThreadManager(object):
"""Manages the threads."""
def __init__(self, max_threads=2):
"""Start the thread pool. The number of threads in the pool is defined
by `pool_size`, defaults to 2."""
self._max_threads = max_threads
self._thread_pool = []
self._running = []
self._thread_id = 0
return
def _remove_thread(self, widget, arg=None):
"""Called when the thread completes. We remove it from the thread list
(dictionary, actually) and start the next thread (if there is one)."""
# not actually a widget. It's the object that emitted the signal, in
# this case, the _WorkerThread object.
thread_id = widget.getName()
self._running.remove(thread_id)
_log.debug('Thread id %s completed, %d threads in the queue, ' \
'%d still running', thread_id, len(self._thread_pool),
len(self._running))
if self._thread_pool:
if len(self._running) < self._max_threads:
next = self._thread_pool.pop()
_log.debug('Dequeuing thread %s', next.getName())
self._running.append(next.getName())
next.start()
return
def add_work(self, complete_cb, exception_cb, func, *args, **kwargs):
"""Add a work to the thread list. `complete_cb` is the function to be
called with the result of the work. `exception_cb` is the function to
be called if there are any exceptions raised. Note that, once the
work is complete, one of those will be called, not both. `func` is the
function to be called in the secondary threads. `args` and `kwargs`
are parameters passed to the function."""
thread = _WorkerThread(func, *args, **kwargs)
thread_id = '%s-%s' % (self._thread_id, func.__name__)
thread.connect('completed', complete_cb)
thread.connect('completed', self._remove_thread)
thread.connect('exception', exception_cb)
thread.connect('exception', self._remove_thread)
thread.setName(thread_id)
if len(self._running) < self._max_threads:
# immediatelly start the thread
self._running.append(thread_id)
thread.start()
else:
# add the thread to the queue
running_names = ', '.join(self._running)
_log.debug('Threads %s running, adding %s to the queue',
running_names, thread_id)
self._thread_pool.append(thread)
self._thread_id += 1
return
def clear(self):
"""Clear the thread pool list. This will cause the manager to stop
working after the threads finish."""
self._thread_pool = []
self._running = []
return
# ---------------------------------------------------------------------- # ----------------------------------------------------------------------
# Mitter interface object # Mitter interface object
# ---------------------------------------------------------------------- # ----------------------------------------------------------------------
@ -248,7 +98,10 @@ class Interface(object):
main_window.set_icon(self._images['icon']) main_window.set_icon(self._images['icon'])
(menu, toolbar, accelerators) = self._create_menu_and_toolbar() (menu, toolbar, accelerators) = self._create_menu_and_toolbar()
self._update_field = self._create_update_box() self._update_field = UpdateBox(
self._images['avatar'],
self._options[self.NAMESPACE]['spell_check']
)
self._statusbar = self._create_statusbar() self._statusbar = self._create_statusbar()
self._main_tabs = gtk.Notebook() self._main_tabs = gtk.Notebook()
@ -294,10 +147,7 @@ class Interface(object):
main_window.connect('delete-event', self._quit_app) main_window.connect('delete-event', self._quit_app)
main_window.connect('size-request', self._grid_resize) main_window.connect('size-request', self._grid_resize)
self._update_text.get_buffer().connect('changed', self._count_chars) self._update_field.connect('focus-in-event', self._on_textarea_focus)
self._update_text.connect('focus-out-event',
self._remove_count_status)
self._update_text.connect('focus-in-event', self._on_textarea_focus)
return main_window return main_window
@ -515,56 +365,6 @@ class Interface(object):
return (main_menu, main_toolbar, uimanager.get_accel_group()) return (main_menu, main_toolbar, uimanager.get_accel_group())
def _create_update_box(self):
"""Create the widgets related to the update box"""
self._update_text = gtk.TextView()
self._update_text.set_property('wrap-mode', gtk.WRAP_WORD)
avatar = gtk.Image()
avatar.set_from_pixbuf(self._images['avatar'])
status_bar = gtk.HBox(False, 0)
self._update_status = gtk.Label()
close_button = gtk.Button(stock=gtk.STOCK_CLOSE)
close_button.set_relief(gtk.RELIEF_NONE)
close_button.connect('clicked', self._clear_text)
status_bar = gtk.HBox(False, 0)
status_bar.pack_start(self._update_status, expand=True, fill=True,
padding=0)
status_bar.pack_start(close_button, expand=False, fill=False,
padding=0)
update_bar = gtk.HBox(False, 3)
update_bar.pack_start(self._update_text, expand=True, fill=True,
padding=0)
update_bar.pack_start(avatar, expand=False, fill=False, padding=0)
self._update_box = gtk.VBox(False, 0)
self._update_box.pack_start(status_bar, expand=False, fill=True,
padding=0)
self._update_box.pack_start(update_bar, expand=False, fill=True,
padding=0)
# Spell checking the update box
spell_check_enabled = self._options[self.NAMESPACE]['spell_check']
if spell_check_enabled:
try:
import gtkspell
import locale
self.spell_check_support = True
language = locale.getlocale()[0]
self.spell_check = gtkspell.Spell(self._update_text, language)
_log.debug('Spell checking turned on with language: %s' \
% (language))
except:
self._options[self.NAMESPACE]['spell_check'] = False
self.spell_check_support = False
_log.debug('Error initializing spell checking: ' \
'spell checking disabled')
return self._update_box
def _create_statusbar(self): def _create_statusbar(self):
"""Create the statusbar.""" """Create the statusbar."""
@ -798,39 +598,12 @@ class Interface(object):
def _show_update_box(self): def _show_update_box(self):
"""Enables the update box.""" """Enables the update box."""
self._update_box.show_all() self._update_field.show()
self._update_text.grab_focus()
return
def _hide_update_box(self):
"""Hides de update box."""
self._update_box.hide_all()
return return
# ------------------------------------------------------------ # ------------------------------------------------------------
# Widget callback functions # Widget callback functions
# ------------------------------------------------------------ # ------------------------------------------------------------
def _count_chars(self, text_buffer):
"""Count the number of chars in the edit field and update the
label that shows the available space."""
text = _buffer_text(text_buffer)
count = len(text)
if self._reply_message_id:
suffix = _('(replying to %s)') % (
self._reply_message_id.author.username)
else:
suffix = ''
# TODO: gettext to properly use "characters"/"character"
text = N_('%d character %s', '%d characters %s', count) % (count,
suffix)
self._statusbar.push(self._remove_count_status(), text)
self._update_sensitivity(not (count == 0))
return True
def _update_status(self, widget): def _update_status(self, widget):
"""Update your status.""" """Update your status."""
_log.debug('Updating status.') _log.debug('Updating status.')
@ -840,7 +613,6 @@ class Interface(object):
return return
_log.debug('Status: %s', status) _log.debug('Status: %s', status)
self._remove_count_status()
self._update_statusbar(_('Sending update...')) self._update_statusbar(_('Sending update...'))
self._update_sensitivity(False) self._update_sensitivity(False)
@ -857,7 +629,6 @@ class Interface(object):
self._delete_info = None self._delete_info = None
self._clear_reply() self._clear_reply()
self._remove_count_status()
self._hide_update_box() self._hide_update_box()
# change the focus to the grid. # change the focus to the grid.
@ -1218,19 +989,9 @@ class Interface(object):
self._main_tabs.set_current_page(user_data) self._main_tabs.set_current_page(user_data)
return return
def _remove_count_status(self, widget=None, user_data=None):
"""Find the context with the char count in the statusbar and remove
it (pop). Returns the context of the count, in case some other
function wants to add another message."""
count_context = self._statusbar.get_context_id('counter')
self._statusbar.pop(count_context)
return count_context
def _on_textarea_focus(self, widget, user_data=None): def _on_textarea_focus(self, widget, user_data=None):
"""Called when the text area gets the focus. Just to add the counter """Called when the text area gets the focus. Just to add the counter
again.""" again."""
self._count_chars(self._update_text.get_buffer())
# disable the message actions (they will be activated properly when # disable the message actions (they will be activated properly when
# the user leaves the textarea.) # the user leaves the textarea.)
self._delete_action.set_property('sensitive', False) self._delete_action.set_property('sensitive', False)
@ -1320,11 +1081,11 @@ class Interface(object):
"""Download a picture from the web. Can be used in a thread.""" """Download a picture from the web. Can be used in a thread."""
request = urllib2.Request(url=url) request = urllib2.Request(url=url)
timeout = self._options['NetworkManager']['timeout'] timeout = self._options['NetworkManager']['timeout']
_log.debug('Starting request of %s (timeout %d)' % (url, timeout)) _log.debug('Starting request of %s (timeout %ds)' % (
url, timeout))
response = urllib2.urlopen(request, timeout=timeout) response = urllib2.urlopen(request, timeout=timeout)
data = response.read() data = response.read()
_log.debug('Request completed') _log.debug('Request completed')
return (url, data) return (url, data)
### Results from the picture request ### Results from the picture request
@ -1344,6 +1105,19 @@ class Interface(object):
grid.queue_draw() grid.queue_draw()
return return
### Results from the avatar download request
def _post_avatar_pic(self, widget, data):
"""Called after the data from the picture of the user's avatar is
available."""
(url, data) = data
loader = gtk.gdk.PixbufLoader()
loader.write(data)
loader.close()
self._update_field.pixbuf = loader.get_pixbuf()
return
def _exception_download_pic(self, widget, exception): def _exception_download_pic(self, widget, exception):
"""Called in case we have a problem downloading an user avatar.""" """Called in case we have a problem downloading an user avatar."""
_log.debug('Exception trying to get an avatar.') _log.debug('Exception trying to get an avatar.')
@ -1500,7 +1274,14 @@ class Interface(object):
self._main_window = self._create_main_window() self._main_window = self._create_main_window()
self._main_window.show() self._main_window.show()
self._threads = _ThreadManager() self._threads = ThreadManager()
# get a user avatar. We need that for the updatebox.
user = self._connection.user()
self._threads.add_work(self._post_avatar_pic,
self._exception_download_pic,
self._download_pic,
user.avatar)
# queue the first fetch # queue the first fetch
self._refresh_id = None # The auto-refresh manager. self._refresh_id = None # The auto-refresh manager.
@ -1528,8 +1309,8 @@ class Interface(object):
help=_('Disable the use of the status icon.'), help=_('Disable the use of the status icon.'),
default=True, default=True,
conflict_group='interface') conflict_group='interface')
# Most of the options for non-cmd-options are useless, but I'm keeping # Most of the description for non-cmd-options are useless, but
# them as documentation. # I'm keeping them as documentation.
options.add_option( options.add_option(
group=self.NAMESPACE, group=self.NAMESPACE,
option='width', option='width',
@ -1566,16 +1347,6 @@ class Interface(object):
default=5, default=5,
conflict_group='interface', conflict_group='interface',
is_cmd_option=False) is_cmd_option=False)
# TODO: Should we use this, anyway?
#options.add_option(
# group=self.NAMESPACE,
# option='max_status_display',
# help='Maximum number of elements to keep internally',
# type='int',
# metavar='MESSAGES',
# default=60,
# conflict_group='interface',
# is_cmd_option=False) # TODO: Should it be config only?
options.add_option( options.add_option(
group=self.NAMESPACE, group=self.NAMESPACE,
option='link_colour', option='link_colour',

Loading…
Cancel
Save