|
|
|
#!/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._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'):
|
|
|
|
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)
|