#!/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 gobject.threads_init() import logging import urllib2 import gettext import datetime import warnings 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.ui.helpers.gdk_avatarcache import AvatarCache from mitterlib.constants import gpl_3, version # ---------------------------------------------------------------------- # Constants # ---------------------------------------------------------------------- _log = logging.getLogger('ui.pygtk') # ---------------------------------------------------------------------- # I18n bits # ---------------------------------------------------------------------- t = gettext.translation('ui_pygtk', fallback=True) _ = t.gettext N_ = t.ngettext # ---------------------------------------------------------------------- # Mitter interface object # ---------------------------------------------------------------------- class Interface(object): """Linux/GTK interface for Mitter.""" NAMESPACE = 'pygtk' PRIORITY = 20 # ------------------------------------------------------------ # Widget creation functions # ------------------------------------------------------------ def _create_main_window(self): """Returns the object with the main window and the attached widgets.""" main_window = gtk.Window(gtk.WINDOW_TOPLEVEL) initial_width = int(self._options[self.NAMESPACE]['width']) initial_height = int(self._options[self.NAMESPACE]['height']) _log.debug('Initial size: %d x %d', initial_width, initial_height) initial_x = int(self._options[self.NAMESPACE]['position_x']) initial_y = int(self._options[self.NAMESPACE]['position_y']) _log.debug('Initial position: %d x %d', initial_x, initial_y) main_window.set_title('Mitter') main_window.set_size_request(450, 300) # very small minimal size main_window.resize(initial_width, initial_height) main_window.move(initial_x, initial_y) if self._images['icon']: main_window.set_icon(self._images['icon']) (menu, toolbar, accelerators) = self._create_menu_and_toolbar() self._update_field = UpdateBox( self._images['avatar'], self._options[self.NAMESPACE]['spell_check']) self._statusbar = self._create_statusbar() # the messages grid messages = MessageGrid(self._avatars) messages.link_color = self._options[self.NAMESPACE]['link_color'] messages.unread_char = self._options[self.NAMESPACE]['unread_char'] messages.favorite_char = ( self._options[self.NAMESPACE]['favorite_char']) messages.unfavorite_char = ( self._options[self.NAMESPACE]['unfavorite_char']) messages.protected_char = ( self._options[self.NAMESPACE]['protected_char']) messages.connect('count-changed', self._update_message_count) messages.connect('message-changed', self._message_changed) # replies grid replies = MessageGrid(self._avatars) replies.link_color = self._options[self.NAMESPACE]['link_color'] replies.unread_char = self._options[self.NAMESPACE]['unread_char'] replies.favorite_char = ( self._options[self.NAMESPACE]['favorite_char']) replies.unfavorite_char = ( self._options[self.NAMESPACE]['unfavorite_char']) replies.protected_char = ( self._options[self.NAMESPACE]['protected_char']) replies.connect('count-changed', self._update_replies_count) replies.connect('message-changed', self._message_changed) self._main_tabs = gtk.Notebook() self._main_tabs.insert_page(messages, gtk.Label('Messages (0)')) self._main_tabs.insert_page(replies, gtk.Label('Replies (0)')) update_box = gtk.VBox() update_box.set_property('border_width', 2) update_box.pack_start(self._main_tabs, expand=True, fill=True, padding=0) update_box.pack_start(self._update_field, expand=False, fill=False, padding=0) box = gtk.VBox(False, 1) box.pack_start(menu, False, True, 0) box.pack_start(toolbar, False, False, 0) box.pack_start(update_box, True, True, 0) box.pack_start(self._statusbar, False, False, 0) main_window.add(box) main_window.add_accel_group(accelerators) # show widgets (except the update box, which will be shown when # requested) menu.show_all() toolbar.show_all() update_box.show() self._main_tabs.show_all() self._statusbar.show_all() box.show() # now that all elements are created, connect the signals main_window.connect('destroy', self._quit_app) main_window.connect('delete-event', self._quit_app) main_window.connect('size-request', self._window_resize) self._update_field.connect('text-focus', self._on_textarea_focus) return main_window def _create_menu_and_toolbar(self): """Create the main menu and the toolbar.""" # UI elements ui_elements = ''' ''' # The group with all actions; we are going to split them using the # definitions inside the XML. self._action_group = gtk.ActionGroup('Mitter') # Actions related to the UI elements above # Top-level menu actions file_action = gtk.Action('File', _('_File'), _('File'), None) self._action_group.add_action(file_action) edit_action = gtk.Action('Edit', _('_Edit'), _('Edit'), None) self._action_group.add_action(edit_action) message_action = gtk.Action('Message', _('_Message'), _('Message related options'), None) self._action_group.add_action(message_action) view_action = gtk.Action('View', _('_View'), _('View'), None) self._action_group.add_action(view_action) help_action = gtk.Action('Help', _('_Help'), _('Help'), None) self._action_group.add_action(help_action) # File actions quit_action = gtk.Action('Quit', _('_Quit'), _('Exit Mitter'), gtk.STOCK_QUIT) quit_action.connect('activate', self._quit_app) self._action_group.add_action_with_accel(quit_action, None) # Edit actions refresh_action = gtk.Action('Refresh', _('_Refresh'), _('Update the listing'), gtk.STOCK_REFRESH) refresh_action.connect('activate', self._refresh) self._action_group.add_action_with_accel(refresh_action, None) clear_action = gtk.Action('Clear', _('_Clear'), _('Clear the message list'), gtk.STOCK_CLEAR) clear_action.connect('activate', self._clear_posts) self._action_group.add_action_with_accel(clear_action, 'l') all_read_action = gtk.Action('AllRead', _('_Mark All Read'), _('Mark all messages as read'), gtk.STOCK_APPLY) all_read_action.connect('activate', self._mark_all_read) self._action_group.add_action(all_read_action) update_action = gtk.Action('Update', _('_Update'), _('Update your status'), gtk.STOCK_ADD) update_action.connect('activate', self._update_status) self._action_group.add_action_with_accel(update_action, 'Return') cancel_action = gtk.Action('Cancel', _('_Cancel'), _('Cancel the update'), gtk.STOCK_CANCEL) cancel_action.connect('activate', self._clear_text) self._action_group.add_action_with_accel(cancel_action, 'Escape') settings_action = gtk.Action('Settings', _('_Settings'), _('Settings'), gtk.STOCK_PREFERENCES) settings_action.connect('activate', self._show_settings) self._action_group.add_action(settings_action) # Message actions new_action = gtk.Action('New', _('_New'), _('Post a new message'), gtk.STOCK_ADD) new_action.connect('activate', self._new_message) self._action_group.add_action_with_accel(new_action, 'n') delete_action = gtk.Action('Delete', _('_Delete'), _('Delete a post'), gtk.STOCK_DELETE) delete_action.set_property('sensitive', False) delete_action.connect('activate', self._delete_message) self._action_group.add_action_with_accel(delete_action, 'Delete') reply_action = gtk.Action('Reply', _('_Reply'), _("Send a response to someone's else message"), gtk.STOCK_REDO) reply_action.set_property('sensitive', False) reply_action.connect('activate', self._reply_message) self._action_group.add_action_with_accel(reply_action, 'r') repost_action = gtk.Action('Repost', _('Re_post'), _("Put someone's else message on your timeline"), gtk.STOCK_CONVERT) repost_action.set_property('sensitive', False) repost_action.connect('activate', self._repost_message) self._action_group.add_action_with_accel(repost_action, 'p') favorite_action = gtk.Action('Favorite', _('_Favorite'), _('Toggle the favorite status of a message'), gtk.STOCK_ABOUT) favorite_action.set_property('sensitive', False) favorite_action.connect('activate', self._favorite_message) self._action_group.add_action_with_accel(favorite_action, 'f') # view actions view_messages_action = gtk.Action('Messages', _('_Messages'), _('Display messages'), None) view_messages_action.connect('activate', self._change_tab, 0) self._action_group.add_action_with_accel(view_messages_action, '1') view_replies_action = gtk.Action('Replies', _('_Replies'), _('Display replies'), None) view_replies_action.connect('activate', self._change_tab, 1) self._action_group.add_action_with_accel(view_replies_action, '2') # Help actions about_action = gtk.Action('About', _('_About'), _('About Mitter'), gtk.STOCK_ABOUT) about_action.connect('activate', self._show_about) self._action_group.add_action(about_action) # definition of the UI uimanager = gtk.UIManager() uimanager.insert_action_group(self._action_group, 0) uimanager.add_ui_from_string(ui_elements) main_menu = uimanager.get_widget('/MainMenu') main_toolbar = uimanager.get_widget('/MainToolbar') return (main_menu, main_toolbar, uimanager.get_accel_group()) def _create_statusbar(self): """Create the statusbar.""" statusbar = gtk.Statusbar() self._statusbar_context = statusbar.get_context_id('Mitter') return statusbar def _show_about(self, widget): """Show the about dialog.""" about_window = gtk.AboutDialog() about_window.set_name('Mitter') about_window.set_version(version) about_window.set_copyright('2007-2010 Mitter Contributors') about_window.set_license(gpl_3) about_window.set_website('http://code.google.com/p/mitter') about_window.set_website_label(_('Mitter project page')) about_window.set_authors([ 'Main developers:', 'Julio Biason', 'Deepak Sarda', 'Gerald Kaszuba', ' ', 'And patches from:', 'Santiago Gala', 'Sugree Phatanapherom', 'Kristian Rietveld', '"Wiennat"', 'Philip Reynolds', 'Greg McIntyre', '"Alexander"', 'Renato Covarrubias']) if self._images['logo']: about_window.set_logo(self._images['logo']) about_window.run() about_window.hide() # ------------------------------------------------------------ # Helper functions # ------------------------------------------------------------ def _update_statusbar(self, message): """Update the statusbar with the message.""" self._statusbar.pop(self._statusbar_context) self._statusbar.push(self._statusbar_context, message) return def _refresh(self, widget=None): """Request a refresh. *widget* is the widget that called this function (we basically ignore it.)""" # disable the refresh from the menu, so we don't get duplicate # results. refresh = self._action_group.get_action('Refresh'); refresh.set_property('sensitive', False) if self._refresh_id: # "De-queue" the next refresh _log.debug('Dequeuing next refresh') gobject.source_remove(self._refresh_id) self._refresh_id = None # do the refresh self._update_statusbar(_('Retrieving messages...')) self._threads.add_work(self._post_get_messages, self._exception_get_messages, self._connection.messages) return def _clear_reply(self): """Clear the info about a reply.""" self._reply_message_id = None return def _update_title(self, messages, replies): """Update the window title with the information about the number of unread messages and unread replies.""" title_info = { 'message_count': str(messages), 'message': N_('message', 'messages', messages), 'replies_count': str(replies), 'replies': N_('reply', 'replies', replies)} mask = self._options[self.NAMESPACE]['window_title'] for variable in title_info: mask = mask.replace('{' + variable + '}', title_info[variable]) self._main_window.set_title(mask) return def _hide_update_field(self): """Hides the update field and recheck the selected message for enabling (or not) the actions.""" self._update_field.hide() page = self._main_tabs.get_current_page() child = self._main_tabs.get_nth_page(page) if child.selected: self._message_changed(child, child.selected) return # ------------------------------------------------------------ # Widget callback functions # ------------------------------------------------------------ def _message_changed(self, widget, data): """Callback from the MesageGrids when the selected message changes.""" self._action_group.get_action('Delete').set_property('sensitive', data.deletable) self._action_group.get_action('Reply').set_property('sensitive', data.replyable) self._action_group.get_action('Repost').set_property('sensitive', data.repostable) self._action_group.get_action('Favorite').set_property('sensitive', data.favoritable) return def _update_message_count(self, widget, data): """Callback from the MessageGrid for messages when the number of messages changes.""" child = self._main_tabs.get_nth_page(0) message = _('Messages (%d)') % (data) # do this specially for tabs since it seems really slow to update the # text (at least, on my computer); so we update only the required tab, # not both (even if we get both values here.) self._main_tabs.set_tab_label_text(child, message) replies = self._main_tabs.get_nth_page(1) self._update_title(data, replies.count) if self._statusicon: if (data + replies.count) > 0: self._statusicon.set_from_pixbuf(self._images['new-messages']) return self._statusicon.set_from_pixbuf(self._images['icon']) return def _update_replies_count(self, widget, data): """Callback from the MessageGrid for replies when the number of messages changes.""" child = self._main_tabs.get_nth_page(1) message = _('Replies (%d)') % (data) self._main_tabs.set_tab_label_text(child, message) messages = self._main_tabs.get_nth_page(0) self._update_title(messages.count, data) if self._statusicon: if (data + messages.count) > 0: self._statusicon.set_from_pixbuf(self._images['new-messages']) return self._statusicon.set_from_pixbuf(self._images['icon']) return 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'): self._update_field.show() return status = self._update_field.text.strip() if not status: _log.debug('Empty message, aborting.') self._hide_update_field() return page = self._main_tabs.get_current_page() child = self._main_tabs.get_nth_page(page) if child.selected: if child.selected.reply_prefix.strip() == status: _log.debug('Empty reply, aborting.') self._hide_update_field() return _log.debug('Status: %s', status) self._update_statusbar(_('Sending update...')) self._action_group.get_action('Update').set_sensitive(False) self._threads.add_work(self._post_update_status, self._exception_update_status, self._connection.update, status=status, reply_to=self._reply_message_id) return def _clear_text(self, widget): """Clear the text field.""" self._clear_reply() self._hide_update_field() # change the focus to the grid. page = self._main_tabs.get_current_page() child = self._main_tabs.get_nth_page(page) child.get_child().grab_focus() # notebook have ScrolledWindows, # TreeViews inside that. return def _quit_app(self, widget=None, user_data=None): """Callback when the window is destroyed or the user selects "Quit".""" (x, y) = self._main_window.get_position() _log.debug('Current position: %d x %d', x, y) self._options[self.NAMESPACE]['position_x'] = x self._options[self.NAMESPACE]['position_y'] = y (width, height) = self._main_window.get_size() _log.debug('Current window size: %d x %d', width, height) self._options[self.NAMESPACE]['width'] = width self._options[self.NAMESPACE]['height'] = height # TODO: Kill any threads running. self._threads.clear() gtk.main_quit() return def _window_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.""" (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): """Delete a message.""" page = self._main_tabs.get_current_page() message = self._main_tabs.get_nth_page(page).selected if not message: return confirm = gtk.MessageDialog(parent=self._main_window, type=gtk.MESSAGE_QUESTION, message_format=_('Delete this message?'), buttons=gtk.BUTTONS_YES_NO) option = confirm.run() confirm.hide() _log.debug("Option selected: %s" % (option)) if option == -9: _log.debug("Delete cancelled") return False self._update_statusbar(_('Deleting message...')) _log.debug('Deleting messing %d', message.id) self._threads.add_work(self._post_delete_message, self._exception_delete_message, self._connection.delete_message, message) return def _reply_message(self, widget, user_data=None): """Reply to someone else's message.""" page = self._main_tabs.get_current_page() grid = self._main_tabs.get_nth_page(page) message = grid.selected if not message: return self._update_field.text = message.reply_prefix self._reply_message_id = message self._update_field.show(message.author) return def _repost_message(self, widget, user_data=None): """Repost someone else's message on your timeline.""" page = self._main_tabs.get_current_page() grid = self._main_tabs.get_nth_page(page) message = grid.selected if not message: return self._update_statusbar(_('Reposting %s message...') % (message.author.username)) self._threads.add_work(self._post_repost_message, self._exception_repost_message, self._connection.repost, message) return def _favorite_message(self, widget, user_data=None): """Toggle the favorite status of a message.""" page = self._main_tabs.get_current_page() grid = self._main_tabs.get_nth_page(page) message = grid.selected if not message: return # TODO: I removed the iterator for the grid; we need to find a wait to # the tell the grid a message was favorited if message.favorite: display = _('Removing message from %s from favorites...') else: display = _('Marking message from %s as favorite...') self._update_statusbar(display % (message.author.username)) self._threads.add_work(self._post_favorite_message, self._exception_favorite_message, self._connection.favorite, message) return def _show_settings(self, widget, user_data=None): """Display the settings window.""" settings_window = gtk.Dialog(title=_('Settings'), parent=self._main_window, flags=gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, buttons=(gtk.STOCK_OK, 0)) # the tabs tabs = gtk.Notebook() # first page is the interface settings self._refresh_interval_field = gtk.SpinButton() self._refresh_interval_field.set_range(1, 99) self._refresh_interval_field.set_numeric(True) self._refresh_interval_field.set_value( self._options[self.NAMESPACE]['refresh_interval']) self._refresh_interval_field.set_increments(1, 5) interface_box = gtk.Table(rows=2, columns=1, homogeneous=False) interface_box.attach(gtk.Label(_('Refresh interval (minutes):')), 0, 1, 0, 1) interface_box.attach(self._refresh_interval_field, 0, 1, 1, 2) interface_box.show_all() tabs.insert_page(interface_box, gtk.Label(_('Interface'))) # second page is the network manager settings self._proxy_field = gtk.Entry() if self._options['NetworkManager']['proxy']: self._proxy_field.set_text( self._options['NetworkManager']['proxy']) manager_box = gtk.Table(rows=1, columns=2, homogeneous=False) manager_box.attach(gtk.Label(_('Proxy:')), 0, 1, 0, 1) manager_box.attach(self._proxy_field, 0, 1, 1, 2) manager_box.show_all() tabs.insert_page(manager_box, gtk.Label(_('Networks'))) # We store the fields in a dictionary, inside dictionaries for each # NAMESPACE. To set the values, we just run the dictionaries setting # self._options. self._fields = {self.NAMESPACE: {'refresh_interval': (self._refresh_interval_field, 'int')}} # next pages are each network settings net_options = self._connection.settings() for network in net_options: network_name = network['name'] rows = len(network['options']) net_box = gtk.Table(rows=rows, columns=2, homogeneous=False) self._fields[network_name] = {} row = 0 for option in network['options']: option_name = option['name'] option_value = '' try: option_value = self._options[network_name][option_name] except KeyError: pass new_field = gtk.Entry() if option_value: new_field.set_text(option_value) # Ony "str" and "passwd" are valid type and both use Entry() if option['type'] == 'passwd': new_field.set_visibility(False) net_box.attach(gtk.Label(option_name), row, row+1, 0, 1) net_box.attach(new_field, row, row+1, 1, 2) row += 1 self._fields[network_name][option_name] = (new_field, option['type']) net_box.show_all() tabs.insert_page(net_box, gtk.Label(network_name)) tabs.show_all() settings_window.vbox.pack_start(tabs, True, True, 0) settings_window.connect('response', self._update_settings) settings_window.run() settings_window.hide() def _update_settings(self, widget, response_id=0, user_data=None): """Update the interface settings.""" for namespace in self._fields: for option in self._fields[namespace]: (field, field_type) = self._fields[namespace][option] value = field.get_text() if field_type == 'int': value = int(value) self._options[namespace][option] = value self._options.save() return True def _change_tab(self, widget, user_data=None): """Change the notebook tab to display a differnt tab.""" if not user_data: user_data = 0 self._main_tabs.set_current_page(user_data) return def _on_textarea_focus(self, widget, user_data=None): """Called when the text area gets the focus. Just to add the counter again.""" # disable the message actions (they will be activated properly when # the user leaves the textarea.) _log.debug('Disabling message actions due focus on textarea') self._action_group.get_action('Delete').set_property('sensitive', False) self._action_group.get_action('Reply').set_property('sensitive', False) self._action_group.get_action('Repost').set_property('sensitive', False) self._action_group.get_action('Favorite').set_property('sensitive', False) return def _new_message(self, widget, user_data=None): """Opens the text area for a new message.""" self._update_field.show() return # ------------------------------------------------------------ # Network related functions # ------------------------------------------------------------ ### Results from the "messages" request def _post_get_messages(self, widget, results): """Function called after the data from the messages list is retrieved.""" _log.debug('%d new tweets', len(results)) grid = self._main_tabs.get_nth_page(0) grid.update(results) # now get replies self._update_statusbar(_('Retrieving replies...')) self._threads.add_work(self._post_get_replies, self._exception_get_messages, self._connection.replies) return def _exception_get_messages(self, widget, exception): """Function called if the retrival of current messages returns an exception.""" _log.debug(str(exception)) message = _('%s\nAuto-refresh is now disabled. Use the "refresh" ' \ 'option to re-enable it.') % (str(exception)) error_win = gtk.MessageDialog(parent=self._main_window, type=gtk.MESSAGE_ERROR, message_format=_message, buttons=gtk.BUTTONS_OK) error_win.run() error_win.hide() self._update_statusbar(_('Auto-update disabled')) if self._statusicon: self._statusicon.set_from_pixbuf(self._images['icon-error']) # re-enable the refresh from the menu refresh = self._action_group.get_action('Refresh') refresh.set_property('sensitive', True) return ### replies callback def _post_get_replies(self, widget, results): """Called after we retrieve the replies.""" grid = self._main_tabs.get_nth_page(1) grid.update(results) interval = self._options[self.NAMESPACE]['refresh_interval'] # once our update went fine, we can queue the next one. This avoids # any problems if case there is an exception. now = datetime.datetime.now() delta = datetime.timedelta(seconds=interval*60) next_call = now + delta _log.debug('Queueing next refresh in %d minutes', interval) prefix = _('New messages retrieved.') suffix = N_('Next update in %d minute', 'Next update in %d minutes', interval) next_update = next_call.strftime('%H:%M:%S') # TODO: Check if the time format string should go in the config file. message = '%s %s %s.' % (prefix, suffix, next_update) self._update_statusbar(message % (interval)) self._refresh_id = gobject.timeout_add( interval * 60 * 1000, self._refresh, None) # re-enable the refresh action refresh = self._action_group.get_action('Refresh') refresh.set_property('sensitive', True) return ### image download function def _download_pic(self, url): """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 %ds)' % ( url, timeout)) response = urllib2.urlopen(request, timeout=timeout) data = response.read() _log.debug('Request completed') return (url, data) ### 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.') _log.debug(str(exception)) return ### Results for the update status call def _post_update_status(self, widget, data): """Called when the status is updated correctly.""" self._update_statusbar(_('Your status was updated.')) self._action_group.get_action('Update').set_sensitive(True) self._clear_text(None) # re-enable (or not) the actions based on the currently selected # message page = self._main_tabs.get_current_page() grid = self._main_tabs.get_nth_page(page) message = grid.selected if message: self._message_changed(grid, message) return def _exception_update_status(self, widget, exception): """Called when there is an exception updating the status.""" _log.debug('Update error') _log.debug(str(exception)) message = _('%s\nPlease, try again.') % (message) error_win = gtk.MessageDialog(parent=self._main_window, type=gtk.MESSAGE_ERROR, message_format=message, buttons=gtk.BUTTONS_OK) error_win.run() error_win.hide() # re-enable the update, or the user won't be able to repost their # message. self._action_group.get_action('Update').set_sensitive(True) return ### Results for the delete message call def _post_delete_message(self, widget, message): """Called when the message is deleted successfully.""" _log.debug('Message deleted.') # any grid can take care of deleting the message MessageGrid.delete(message) self._update_statusbar(_('Message deleted.')) return def _exception_delete_message(self, widget, exception): """Called when the message cannot be deleted.""" _log.debug('Delete error') _log.debug(str(exception)) error_win = gtk.MessageDialog(parent=self._main_window, type=gtk.MESSAGE_ERROR, message_format=str(exception), buttons=gtk.BUTTONS_OK) error_win.run() error_win.hide() return ### Results for the repost message call def _post_repost_message(self, widget, data): """Called when the message is reposted successfully.""" _log.debug('Repost successful') self._update_statusbar(_('Message reposted')) return def _exception_repost_message(self, widget, exception): """Called when the message cannot be reposted.""" _log.debug('Repost error.') _log.debug(str(exception)) error_win = gtk.MessageDialog(parent=self._main_window, type=gtk.MESSAGE_ERROR, message_format=str(exception), buttons=gtk.BUTTONS_OK) error_win.run() error_win.hide() return ### Results from the favorite call def _post_favorite_message(self, widget, status): """Called when the message was favorited successfully.""" _log.debug('Favorite status changed.') if status: display = _('Message favorited.') else: display = _('Message unfavorited.') self._update_statusbar(display) return def _exception_favorite_message(self, widget, exception): """Called when the message couldn't be favorited.""" _log.debug('Favorite error.') _log.debug(str(exception)) error_win = gtk.MessageDialog(parent=self._main_window, type=gtk.MESSAGE_ERROR, message_format=str(exception), buttons=gtk.BUTTONS_OK) error_win.run() error_win.hide() return # ------------------------------------------------------------ # Required functions for all interfaces # ------------------------------------------------------------ def __init__(self, connection, options): """Start the interface. `connection` is the :class:`Networks` object with all the available networks. `options` is the :class:`ConfigOpt` object with the configuration to run Mitter.""" # Load images unknown_pixbuf = find_image('unknown.png') if unknown_pixbuf: default_pixmap = gtk.gdk.pixbuf_new_from_file( unknown_pixbuf) else: default_pixmap = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, has_alpha=False, bits_per_sample=8, width=48, height=48) self._images = {} self._images['avatar'] = default_pixmap self._images['logo'] = gtk.gdk.pixbuf_new_from_file( find_image('mitter-big.png')) self._connection = connection self._options = options # icons (app and statusicon) self._images['icon'] = gtk.gdk.pixbuf_new_from_file( find_image('mitter.png')) self._images['new-messages'] = gtk.gdk.pixbuf_new_from_file( find_image('mitter-new.png')) self._images['icon-error'] = gtk.gdk.pixbuf_new_from_file( find_image('mitter-error.png')) # This is the ugly bit for speeding up things and making # interthread communication. self._reply_message_id = None return def __call__(self): """Call function; displays the interface. This method should appear on every interface.""" # turn warnings into exception (so we can catch them) warnings.simplefilter('error') if self._options[self.NAMESPACE]['statusicon']: self._statusicon = gtk.StatusIcon() self._statusicon.set_from_pixbuf(self._images['icon']) self._statusicon.connect('activate', self._window_to_tray) else: self._statusicon = None self._threads = ThreadManager() self._avatars = AvatarCache( self._threads, self._images['avatar'], self._options['NetworkManager']['timeout']) self._main_window = self._create_main_window() self._main_window.show() # 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. self._refresh() gtk.main() return @classmethod def options(self, options): """Add the options for this interface.""" # Remember to update CHEAT-CODES in case you add or remove any # options. options.add_group(self.NAMESPACE, 'GTK+ Interface') options.add_option('--refresh-interval', group=self.NAMESPACE, option='refresh_interval', help=_('Refresh interval.'), type='int', metavar='MINUTES', default=5, conflict_group='interface') options.add_option('--disable-statusicon', group=self.NAMESPACE, action='store_false', option='statusicon', help=_('Disable the use of the status icon.'), default=True, conflict_group='interface') # 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', help='Window width', type='int', metavar='PIXELS', default=450, conflict_group='interface', is_cmd_option=False) options.add_option( group=self.NAMESPACE, option='height', help='Window height', type='int', metavar='PIXELS', default=300, conflict_group='interface', is_cmd_option=False) options.add_option( group=self.NAMESPACE, option='position_x', help='Window position on the X axis', type='int', metavar='PIXELS', default=5, conflict_group='interface', is_cmd_option=False) options.add_option( group=self.NAMESPACE, option='position_y', help='Window position on the Y axis', type='int', metavar='PIXELS', default=5, conflict_group='interface', is_cmd_option=False) options.add_option( group=self.NAMESPACE, option='link_color', help='Color of links in the interface', type='str', metavar='COLOR', default='blue', conflict_group='interface', is_cmd_option=False) options.add_option( group=self.NAMESPACE, option='spell_check', help='Spell checking update text', type='boolean', metavar='SPELL', default=False, conflict_group='interface', is_cmd_option=False) options.add_option( group=self.NAMESPACE, option='unread_char', help='String to be used to indicate a message is unread.', metavar='CHAR', default='●', is_cmd_option=False) options.add_option( group=self.NAMESPACE, option='unfavorite_char', help='String to be used to indicate a message is not ' \ 'marked as favorite.', metavar='CHAR', default='★', is_cmd_option=False) options.add_option( group=self.NAMESPACE, option='favorite_char', help='String to be used to indicate a message is marked ' \ 'as favorite.', metavar='CHAR', default='☆', is_cmd_option=False) options.add_option( group=self.NAMESPACE, option='protected_char', help='String to be used to indicate that a message is ' \ 'protected/private.', metavar='CHAR', default=_('(protected)'), is_cmd_option=False) options.add_option( group=self.NAMESPACE, option='window_title', help='Format string to be used in the title. Variables ' \ 'can be accessed with {variable_name}. Mitter will ' \ 'pass the following variables:\n' \ '"message_count": the number of unread messages,\n' \ '"message": the translatable word for ' \ '"message(s)",\n' \ '"replies_count": the number of unread replies,\n' \ '"replies": the translatable word for "replies"', metavar='STRING', default='Mitter ({message_count} {message}, ' \ '{replies_count} {replies})', is_cmd_option=False)