You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1249 lines
48 KiB
1249 lines
48 KiB
#!/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 |
|
|
|
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.ui.helpers.gtk_smartbar import SmartStatusbar |
|
from mitterlib.constants import gpl_3, version |
|
|
|
from mitterlib.network import NetworksNoNetworkSetupError |
|
|
|
# ---------------------------------------------------------------------- |
|
# 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._update_field.connect('status-updated', self._update_status) |
|
self._statusbar = SmartStatusbar() |
|
|
|
# the messages grid |
|
messages = MessageGrid(self._avatars) |
|
messages.link_color = self._options[self.NAMESPACE]['link_color'] |
|
messages.user_color = self._options[self.NAMESPACE]['user_color'] |
|
messages.group_color = self._options[self.NAMESPACE]['group_color'] |
|
messages.tag_color = self._options[self.NAMESPACE]['tag_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.user_color = self._options[self.NAMESPACE]['user_color'] |
|
replies.group_color = self._options[self.NAMESPACE]['group_color'] |
|
replies.tag_color = self._options[self.NAMESPACE]['tag_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 = ''' |
|
<ui> |
|
<toolbar name="MainToolbar"> |
|
<toolitem action="Refresh" /> |
|
<toolitem action="Clear" /> |
|
<toolitem action="AllRead" /> |
|
<separator /> |
|
<toolitem action="New" /> |
|
<toolitem action="Delete" /> |
|
<toolitem action="Reply" /> |
|
<toolitem action="Repost" /> |
|
<toolitem action="Favorite" /> |
|
<separator /> |
|
<toolitem action="Settings" /> |
|
<toolitem action="Quit" /> |
|
</toolbar> |
|
<menubar name="MainMenu"> |
|
<menu action="File"> |
|
<menuitem action="Quit" /> |
|
</menu> |
|
<menu action="Edit"> |
|
<menuitem action="Refresh" /> |
|
<menuitem action="Clear" /> |
|
<menuitem action="AllRead" /> |
|
<separator /> |
|
<menuitem action="Update" /> |
|
<menuitem action="Cancel" /> |
|
<separator /> |
|
<menuitem action="Settings" /> |
|
</menu> |
|
<menu action="View"> |
|
<menuitem action="Messages" /> |
|
<menuitem action="Replies" /> |
|
</menu> |
|
<menu action="Message"> |
|
<menuitem action="New" /> |
|
<menuitem action="Delete" /> |
|
<menuitem action="Reply" /> |
|
<menuitem action="Repost" /> |
|
<menuitem action="Favorite" /> |
|
</menu> |
|
<menu action="Help"> |
|
<menuitem action="About" /> |
|
</menu> |
|
</menubar> |
|
</ui> |
|
''' |
|
|
|
# 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, '<Ctrl>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, '<Ctrl>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, '<Ctrl>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, '<Ctrl>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, '<Ctrl>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, |
|
'<Alt>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, '<Alt>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 _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 _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._statusbar.volatile(_('Retrieving messages...'), pair='refresh') |
|
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'): |
|
# update box is not visible; show it. |
|
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._statusbar.volatile(_('Sending update...'), pair='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._statusbar.volatile(_('Deleting message...'), pair='delete') |
|
_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._statusbar.volatile(_('Reposting %s message...') % |
|
(message.author.username), pair='repost') |
|
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._statusbar.volatile(display % (message.author.username), |
|
pair='favorite') |
|
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() |
|
|
|
# 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 = {} |
|
|
|
# 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)) |
|
|
|
# second to last 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'))) |
|
|
|
# last 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() |
|
|
|
self._fields[self.NAMESPACE] = { |
|
'refresh_interval': (self._refresh_interval_field, 'int')} |
|
|
|
tabs.insert_page(interface_box, gtk.Label(_('Interface'))) |
|
|
|
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.""" |
|
need_refresh = False |
|
_log.debug('Saving options') |
|
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) |
|
|
|
# if any of the options change, do another refresh |
|
if self._options[namespace][option] != value: |
|
need_refresh = True |
|
self._options[namespace][option] = value |
|
|
|
self._options.save() |
|
|
|
if need_refresh: |
|
self._refresh() |
|
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._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)) |
|
|
|
# if we get a NetworksNoNetworkSetupError, we need to show the |
|
# settings window |
|
if isinstance(exception, NetworksNoNetworkSetupError): |
|
self._statusbar.pop(self._statusbar_context) |
|
self._show_settings(None) |
|
# don't enable the refresh yet, the settings should take care of |
|
# that once the user set a new config. |
|
return |
|
|
|
message = _('%s\n\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._statusbar.static(_('Auto-update disabled')) |
|
self._statusbar.volatile(_('Error retrieving new messages'), |
|
pair='update') |
|
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) |
|
|
|
self._statusbar.volatile(_('Messages retrieved'), pair='refresh') |
|
|
|
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 (at %s).' % (prefix, suffix, next_update) |
|
self._statusbar.static(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)) |
|
try: |
|
response = urllib2.urlopen(request, timeout=timeout) |
|
except TypeError, e: |
|
# Python 2.5 don't have a timeout parameter |
|
response = urllib2.urlopen(request) |
|
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._statusbar.volatile(_('Your status was updated.'), pair='update') |
|
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.') % (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() |
|
|
|
# re-enable the update, or the user won't be able to repost their |
|
# message. |
|
self._action_group.get_action('Update').set_sensitive(True) |
|
self._statusbar.volatile(_('Update failed.'), pair='update') |
|
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._statusbar.volatile(_('Message deleted.'), pair='delete') |
|
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() |
|
self._statusbar.volatile(_('Error deleting the message'), |
|
pair='delete') |
|
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._statusbar.volatile(_('Message reposted'), pair='repost') |
|
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() |
|
self._statusbar.volatile(_("Error reposting the message"), |
|
pair='repost') |
|
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._statusbar.volatile(display, pair='favorite') |
|
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() |
|
self._statusbar.volatile( |
|
_('Error changing the favorite status of the message'), |
|
pair='favorite') |
|
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. |
|
try: |
|
user = self._connection.user() |
|
self._threads.add_work(self._post_avatar_pic, |
|
self._exception_download_pic, |
|
self._download_pic, |
|
user.avatar) |
|
except NetworksNoNetworkSetupError: |
|
# on the first run, the user won't have an avatar 'cause, well, |
|
# there are no networks set up. |
|
pass # and the user gets the default 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='user_color', |
|
help='Color for usernames', |
|
type='str', |
|
metavar='COLOR', |
|
default='seagreen', |
|
conflict_group='interface', |
|
is_cmd_option=False) |
|
options.add_option( |
|
group=self.NAMESPACE, |
|
option='group_color', |
|
help='Color for group names', |
|
type='str', |
|
metavar='COLOR', |
|
default='red', |
|
conflict_group='interface', |
|
is_cmd_option=False) |
|
options.add_option( |
|
group=self.NAMESPACE, |
|
option='tag_color', |
|
help='Color for tags', |
|
type='str', |
|
metavar='COLOR', |
|
default='chocolate', |
|
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=_('<small>(protected)</small>'), |
|
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)
|
|
|