From 7ad74aa7146a47ff277e6c5f82b4b7e77a8d3de0 Mon Sep 17 00:00:00 2001 From: Julio Biason Date: Sun, 28 Mar 2010 00:53:25 -0300 Subject: [PATCH] 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. --- mitterlib/ui/helpers/gtk_threading.py | 145 +++++++++++++ mitterlib/ui/helpers/gtk_updatebox.py | 125 +++++++++++ mitterlib/ui/ui_pygtk.py | 299 +++----------------------- 3 files changed, 305 insertions(+), 264 deletions(-) create mode 100644 mitterlib/ui/helpers/gtk_threading.py create mode 100644 mitterlib/ui/helpers/gtk_updatebox.py diff --git a/mitterlib/ui/helpers/gtk_threading.py b/mitterlib/ui/helpers/gtk_threading.py new file mode 100644 index 0000000..0d0bfc9 --- /dev/null +++ b/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 +# ---------------------------------------------------------------------- + +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 + diff --git a/mitterlib/ui/helpers/gtk_updatebox.py b/mitterlib/ui/helpers/gtk_updatebox.py new file mode 100644 index 0000000..3a8b572 --- /dev/null +++ b/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 . + +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) \ No newline at end of file diff --git a/mitterlib/ui/ui_pygtk.py b/mitterlib/ui/ui_pygtk.py index b53675e..f298128 100644 --- a/mitterlib/ui/ui_pygtk.py +++ b/mitterlib/ui/ui_pygtk.py @@ -1,8 +1,8 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -# Mitter, a Maemo client for Twitter. -# Copyright (C) 2007, 2008 Julio Biason, Deepak Sarda +# 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 @@ -23,7 +23,6 @@ import gobject gobject.threads_init() import logging -import threading import re import urllib2 import webbrowser @@ -32,10 +31,11 @@ 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.constants import gpl_3, version from mitterlib.ui.helpers import timesince - # Constants _log = logging.getLogger('ui.pygtk') @@ -63,156 +63,6 @@ MESSAGE_FORMAT = ('%(favourite_star)s' '%(message_age)s') -# ---------------------------------------------------------------------- -# 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 -# ---------------------------------------------------------------------- - -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 # ---------------------------------------------------------------------- @@ -248,7 +98,10 @@ class Interface(object): main_window.set_icon(self._images['icon']) (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._main_tabs = gtk.Notebook() @@ -294,10 +147,7 @@ class Interface(object): main_window.connect('delete-event', self._quit_app) main_window.connect('size-request', self._grid_resize) - self._update_text.get_buffer().connect('changed', self._count_chars) - self._update_text.connect('focus-out-event', - self._remove_count_status) - self._update_text.connect('focus-in-event', self._on_textarea_focus) + self._update_field.connect('focus-in-event', self._on_textarea_focus) return main_window @@ -515,56 +365,6 @@ class Interface(object): 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): """Create the statusbar.""" @@ -798,39 +598,12 @@ class Interface(object): def _show_update_box(self): """Enables the update box.""" - self._update_box.show_all() - self._update_text.grab_focus() - return - - def _hide_update_box(self): - """Hides de update box.""" - self._update_box.hide_all() + self._update_field.show() return # ------------------------------------------------------------ # 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): """Update your status.""" _log.debug('Updating status.') @@ -840,7 +613,6 @@ class Interface(object): return _log.debug('Status: %s', status) - self._remove_count_status() self._update_statusbar(_('Sending update...')) self._update_sensitivity(False) @@ -857,7 +629,6 @@ class Interface(object): self._delete_info = None self._clear_reply() - self._remove_count_status() self._hide_update_box() # change the focus to the grid. @@ -1218,19 +989,9 @@ class Interface(object): self._main_tabs.set_current_page(user_data) 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): """Called when the text area gets the focus. Just to add the counter again.""" - self._count_chars(self._update_text.get_buffer()) - # disable the message actions (they will be activated properly when # the user leaves the textarea.) 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.""" request = urllib2.Request(url=url) 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) data = response.read() _log.debug('Request completed') - return (url, data) ### Results from the picture request @@ -1344,6 +1105,19 @@ class Interface(object): grid.queue_draw() 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): """Called in case we have a problem downloading an user 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.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 self._refresh_id = None # The auto-refresh manager. @@ -1528,8 +1309,8 @@ class Interface(object): help=_('Disable the use of the status icon.'), default=True, conflict_group='interface') - # Most of the options for non-cmd-options are useless, but I'm keeping - # them as documentation. + # Most of the description for non-cmd-options are useless, but + # I'm keeping them as documentation. options.add_option( group=self.NAMESPACE, option='width', @@ -1566,16 +1347,6 @@ class Interface(object): default=5, conflict_group='interface', 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( group=self.NAMESPACE, option='link_colour',