A micro-blogging tool with multiple interfaces.
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.

1173 lines
45 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
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)
15 years ago
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 = '''
<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)
15 years ago
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'),
15 years ago
_('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')
15 years ago
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 _create_statusbar(self):
"""Create the statusbar."""
statusbar = gtk.Statusbar()
self._statusbar_context = statusbar.get_context_id('Mitter')
return statusbar
16 years ago
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')
16 years ago
about_window.set_license(gpl_3)
about_window.set_website('http://code.google.com/p/mitter')
15 years ago
about_window.set_website_label(_('Mitter project page'))
16 years ago
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'])
15 years ago
if self._images['logo']:
about_window.set_logo(self._images['logo'])
16 years ago
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
15 years ago
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
15 years ago
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,
15 years ago
status=status,
reply_to=self._reply_message_id)
return
def _clear_text(self, widget):
"""Clear the text field."""
15 years ago
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?'),
15 years ago
buttons=gtk.BUTTONS_YES_NO)
option = confirm.run()
confirm.hide()
_log.debug("Option selected: %s" % (option))
if option == -9:
15 years ago
_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
15 years ago
def _reply_message(self, widget, user_data=None):
15 years ago
"""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
15 years ago
self._reply_message_id = message
self._update_field.show(message.author)
15 years ago
return
15 years ago
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))
15 years ago
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:
15 years ago
display = _('Removing message from %s from favorites...')
else:
15 years ago
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
15 years ago
def _show_settings(self, widget, user_data=None):
"""Display the settings window."""
settings_window = gtk.Dialog(title=_('Settings'),
15 years ago
parent=self._main_window,
flags=gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
buttons=(gtk.STOCK_OK, 0))
15 years ago
15 years ago
# 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):')),
15 years ago
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')))
15 years ago
# 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')))
15 years ago
# 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')}}
15 years ago
# 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)
15 years ago
# 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'])
15 years ago
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]
15 years ago
value = field.get_text()
if field_type == 'int':
value = int(value)
15 years ago
self._options[namespace][option] = value
self._options.save()
15 years ago
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)
15 years ago
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)
15 years ago
# now get replies
self._update_statusbar(_('Retrieving replies...'))
15 years ago
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))
error_win = gtk.MessageDialog(parent=self._main_window,
type=gtk.MESSAGE_ERROR,
message_format=_('Error retrieving current messages. ' \
'Auto-refresh disabled. Use the "Refresh" option ' \
'to re-enable it.'),
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
15 years ago
### 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)
15 years ago
interval = self._options[self.NAMESPACE]['refresh_interval']
15 years ago
# 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
15 years ago
_log.debug('Queueing next refresh in %d minutes', interval)
15 years ago
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)
15 years ago
self._update_statusbar(message % (interval))
15 years ago
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)
15 years ago
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))
error_win = gtk.MessageDialog(parent=self._main_window,
type=gtk.MESSAGE_ERROR,
message_format=_('Error updating your status. Please ' \
'try again.'),
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))
return
15 years ago
### 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'))
15 years ago
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=_('Error reposting message. Please ' \
'try again.'),
buttons=gtk.BUTTONS_OK)
error_win.run()
error_win.hide()
15 years ago
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:
15 years ago
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,
15 years ago
message_format=_('Error changing favorite status of the ' \
'message. Please, try again.'),
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
15 years ago
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.
15 years ago
self._reply_message_id = None
return
def __call__(self):
"""Call function; displays the interface. This method should
appear on every interface."""
if self._options[self.NAMESPACE]['statusicon']:
self._statusicon = gtk.StatusIcon()
15 years ago
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='&#9679;',
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='&#9733;',
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='&#9734;',
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)