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.

1389 lines
51 KiB

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Mitter, a multiple-interface client for microblogging services.
# Copyright (C) 2007-2010 Mitter Contributors
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import gtk
import gobject
gobject.threads_init()
import logging
import re
import urllib2
15 years ago
import webbrowser
import gettext
from cgi import escape as html_escape
from mitterlib.ui.helpers.image_helpers import find_image
from mitterlib.ui.helpers.gtk_threading import ThreadManager
from mitterlib.ui.helpers.gtk_updatebox import UpdateBox
from mitterlib.constants import gpl_3, version
from mitterlib.ui.helpers import timesince
# Constants
_log = logging.getLogger('ui.pygtk')
URL_RE = re.compile(r'(https?://[^\s\n\r]+)', re.I)
# ----------------------------------------------------------------------
# I18n bits
# ----------------------------------------------------------------------
t = gettext.translation('ui_pygtk', fallback=True)
_ = t.gettext
N_ = t.ngettext
# ----------------------------------------------------------------------
# String with the format to the message
# ----------------------------------------------------------------------
MESSAGE_FORMAT = ('%(favourite_star)s'
'<b>%(full_name)s</b> '
'<small>(%(username)s'
'%(message_type)s'
')</small>:'
'%(read_status)s '
'%(protected_status)s '
'\n'
'%(message)s\n'
'<small>%(message_age)s</small>')
# ----------------------------------------------------------------------
# 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()
self._main_tabs = gtk.Notebook()
self._grids = []
15 years ago
(grid_widget, grid) = self._create_grid('_new_message_count')
15 years ago
self._main_tabs.insert_page(grid_widget, gtk.Label('Messages'))
self._grids.append((grid, '_new_messages_count'))
15 years ago
(replies_widget, replies) = self._create_grid(
15 years ago
'_new_replies_count')
self._main_tabs.insert_page(replies_widget, gtk.Label('Replies'))
self._grids.append((replies, '_new_replies_count'))
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()
self._message_count_updated()
# 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._grid_resize)
self._update_field.connect('focus-in-event', self._on_textarea_focus)
return main_window
15 years ago
def _create_grid(self, counter):
"""Add the displaying grid."""
# Store NetworkData objects only
grid_store = gtk.ListStore(object)
grid_store.set_sort_column_id(0, gtk.SORT_ASCENDING)
grid_store.set_sort_func(0, self._order_datetime)
15 years ago
grid = gtk.TreeView(grid_store)
grid.set_property('headers-visible', False)
grid.set_rules_hint(True) # change color for each row
user_renderer = gtk.CellRendererPixbuf()
user_column = gtk.TreeViewColumn('User', user_renderer)
user_column.set_fixed_width(48) # avatar size (we resize to 48x48)
user_column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
user_column.set_cell_data_func(user_renderer,
self._cell_renderer_user)
message_renderer = gtk.CellRendererText()
message_renderer.set_property('wrap-mode', gtk.WRAP_WORD)
message_renderer.set_property('wrap-width', 200)
message_column = gtk.TreeViewColumn('Message',
message_renderer)
message_column.set_cell_data_func(message_renderer,
self._cell_renderer_message)
15 years ago
grid.append_column(user_column)
grid.append_column(message_column)
15 years ago
grid.set_resize_mode(gtk.RESIZE_IMMEDIATE)
grid.connect('cursor-changed', self._message_selected)
grid.connect('button-press-event', self._click_message)
grid.connect('popup-menu', self._message_popup) # Menu button
grid.connect('cursor-changed', self._mark_message_read, counter)
scrolled_window = gtk.ScrolledWindow()
scrolled_window.set_policy(gtk.POLICY_NEVER, gtk.POLICY_ALWAYS)
15 years ago
scrolled_window.add(grid)
15 years ago
return (scrolled_window, grid)
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="Favourite" />
<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="Favourite" />
</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.
action_group = gtk.ActionGroup('Mitter')
# Actions related to the UI elements above
# Top-level menu actions
file_action = gtk.Action('File', _('_File'), _('File'), None)
action_group.add_action(file_action)
edit_action = gtk.Action('Edit', _('_Edit'), _('Edit'), None)
action_group.add_action(edit_action)
message_action = gtk.Action('Message', _('_Message'),
_('Message related options'), None)
action_group.add_action(message_action)
view_action = gtk.Action('View', _('_View'), _('View'), None)
action_group.add_action(view_action)
help_action = gtk.Action('Help', _('_Help'), _('Help'), None)
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)
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)
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)
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)
action_group.add_action(all_read_action)
self._update_action = gtk.Action('Update', _('_Update'),
_('Update your status'), gtk.STOCK_ADD)
self._update_action.connect('activate', self._update_status)
action_group.add_action_with_accel(self._update_action,
'Return')
self._cancel_action = gtk.Action('Cancel', _('_Cancel'),
_('Cancel the update'), gtk.STOCK_CANCEL)
self._cancel_action.connect('activate', self._clear_text)
action_group.add_action_with_accel(self._cancel_action,
'Escape')
settings_action = gtk.Action('Settings', _('_Settings'),
_('Settings'), gtk.STOCK_PREFERENCES)
15 years ago
settings_action.connect('activate', self._show_settings)
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)
action_group.add_action_with_accel(new_action, '<Ctrl>n')
self._delete_action = gtk.Action('Delete', _('_Delete'),
_('Delete a post'), gtk.STOCK_DELETE)
self._delete_action.set_property('sensitive', False)
self._delete_action.connect('activate', self._delete_message)
action_group.add_action_with_accel(self._delete_action, 'Delete')
self._reply_action = gtk.Action('Reply', _('_Reply'),
_("Send a response to someone's else message"), gtk.STOCK_REDO)
self._reply_action.set_property('sensitive', False)
15 years ago
self._reply_action.connect('activate', self._reply_message)
action_group.add_action_with_accel(self._reply_action, '<Ctrl>r')
self._repost_action = gtk.Action('Repost', _('Re_post'),
_("Put someone's else message on your timeline"),
gtk.STOCK_CONVERT)
self._repost_action.set_property('sensitive', False)
15 years ago
self._repost_action.connect('activate', self._repost_message)
action_group.add_action_with_accel(self._repost_action, '<Ctrl>p')
15 years ago
self._favourite_action = gtk.Action('Favourite', _('_Favorite'),
_('Toggle the favorite status of a message'),
gtk.STOCK_ABOUT)
self._favourite_action.set_property('sensitive', False)
self._favourite_action.connect('activate', self._favourite_message)
action_group.add_action_with_accel(self._favourite_action, '<Ctrl>f')
# view actions
view_messages_action = gtk.Action('Messages', _('_Messages'),
_('Display messages'), None)
view_messages_action.connect('activate', self._change_tab, 0)
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)
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)
action_group.add_action(about_action)
# definition of the UI
uimanager = gtk.UIManager()
uimanager.insert_action_group(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
15 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')
15 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'))
15 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"'])
15 years ago
if self._images['logo']:
about_window.set_logo(self._images['logo'])
15 years ago
about_window.run()
about_window.hide()
# ------------------------------------------------------------
# Cell rendering functions
# ------------------------------------------------------------
def _cell_renderer_user(self, column, cell, store, position):
"""Callback for the user column. Used to created the pixbuf of the
userpic."""
data = store.get_value(position, 0)
pic = data.author.avatar
cell.set_property('pixbuf', self._avatars[pic])
return
def _cell_renderer_message(self, column, cell, store, position):
"""Callback for the message column. We need this to adjust the markup
property of the cell, as setting it as text won't do any markup
processing."""
data = store.get_value(position, 0)
time = timesince.timesince(data.message_time)
message_values = {}
# unescape escaped entities that pango is not okay with
message_values['message'] = html_escape(data.message)
message_values['username'] = html_escape(data.author.username)
message_values['full_name'] = html_escape(data.author.name)
message_values['message_age'] = time
# highlight URLs
mask = r'<span foreground="%s">\1</span>' % (
self._options[self.NAMESPACE]['link_colour'])
message_values['message'] = URL_RE.sub(mask,
message_values['message'])
# use a different highlight for the current user
# TODO: How to handle this with several networks?
#message = re.sub(r'(@'+self.twitter.username+')',
# r'<span foreground="#FF6633">\1</span>',
# message)
if not data.read:
message_values['read_status'] = (' ' +
self._options[self.NAMESPACE]['unread_char'])
else:
message_values['read_status'] = ''
if data.favourite:
message_values['favourite_star'] = (
self._options[self.NAMESPACE]['unfavourite_char'])
else:
message_values['favourite_star'] = (
self._options[self.NAMESPACE]['favourite_char'])
info = []
15 years ago
if data.reposted_by:
info.append(_(' &#8212; <i>reposted by %s</i>') %
(data.reposted_by.username))
if data.parent_owner:
info.append(_(' &#8212; <i>in reply to %s</i>') %
(data.parent_owner.username))
if data.protected:
message_values['protected_status'] = (
self._options[self.NAMESPACE]['protected_char'])
else:
message_values['protected_status'] = ''
message_values['message_type'] = ''.join(info)
markup = MESSAGE_FORMAT % (message_values)
cell.set_property('markup', markup)
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
# ------------------------------------------------------------
# Helper functions
# ------------------------------------------------------------
def _update_sensitivity(self, enabled):
"""Set the "sensitive" property of the update action and button. Both
should have the same property, so whenever you need to disable/enable
them, use this function."""
self._update_action.set_property('sensitive', enabled)
return
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.)"""
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 _url_popup(self, widget, path, event):
15 years ago
"""Builds the popup with URLs in the cell pointed by *path*. Requires
the *event* that the widget received."""
iter = widget.get_model().get_iter(path)
message = widget.get_model().get_value(iter, 0)
15 years ago
items = []
link = self._connection.link(message)
if link:
network = self._connection.name(message.network)
items.append((_('Open on %s') % (network), link))
15 years ago
urls = URL_RE.findall(message.message)
for url in urls:
if len(url) > 20:
title = url[:20] + '...'
else:
title = url
items.append((title, url))
if len(items) == 0:
return
popup = gtk.Menu()
for url in items:
(title, url) = url
15 years ago
item = gtk.MenuItem(title)
item.connect('activate', self._open_url, url)
popup.append(item)
popup.show_all()
popup.popup(None, None, None, event.button, event.time)
15 years ago
return True
15 years ago
def _message_count_updated(self):
"""Update the elements in the interface that should change in case of
changes in the message count."""
child = self._main_tabs.get_nth_page(0)
self._main_tabs.set_tab_label_text(child, _('Messages (%d)') %
(self._new_message_count))
15 years ago
child = self._main_tabs.get_nth_page(1)
self._main_tabs.set_tab_label_text(child, _('Replies (%d)') %
15 years ago
(self._new_replies_count))
if self._statusicon:
if (self._new_message_count + self._new_replies_count) == 0:
15 years ago
self._statusicon.set_from_pixbuf(self._images['icon'])
else:
self._statusicon.set_from_pixbuf(self._images['new-messages'])
return
15 years ago
def _fill_store(self, store, data):
"""Using the results from the request, fill a gtk.Store."""
for message in data:
message.read = False
pic = message.author.avatar
15 years ago
if not pic in self._avatars:
# set the user avatar to the default image, so it won't get
# queued again. Once downloaded, the _post_download_pic will
# update the image and force a redraw.
self._avatars[pic] = self._images['avatar']
self._threads.add_work(self._post_download_pic,
self._exception_download_pic,
self._download_pic,
pic)
store.prepend([message])
store.sort_column_changed()
return
# ------------------------------------------------------------
# Widget callback functions
# ------------------------------------------------------------
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:
self._update_field.hide()
return
_log.debug('Status: %s', status)
self._update_statusbar(_('Sending update...'))
self._update_sensitivity(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."""
self._delete_info = None
15 years ago
self._clear_reply()
self._update_field.hide()
# 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.
15 years ago
self._message_selected(child.get_child())
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
15 years ago
def _grid_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."""
for grid in self._grids:
(grid, counter) = grid
model = grid.get_model()
if len(model) == 0:
# don't try to update if there are no data to be updated
return
(win_width, win_height) = self._main_window.get_size()
column = grid.get_column(1)
iter = model.get_iter_first()
path = model.get_path(iter)
width = win_width - 70 # 48 = icon size
# TODO: Find out where those 12 pixels came from and/or if they
# are platform specific.
for renderer in column.get_cell_renderers():
renderer.set_property('wrap-width', width)
while iter:
path = model.get_path(iter)
model.row_changed(path, iter)
iter = model.iter_next(iter)
return
def _order_datetime(self, model, iter1, iter2, user_data=None):
"""Used by the ListStore to sort the columns (in our case, "column")
by date."""
message1 = model.get_value(iter1, 0)
message2 = model.get_value(iter2, 0)
if (not message1) or \
(not message1.message_time) or \
(message1.message_time > message2.message_time):
return -1
if (not message2) or \
(not message2.message_time) or \
(message2.message_time > message1.message_time):
return 1
return 0
def _message_selected(self, view, user_data=None):
"""Callback when a row in the list is selected. Mostly, we'll check
if the message is from the logged user and we change de "sensitive"
property of the message actions to reflect what the user can and
cannot do."""
(model, iter) = view.get_selection().get_selected()
15 years ago
if not iter:
return
data = model.get_value(iter, 0)
self._delete_action.set_property('sensitive',
self._connection.can_delete(data))
self._reply_action.set_property('sensitive',
self._connection.can_reply(data))
self._repost_action.set_property('sensitive',
self._connection.can_repost(data))
self._favourite_action.set_property('sensitive',
self._connection.can_favourite(data))
return 0
15 years ago
def _clear_posts(self, widget, user_data=None):
"""Clear the list of posts from the grid."""
15 years ago
page = self._main_tabs.get_current_page()
(grid, counter) = self._grids[page]
grid.get_model().clear()
setattr(self, counter, 0)
self._message_count_updated()
15 years ago
return
def _mark_all_read(self, widget, user_data=None):
"""Mark all messages as read."""
page = self._main_tabs.get_current_page()
(grid, counter) = self._grids[page]
model = grid.get_model()
if len(model) == 0:
# no messages, so we don't worry (if I recall correctly,
# get_iter_first will fail if the model is empty)
return
iter = model.get_iter_first()
while iter:
message = model.get_value(iter, 0)
message.read = True
iter = model.iter_next(iter)
# update the counters
set(attr, self, counter, 0)
self._message_count_updated()
return
def _delete_message(self, widget, user_data=None):
"""Delete a message."""
(grid, counter) = self._grids[self._main_tabs.get_current_page()]
(model, iter) = grid.get_selection().get_selected()
message = model.get_value(iter, 0)
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...'))
self._delete_info = (grid, iter)
_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."""
(grid, _) = self._grids[self._main_tabs.get_current_page()]
(model, iter) = grid.get_selection().get_selected()
15 years ago
message = model.get_value(iter, 0)
self._update_field.text = self._connection.reply_prefix(message)
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."""
(grid, counter) = self._grids[self._main_tabs.get_current_page()]
(model, iter) = grid.get_selection().get_selected()
15 years ago
message = model.get_value(iter, 0)
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 _favourite_message(self, widget, user_data=None):
"""Toggle the favourite status of a message."""
(grid, counter) = self._grids[self._main_tabs.get_current_page()]
(model, iter) = grid.get_selection().get_selected()
message = model.get_value(iter, 0)
self._favourite_info = (grid, iter)
if message.favourite:
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_favourite_message,
self._exception_favourite_message,
self._connection.favourite,
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
15 years ago
def _click_message(self, widget, event, user_data=None):
"""Check the click on the message and, if it's a right click, call the
_message_popup function to show the popup."""
if event.button != 3:
# not right click
return False
path = widget.get_path_at_pos(event.x, event.y)
15 years ago
if not path:
return False
(path, _, _, _) = path
return self._url_popup(widget, path, event)
15 years ago
def _message_popup(self, widget, user_data=None):
15 years ago
"""Builds the popup with the URLs in the message."""
_log.debug('Popup')
(path, _) = widget.get_cursor()
15 years ago
if not path:
return True
# create a syntetic event (the popup requires one)
event = gtk.gdk.Event(gtk.gdk.BUTTON_PRESS)
event.button = 3
return self._url_popup(widget, path, event)
15 years ago
def _open_url(self, widget, user_data=None):
"""Opens an URL (used mostly from popup menu items.)"""
webbrowser.open_new_tab(user_data)
return
def _mark_message_read(self, widget, user_data=None):
"""Mark a message as read when it's selected."""
(path, _) = widget.get_cursor()
if not path:
return True
iter = widget.get_model().get_iter(path)
message = widget.get_model().get_value(iter, 0)
if message.read:
return True
# Do it generically, so we can reuse this function to both grids
counter = getattr(self, user_data)
setattr(self, user_data, counter - 1)
message.read = True
self._message_count_updated()
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.)
self._delete_action.set_property('sensitive', False)
self._reply_action.set_property('sensitive', False)
self._repost_action.set_property('sensitive', False)
self._favourite_action.set_property('sensitive', False)
return
def _new_message(self, widget, user_data=None):
"""Opens the text area for a new message."""
self._show_update_box()
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, counter) = self._grids[0]
store = grid.get_model()
15 years ago
self._fill_store(store, results)
grid.queue_draw()
self._new_message_count += len(results)
self._message_count_updated()
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'])
return
15 years ago
### replies callback
def _post_get_replies(self, widget, results):
"""Called after we retrieve the replies."""
_log.debug("%d new replies", len(results))
(grid, counter) = self._grids[1]
store = grid.get_model()
15 years ago
self._fill_store(store, results)
grid.queue_draw()
15 years ago
self._new_replies_count += len(results)
self._message_count_updated()
# once our update went fine, we can queue the next one. This avoids
# any problems if case there is an exception.
interval = self._options[self.NAMESPACE]['refresh_interval']
_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)
message = '%s %s.' % (prefix, suffix)
self._update_statusbar(message % (interval))
15 years ago
self._refresh_id = gobject.timeout_add(
interval * 60 * 1000,
self._refresh, None)
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 picture request
def _post_download_pic(self, widget, data):
"""Called after the data from the picture is available."""
(url, data) = data
loader = gtk.gdk.PixbufLoader()
loader.write(data)
loader.close()
user_pic = loader.get_pixbuf()
user_pic = user_pic.scale_simple(48, 48, gtk.gdk.INTERP_BILINEAR)
self._avatars[url] = user_pic
(grid, _) = self._grids[0]
grid.queue_draw()
return
### Results from the avatar download request
def _post_avatar_pic(self, widget, data):
"""Called after the data from the picture of the user's avatar is
available."""
(url, data) = data
loader = gtk.gdk.PixbufLoader()
loader.write(data)
loader.close()
self._update_field.pixbuf = loader.get_pixbuf()
return
def _exception_download_pic(self, widget, exception):
"""Called in case we have a problem downloading an user avatar."""
_log.debug('Exception trying to get an avatar.')
_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._clear_text(None)
return
def _exception_update_status(self, widget, exception):
"""Called when there is an exception updating the status."""
# TODO: Need the check the type of exception we got.
_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()
return
### Results for the delete message call
def _post_delete_message(self, widget, data):
"""Called when the message is deleted successfully."""
_log.debug('Message deleted.')
if self._delete_info:
(grid, iter) = self._delete_info
grid.get_model().remove(iter)
self._delete_info = None
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 favourite call
def _post_favourite_message(self, widget, data):
"""Called when the message was favourited successfully."""
_log.debug('Favourite status changed.')
(grid, iter) = self._favourite_info
message = grid.get_model().get_value(iter, 0)
if message.favourite:
15 years ago
display = _('Message unfavorited.')
else:
15 years ago
display = _('Message favorited.')
self._update_statusbar(display)
message.favourite = not message.favourite
self._favourite_info = None
return
def _exception_favourite_message(self, widget, exception):
"""Called when the message couldn't be favourited."""
_log.debug('Favourite 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."""
self._connection = connection
self._options = options
self._avatars = {}
self._pic_queue = set()
# 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'))
# 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._delete_info = None
15 years ago
self._reply_message_id = None
self._favourite_info = None
self._new_message_count = 0 # TODO: Turn this into a @property
self._new_replies_count = 0 # TODO: Turn this into a @property
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._main_window = self._create_main_window()
self._main_window.show()
self._threads = ThreadManager()
# get a user avatar. We need that for the updatebox.
user = self._connection.user()
self._threads.add_work(self._post_avatar_pic,
self._exception_download_pic,
self._download_pic,
user.avatar)
# queue the first fetch
self._refresh_id = None # The auto-refresh manager.
self._refresh()
gtk.main()
@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_colour',
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='unfavourite_char',
help='String to be used to indicate a message is not ' \
'marked as favourite.',
metavar='CHAR',
default='&#9733;',
is_cmd_option=False)
options.add_option(
group=self.NAMESPACE,
option='favourite_char',
help='String to be used to indicate a message is marked ' \
'as favourite.',
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)