|
|
|
#!/usr/bin/env python
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
|
|
# Mitter, a Maemo client for Twitter.
|
|
|
|
# Copyright (C) 2007, 2008 Julio Biason, Deepak Sarda
|
|
|
|
#
|
|
|
|
# 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 threading
|
|
|
|
import Queue
|
|
|
|
import re
|
|
|
|
import urllib2
|
|
|
|
import webbrowser
|
|
|
|
|
|
|
|
from cgi import escape as html_escape
|
|
|
|
|
|
|
|
from mitterlib.ui.helpers.image_helpers import find_image
|
|
|
|
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)
|
|
|
|
|
|
|
|
MESSAGE_FORMAT = '%s' \
|
|
|
|
'<b>%s</b> ' \
|
|
|
|
'<small>(%s' \
|
|
|
|
'%s' \
|
|
|
|
')</small>:' \
|
|
|
|
'%s\n' \
|
|
|
|
'%s\n' \
|
|
|
|
'<small>%s</small>'
|
|
|
|
|
|
|
|
|
|
|
|
# ----------------------------------------------------------------------
|
|
|
|
# Helper Functions (not related to objects or that don't need direct access to
|
|
|
|
# the objects contents.)
|
|
|
|
# ----------------------------------------------------------------------
|
|
|
|
|
|
|
|
def _buffer_text(text_buffer):
|
|
|
|
"""Return the content of a gtk.TextBuffer."""
|
|
|
|
start = text_buffer.get_start_iter()
|
|
|
|
end = text_buffer.get_end_iter()
|
|
|
|
|
|
|
|
text = text_buffer.get_text(start, end, include_hidden_chars=False)
|
|
|
|
|
|
|
|
return text
|
|
|
|
|
|
|
|
# ----------------------------------------------------------------------
|
|
|
|
# Threading related objects.
|
|
|
|
# These classes are based on the code available at http://gist.github.com/51686
|
|
|
|
# (c) 2008, John Stowers <john.stowers@gmail.com>
|
|
|
|
# ----------------------------------------------------------------------
|
|
|
|
|
|
|
|
class _IdleObject(gobject.GObject):
|
|
|
|
"""
|
|
|
|
Override gobject.GObject to always emit signals in the main thread
|
|
|
|
by emmitting on an idle handler
|
|
|
|
"""
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
gobject.GObject.__init__(self)
|
|
|
|
|
|
|
|
def emit(self, *args):
|
|
|
|
gobject.idle_add(gobject.GObject.emit, self, *args)
|
|
|
|
|
|
|
|
|
|
|
|
class _WorkerThread(threading.Thread, _IdleObject):
|
|
|
|
"""
|
|
|
|
A single working thread.
|
|
|
|
"""
|
|
|
|
__gsignals__ = {
|
|
|
|
"completed": (
|
|
|
|
gobject.SIGNAL_RUN_LAST,
|
|
|
|
gobject.TYPE_NONE,
|
|
|
|
(gobject.TYPE_PYOBJECT, )), # list/networkdata
|
|
|
|
"exception": (
|
|
|
|
gobject.SIGNAL_RUN_LAST,
|
|
|
|
gobject.TYPE_NONE,
|
|
|
|
(gobject.TYPE_PYOBJECT, ))} # The exception
|
|
|
|
|
|
|
|
def __init__(self, function, *args, **kwargs):
|
|
|
|
threading.Thread.__init__(self)
|
|
|
|
_IdleObject.__init__(self)
|
|
|
|
self._function = function
|
|
|
|
self._args = args
|
|
|
|
self._kwargs = kwargs
|
|
|
|
|
|
|
|
def run(self):
|
|
|
|
# call the function
|
|
|
|
_log.debug('Thread d %s calling %s', self.getName(),
|
|
|
|
str(self._function.__name__))
|
|
|
|
|
|
|
|
args = self._args
|
|
|
|
kwargs = self._kwargs
|
|
|
|
|
|
|
|
try:
|
|
|
|
result = self._function(*args, **kwargs)
|
|
|
|
except Exception, exc: # Catch ALL exceptions
|
|
|
|
# TODO: Check if this catch all warnins too!
|
|
|
|
_log.debug('Exception inside thread: %s', str(exc))
|
|
|
|
self.emit("exception", exc)
|
|
|
|
return
|
|
|
|
|
|
|
|
_log.debug('Thread id %s completed', self.getName())
|
|
|
|
|
|
|
|
self.emit("completed", result)
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
class _ThreadManager(object):
|
|
|
|
"""Manages the threads."""
|
|
|
|
|
|
|
|
def __init__(self, max_threads=2):
|
|
|
|
"""Start the thread pool. The number of threads in the pool is defined
|
|
|
|
by `pool_size`, defaults to 2."""
|
|
|
|
self._max_threads = max_threads
|
|
|
|
self._thread_pool = []
|
|
|
|
self._running = []
|
|
|
|
self._thread_id = 0
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
def _remove_thread(self, widget, arg=None):
|
|
|
|
"""Called when the thread completes. We remove it from the thread list
|
|
|
|
(dictionary, actually) and start the next thread (if there is one)."""
|
|
|
|
|
|
|
|
# not actually a widget. It's the object that emitted the signal, in
|
|
|
|
# this case, the _WorkerThread object.
|
|
|
|
thread_id = widget.getName()
|
|
|
|
self._running.remove(thread_id)
|
|
|
|
|
|
|
|
_log.debug('Thread id %s completed, %d threads in the queue, ' \
|
|
|
|
'%d still running', thread_id, len(self._thread_pool),
|
|
|
|
len(self._running))
|
|
|
|
|
|
|
|
if self._thread_pool:
|
|
|
|
if len(self._running) < self._max_threads:
|
|
|
|
next = self._thread_pool.pop()
|
|
|
|
_log.debug('Dequeuing thread %s', next.getName())
|
|
|
|
self._running.append(next.getName())
|
|
|
|
next.start()
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
def add_work(self, complete_cb, exception_cb, func, *args, **kwargs):
|
|
|
|
"""Add a work to the thread list. `complete_cb` is the function to be
|
|
|
|
called with the result of the work. `exception_cb` is the function to
|
|
|
|
be called if there are any exceptions raised. Note that, once the
|
|
|
|
work is complete, one of those will be called, not both. `func` is the
|
|
|
|
function to be called in the secondary threads. `args` and `kwargs`
|
|
|
|
are parameters passed to the function."""
|
|
|
|
|
|
|
|
thread = _WorkerThread(func, *args, **kwargs)
|
|
|
|
thread_id = '%s-%s' % (self._thread_id, func.__name__)
|
|
|
|
|
|
|
|
thread.connect('completed', complete_cb)
|
|
|
|
thread.connect('completed', self._remove_thread)
|
|
|
|
thread.connect('exception', exception_cb)
|
|
|
|
thread.connect('exception', self._remove_thread)
|
|
|
|
thread.setName(thread_id)
|
|
|
|
|
|
|
|
if len(self._running) < self._max_threads:
|
|
|
|
# immediatelly start the thread
|
|
|
|
self._running.append(thread_id)
|
|
|
|
thread.start()
|
|
|
|
else:
|
|
|
|
# add the thread to the queue
|
|
|
|
running_names = ', '.join(self._running)
|
|
|
|
_log.debug('Threads %s running, adding %s to the queue',
|
|
|
|
running_names, thread_id)
|
|
|
|
self._thread_pool.append(thread)
|
|
|
|
|
|
|
|
self._thread_id += 1
|
|
|
|
return
|
|
|
|
|
|
|
|
def clear(self):
|
|
|
|
"""Clear the thread pool list. This will cause the manager to stop
|
|
|
|
working after the threads finish."""
|
|
|
|
self._thread_pool = []
|
|
|
|
self._running = []
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# ----------------------------------------------------------------------
|
|
|
|
# Mitter interface object
|
|
|
|
# ----------------------------------------------------------------------
|
|
|
|
|
|
|
|
class Interface(object):
|
|
|
|
"""Linux/GTK interface for Mitter."""
|
|
|
|
|
|
|
|
NAMESPACE = 'pygtk'
|
|
|
|
|
|
|
|
# ------------------------------------------------------------
|
|
|
|
# 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['main']:
|
|
|
|
main_window.set_icon(self._images['main'])
|
|
|
|
|
|
|
|
(menu, toolbar, accelerators) = self._create_menu_and_toolbar()
|
|
|
|
update_field = self._create_update_box()
|
|
|
|
self._statusbar = self._create_statusbar()
|
|
|
|
|
|
|
|
self._main_tabs = gtk.Notebook()
|
|
|
|
|
|
|
|
(grid_widget, self._grid) = self._create_grid('_new_message_count')
|
|
|
|
self._main_tabs.insert_page(grid_widget, gtk.Label('Messages'))
|
|
|
|
|
|
|
|
(replies_widget, self._replies) = self._create_grid(
|
|
|
|
'_new_replies_count')
|
|
|
|
self._main_tabs.insert_page(replies_widget, gtk.Label('Replies'))
|
|
|
|
|
|
|
|
update_box = gtk.VPaned()
|
|
|
|
update_box.pack1(self._main_tabs, resize=True, shrink=False)
|
|
|
|
update_box.pack2(update_field, resize=False, shrink=True)
|
|
|
|
# TODO: Save the size of the update box in the config file.
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
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_text.get_buffer().connect('changed', self._count_chars)
|
|
|
|
self._update_text.connect('focus-out-event',
|
|
|
|
self._remove_count_status)
|
|
|
|
self._update_text.connect('focus-in-event', self._on_textarea_focus)
|
|
|
|
|
|
|
|
return main_window
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
grid.append_column(user_column)
|
|
|
|
grid.append_column(message_column)
|
|
|
|
|
|
|
|
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)
|
|
|
|
scrolled_window.add(grid)
|
|
|
|
|
|
|
|
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" />
|
|
|
|
<separator />
|
|
|
|
<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" />
|
|
|
|
<separator />
|
|
|
|
<menuitem action="Update" />
|
|
|
|
<menuitem action="Cancel" />
|
|
|
|
<menuitem action="ShrinkURL" />
|
|
|
|
<menuitem action="MuteNotify" />
|
|
|
|
<separator />
|
|
|
|
<menuitem action="Settings" />
|
|
|
|
</menu>
|
|
|
|
<menu action="Message">
|
|
|
|
<menuitem action="Delete" />
|
|
|
|
<menuitem action="Reply" />
|
|
|
|
<menuitem action="Repost" />
|
|
|
|
<menuitem action="Favourite" />
|
|
|
|
</menu>
|
|
|
|
<menu action="View">
|
|
|
|
<menuitem action="Messages" />
|
|
|
|
<menuitem action="Replies" />
|
|
|
|
</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)
|
|
|
|
|
|
|
|
self._update_action = gtk.Action('Update', '_Update',
|
|
|
|
'Update your status', gtk.STOCK_ADD)
|
|
|
|
self._update_action.set_property('sensitive', False)
|
|
|
|
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')
|
|
|
|
|
|
|
|
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')
|
|
|
|
|
|
|
|
settings_action = gtk.Action('Settings', '_Settings',
|
|
|
|
'Settings', gtk.STOCK_PREFERENCES)
|
|
|
|
settings_action.connect('activate', self._show_settings)
|
|
|
|
action_group.add_action(settings_action)
|
|
|
|
|
|
|
|
# Message actions
|
|
|
|
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)
|
|
|
|
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)
|
|
|
|
self._repost_action.connect('activate', self._repost_message)
|
|
|
|
action_group.add_action_with_accel(self._repost_action, '<Ctrl>p')
|
|
|
|
|
|
|
|
self._favourite_action = gtk.Action('Favourite', '_Favourite',
|
|
|
|
'Toggle the favourite 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')
|
|
|
|
|
|
|
|
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_update_box(self):
|
|
|
|
"""Create the widgets related to the update box"""
|
|
|
|
self._update_text = gtk.TextView()
|
|
|
|
self._update_text.set_property('wrap-mode', gtk.WRAP_WORD)
|
|
|
|
text_buffer = self._update_text.get_buffer()
|
|
|
|
|
|
|
|
self._update_button = gtk.Button(label='Send')
|
|
|
|
self._update_button.connect('clicked', self._update_status)
|
|
|
|
self._update_button.set_property('sensitive', False)
|
|
|
|
|
|
|
|
self._cancel_button = gtk.Button(label='Cancel')
|
|
|
|
self._cancel_button.connect('clicked', self._clear_text)
|
|
|
|
|
|
|
|
info_box = gtk.HBox(True, 0)
|
|
|
|
self._count_label = gtk.Label()
|
|
|
|
self._count_label.set_justify(gtk.JUSTIFY_LEFT)
|
|
|
|
self._reply_label = gtk.Label()
|
|
|
|
|
|
|
|
update_box = gtk.HBox(False, 0)
|
|
|
|
update_box.pack_start(self._update_text, expand=True, fill=True,
|
|
|
|
padding=0)
|
|
|
|
update_box.pack_start(self._update_button, expand=False, fill=False,
|
|
|
|
padding=0)
|
|
|
|
update_box.pack_start(self._cancel_button, expand=False, fill=False,
|
|
|
|
padding=0)
|
|
|
|
|
|
|
|
# Spell checking the update box
|
|
|
|
spell_check_enabled = self._options[self.NAMESPACE]['spell_check']
|
|
|
|
if spell_check_enabled:
|
|
|
|
try:
|
|
|
|
import gtkspell
|
|
|
|
import locale
|
|
|
|
self.spell_check_support = True
|
|
|
|
language = locale.getlocale()[0]
|
|
|
|
self.spell_check = gtkspell.Spell(self._update_text, language)
|
|
|
|
_log.debug('Spell checking turned on with language: %s' \
|
|
|
|
% (language))
|
|
|
|
except:
|
|
|
|
self._options[self.NAMESPACE]['spell_check'] = False
|
|
|
|
self.spell_check_support = False
|
|
|
|
_log.debug('Error initializing spell checking: ' \
|
|
|
|
'spell checking disabled')
|
|
|
|
|
|
|
|
return update_box
|
|
|
|
|
|
|
|
def _create_statusbar(self):
|
|
|
|
"""Create the statusbar."""
|
|
|
|
statusbar = gtk.Statusbar()
|
|
|
|
self._statusbar_context = statusbar.get_context_id('Mitter')
|
|
|
|
return statusbar
|
|
|
|
|
|
|
|
def _show_about(self, widget):
|
|
|
|
"""Show the about dialog."""
|
|
|
|
about_window = gtk.AboutDialog()
|
|
|
|
about_window.set_name('Mitter')
|
|
|
|
about_window.set_version(version)
|
|
|
|
about_window.set_copyright('2007-2009 Mitter Contributors')
|
|
|
|
about_window.set_license(gpl_3)
|
|
|
|
about_window.set_website('http://code.google.com/p/mitter')
|
|
|
|
about_window.set_website_label('Mitter on GoogleCode')
|
|
|
|
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"'])
|
|
|
|
if self._images['main']:
|
|
|
|
about_window.set_logo(self._images['main'])
|
|
|
|
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.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)
|
|
|
|
|
|
|
|
# unescape escaped entities that pango is not okay with
|
|
|
|
message = html_escape(data.message)
|
|
|
|
username = html_escape(data.username)
|
|
|
|
full_name = html_escape(data.name)
|
|
|
|
|
|
|
|
# highlight URLs
|
|
|
|
mask = r'<span foreground="%s">\1</span>' % (
|
|
|
|
self._options[self.NAMESPACE]['link_colour'])
|
|
|
|
message = URL_RE.sub(mask, 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:
|
|
|
|
read_status = '<big>●</big>'
|
|
|
|
else:
|
|
|
|
read_status = ''
|
|
|
|
|
|
|
|
if data.favourite:
|
|
|
|
favourite = '★'
|
|
|
|
else:
|
|
|
|
favourite = '☆'
|
|
|
|
|
|
|
|
info = []
|
|
|
|
if data.reposted_by:
|
|
|
|
info.append(' — <i>reposted by %s</i>' % (data.reposted_by))
|
|
|
|
|
|
|
|
if data.parent_owner:
|
|
|
|
info.append(' — <i>in reply to %s</i>' %
|
|
|
|
(data.parent_owner))
|
|
|
|
|
|
|
|
info_message = ''.join(info)
|
|
|
|
|
|
|
|
markup = MESSAGE_FORMAT % (favourite, full_name, username,
|
|
|
|
info_message, read_status, message, time)
|
|
|
|
|
|
|
|
cell.set_property('markup', markup)
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
def _cell_renderer_options(self, column, cell, store, position):
|
|
|
|
"""Callback for the options renderer. Adds the delete icon if the
|
|
|
|
message belongs to the user or reply if not."""
|
|
|
|
data = store.get_value(position, 0)
|
|
|
|
cell.set_property('pixbuf', self._reply_pixbuf)
|
|
|
|
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_button.set_property('sensitive', enabled)
|
|
|
|
self._update_action.set_property('sensitive', enabled)
|
|
|
|
self._cancel_button.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
|
|
|
|
|
|
|
|
def _clear_reply(self):
|
|
|
|
"""Clear the info about a reply."""
|
|
|
|
self._reply_message_id = None
|
|
|
|
self._reply_label.set_text('')
|
|
|
|
return
|
|
|
|
|
|
|
|
def _url_popup(self, widget, path, event):
|
|
|
|
"""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)
|
|
|
|
|
|
|
|
items = []
|
|
|
|
link = self._connection.link(message)
|
|
|
|
if link:
|
|
|
|
network = self._connection.name(message.network)
|
|
|
|
items.append(('Open on %s' % (network), link))
|
|
|
|
|
|
|
|
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
|
|
|
|
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)
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
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))
|
|
|
|
|
|
|
|
child = self._main_tabs.get_nth_page(1)
|
|
|
|
self._main_tabs.set_tab_label_text(child, 'Replies (%d)' %
|
|
|
|
(self._new_replies_count))
|
|
|
|
|
|
|
|
if self._statusicon:
|
|
|
|
if self._new_message_count == 0:
|
|
|
|
self._statusicon.set_from_pixbuf(self._images['main'])
|
|
|
|
else:
|
|
|
|
self._statusicon.set_from_pixbuf(self._images['new-messages'])
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
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.avatar
|
|
|
|
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
|
|
|
|
|
|
|
|
def _update_word_wrap(self, widget):
|
|
|
|
"""Update the word wrap for the widget."""
|
|
|
|
model = widget.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 = widget.get_column(1)
|
|
|
|
iter = model.get_iter_first()
|
|
|
|
path = model.get_path(iter)
|
|
|
|
|
|
|
|
cell_rectangle = widget.get_cell_area(path, column)
|
|
|
|
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 _get_current_grid(self):
|
|
|
|
"""Return the grid of the current selected tab."""
|
|
|
|
page = self._main_tabs.get_current_page()
|
|
|
|
if page == 0:
|
|
|
|
return self._grid
|
|
|
|
elif page == 1:
|
|
|
|
return self._replies
|
|
|
|
|
|
|
|
|
|
|
|
# ------------------------------------------------------------
|
|
|
|
# Widget callback functions
|
|
|
|
# ------------------------------------------------------------
|
|
|
|
def _count_chars(self, text_buffer):
|
|
|
|
"""Count the number of chars in the edit field and update the
|
|
|
|
label that shows the available space."""
|
|
|
|
text = _buffer_text(text_buffer)
|
|
|
|
count = len(text)
|
|
|
|
|
|
|
|
if self._reply_message_id:
|
|
|
|
suffix = '(replying to %s)' % (self._reply_message_id.username)
|
|
|
|
else:
|
|
|
|
suffix = ''
|
|
|
|
|
|
|
|
# TODO: gettext to properly use "characters"/"character"
|
|
|
|
text = '%d characters %s' % (count, suffix)
|
|
|
|
|
|
|
|
self._statusbar.push(self._remove_count_status(), text)
|
|
|
|
self._update_sensitivity(not (count == 0))
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
def _update_status(self, widget):
|
|
|
|
"""Update your status."""
|
|
|
|
_log.debug('Updating status.')
|
|
|
|
status = _buffer_text(self._update_text.get_buffer())
|
|
|
|
status = status.strip()
|
|
|
|
if not status:
|
|
|
|
return
|
|
|
|
|
|
|
|
_log.debug('Status: %s', status)
|
|
|
|
self._remove_count_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,
|
|
|
|
status=status,
|
|
|
|
reply_to=self._reply_message_id)
|
|
|
|
return
|
|
|
|
|
|
|
|
def _clear_text(self, widget):
|
|
|
|
"""Clear the text field."""
|
|
|
|
self._update_text.get_buffer().set_text('')
|
|
|
|
|
|
|
|
self._delete_iter = None
|
|
|
|
self._clear_reply()
|
|
|
|
self._remove_count_status()
|
|
|
|
|
|
|
|
# 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 _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."""
|
|
|
|
|
|
|
|
self._update_word_wrap(self._grid)
|
|
|
|
self._update_word_wrap(self._replies)
|
|
|
|
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()
|
|
|
|
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
|
|
|
|
|
|
|
|
def _clear_posts(self, widget, user_data=None):
|
|
|
|
"""Clear the list of posts from the grid."""
|
|
|
|
page = self._main_tabs.get_current_page()
|
|
|
|
if page == 0:
|
|
|
|
# messages
|
|
|
|
self._grid.get_model().clear()
|
|
|
|
self._new_message_count = 0
|
|
|
|
elif page == 1:
|
|
|
|
self._replies.get_model().clear()
|
|
|
|
self._new_replies_count = 0
|
|
|
|
self._message_count_updated()
|
|
|
|
return
|
|
|
|
|
|
|
|
def _delete_message(self, widget, user_data=None):
|
|
|
|
"""Delete a message."""
|
|
|
|
(model, iter) = self._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?',
|
|
|
|
buttons=gtk.BUTTONS_YES_NO);
|
|
|
|
option = confirm.run()
|
|
|
|
confirm.hide()
|
|
|
|
|
|
|
|
_log.debug("Option selected: %s" % (option))
|
|
|
|
if option == -9:
|
|
|
|
_log.debug("Delete cancelled");
|
|
|
|
return False
|
|
|
|
|
|
|
|
self._update_statusbar('Deleting message...')
|
|
|
|
self._delete_iter = 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
|
|
|
|
|
|
|
|
def _reply_message(self, widget, user_data=None):
|
|
|
|
"""Reply to someone else's message."""
|
|
|
|
grid = self._get_current_grid()
|
|
|
|
(model, iter) = grid.get_selection().get_selected()
|
|
|
|
message = model.get_value(iter, 0)
|
|
|
|
self._update_text.get_buffer().set_text(
|
|
|
|
self._connection.reply_prefix(message))
|
|
|
|
self._reply_message_id = message
|
|
|
|
self._update_text.grab_focus()
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
def _repost_message(self, widget, user_data=None):
|
|
|
|
"""Repost someone else's message on your timeline."""
|
|
|
|
grid = self._get_current_grid()
|
|
|
|
(model, iter) = grid.get_selection().get_selected()
|
|
|
|
message = model.get_value(iter, 0)
|
|
|
|
self._update_statusbar('Reposting %s message...' %
|
|
|
|
(message.username))
|
|
|
|
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 = self._get_current_grid()
|
|
|
|
(model, iter) = grid.get_selection().get_selected()
|
|
|
|
message = model.get_value(iter, 0)
|
|
|
|
|
|
|
|
self._favourite_iter = iter
|
|
|
|
|
|
|
|
if message.favourite:
|
|
|
|
display = 'Removing message from %s from favourites...'
|
|
|
|
else:
|
|
|
|
display = 'Marking message from %s as favourite...'
|
|
|
|
self._update_status(display % (message.username))
|
|
|
|
|
|
|
|
self._threads.add_work(self._post_favourite_message,
|
|
|
|
self._exception_favourite_message,
|
|
|
|
self._connection.favourite,
|
|
|
|
message)
|
|
|
|
return
|
|
|
|
|
|
|
|
def _show_settings(self, widget, user_data=None):
|
|
|
|
"""Display the settings window."""
|
|
|
|
settings_window = gtk.Dialog(title='Settings',
|
|
|
|
parent=self._main_window,
|
|
|
|
flags=gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
|
|
|
|
buttons=(gtk.STOCK_OK, 0))
|
|
|
|
|
|
|
|
# the tabs
|
|
|
|
tabs = gtk.Notebook()
|
|
|
|
|
|
|
|
# first page is the interface settings
|
|
|
|
self._refresh_interval_field = gtk.SpinButton()
|
|
|
|
self._refresh_interval_field.set_range(1, 99)
|
|
|
|
self._refresh_interval_field.set_numeric(True)
|
|
|
|
self._refresh_interval_field.set_value(
|
|
|
|
self._options[self.NAMESPACE]['refresh_interval'])
|
|
|
|
self._refresh_interval_field.set_increments(1, 5)
|
|
|
|
|
|
|
|
interface_box = gtk.Table(rows=1, columns=2, homogeneous=False)
|
|
|
|
interface_box.attach(gtk.Label('Refresh interval (minutes):'),
|
|
|
|
0, 1, 0, 1)
|
|
|
|
interface_box.attach(self._refresh_interval_field, 0, 1, 1, 2)
|
|
|
|
interface_box.show_all()
|
|
|
|
|
|
|
|
tabs.insert_page(interface_box, gtk.Label('Interface'))
|
|
|
|
|
|
|
|
# We store the fields in a dictionary, inside dictionaries for each
|
|
|
|
# NAMESPACE. To set the values, we just run the dictionaries setting
|
|
|
|
# self._options.
|
|
|
|
self._fields = {self.NAMESPACE: {'refresh_interval':
|
|
|
|
(self._refresh_interval_field, 'int')}}
|
|
|
|
|
|
|
|
# next pages are each network settings
|
|
|
|
net_options = self._connection.settings()
|
|
|
|
for network in net_options:
|
|
|
|
network_name = network['name']
|
|
|
|
|
|
|
|
rows = len(network['options'])
|
|
|
|
net_box = gtk.Table(rows=rows, columns=2, homogeneous=False)
|
|
|
|
|
|
|
|
self._fields[network_name] = {}
|
|
|
|
row = 0
|
|
|
|
for option in network['options']:
|
|
|
|
option_name = option['name']
|
|
|
|
option_value = ''
|
|
|
|
|
|
|
|
try:
|
|
|
|
option_value = self._options[network_name][option_name]
|
|
|
|
except KeyError:
|
|
|
|
pass
|
|
|
|
|
|
|
|
new_field = gtk.Entry()
|
|
|
|
new_field.set_text(option_value)
|
|
|
|
# Ony "str" and "passwd" are valid type and both use Entry()
|
|
|
|
if option['type'] == 'passwd':
|
|
|
|
new_field.set_visibility(False)
|
|
|
|
|
|
|
|
net_box.attach(gtk.Label(option_name), row, row+1, 0, 1)
|
|
|
|
net_box.attach(new_field, row, row+1, 1, 2)
|
|
|
|
|
|
|
|
row += 1
|
|
|
|
|
|
|
|
self._fields[network_name][option_name] = (new_field,
|
|
|
|
option['type'])
|
|
|
|
|
|
|
|
net_box.show_all()
|
|
|
|
tabs.insert_page(net_box, gtk.Label(network_name))
|
|
|
|
|
|
|
|
tabs.show_all()
|
|
|
|
settings_window.vbox.pack_start(tabs, True, True, 0)
|
|
|
|
settings_window.connect('response', self._update_settings)
|
|
|
|
settings_window.run()
|
|
|
|
settings_window.hide()
|
|
|
|
|
|
|
|
def _update_settings(self, widget, response_id=0, user_data=None):
|
|
|
|
"""Update the interface settings."""
|
|
|
|
for namespace in self._fields:
|
|
|
|
for option in self._fields[namespace]:
|
|
|
|
(field, field_type) = self._fields[namespace][option]
|
|
|
|
value = field.get_text()
|
|
|
|
if field_type == 'int':
|
|
|
|
value = int(value)
|
|
|
|
self._options[namespace][option] = value
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
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)
|
|
|
|
if not path:
|
|
|
|
return False
|
|
|
|
|
|
|
|
(path, _, _, _) = path
|
|
|
|
return self._url_popup(widget, path, event)
|
|
|
|
return True
|
|
|
|
|
|
|
|
def _message_popup(self, widget, event, user_data=None):
|
|
|
|
"""Builds the popup with the URLs in the message."""
|
|
|
|
_log.debug('Popup')
|
|
|
|
(path, _) = widget.get_cursor()
|
|
|
|
if not path:
|
|
|
|
return True
|
|
|
|
|
|
|
|
return self._url_popup(widget, path, event)
|
|
|
|
|
|
|
|
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 _remove_count_status(self, widget=None, user_data=None):
|
|
|
|
"""Find the context with the char count in the statusbar and remove
|
|
|
|
it (pop). Returns the context of the count, in case some other
|
|
|
|
function wants to add another message."""
|
|
|
|
count_context = self._statusbar.get_context_id('counter')
|
|
|
|
self._statusbar.pop(count_context)
|
|
|
|
return count_context
|
|
|
|
|
|
|
|
def _on_textarea_focus(self, widget, user_data=None):
|
|
|
|
"""Called when the text area gets the focus. Just to add the counter
|
|
|
|
again."""
|
|
|
|
self._count_chars(self._update_text.get_buffer())
|
|
|
|
|
|
|
|
# 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
|
|
|
|
|
|
|
|
# ------------------------------------------------------------
|
|
|
|
# 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))
|
|
|
|
|
|
|
|
store = self._grid.get_model()
|
|
|
|
self._fill_store(store, results)
|
|
|
|
self._grid.queue_draw()
|
|
|
|
|
|
|
|
self._new_message_count += len(results)
|
|
|
|
self._message_count_updated()
|
|
|
|
|
|
|
|
# now get replies
|
|
|
|
self._update_statusbar('Retrieving replies...')
|
|
|
|
self._threads.add_work(self._post_get_replies,
|
|
|
|
self._exception_get_messages,
|
|
|
|
self._connection.replies)
|
|
|
|
return
|
|
|
|
|
|
|
|
def _exception_get_messages(self, widget, exception):
|
|
|
|
"""Function called if the retrival of current messages returns an
|
|
|
|
exception."""
|
|
|
|
_log.debug(str(exception))
|
|
|
|
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
|
|
|
|
|
|
|
|
### replies callback
|
|
|
|
def _post_get_replies(self, widget, results):
|
|
|
|
"""Called after we retrieve the replies."""
|
|
|
|
_log.debug("%d new replies", len(results))
|
|
|
|
|
|
|
|
store = self._replies.get_model()
|
|
|
|
self._fill_store(store, results)
|
|
|
|
self._replies.queue_draw()
|
|
|
|
|
|
|
|
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)
|
|
|
|
self._update_statusbar('New messages retrieved. Next update in ' \
|
|
|
|
'%d minutes.' % (interval))
|
|
|
|
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)
|
|
|
|
_log.debug('Starting request of %s' % (url))
|
|
|
|
response = urllib2.urlopen(request)
|
|
|
|
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
|
|
|
|
|
|
|
|
self._grid.queue_draw()
|
|
|
|
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._update_sensitivity(False)
|
|
|
|
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_iter:
|
|
|
|
self._grid.get_model().remove(self._delete_iter)
|
|
|
|
self._delete_iter = 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
|
|
|
|
|
|
|
|
### Results for the repost message call
|
|
|
|
def _post_repost_message(self, widget, data):
|
|
|
|
"""Called when the message is reposted successfully."""
|
|
|
|
_log.debug('Repost successful')
|
|
|
|
self._update_statusbar('Message reposted')
|
|
|
|
return
|
|
|
|
|
|
|
|
def _exception_repost_message(self, widget, exception):
|
|
|
|
"""Called when the message cannot be reposted."""
|
|
|
|
_log.debug('Repost error.')
|
|
|
|
_log.debug(str(exception))
|
|
|
|
|
|
|
|
error_win = gtk.MessageDialog(parent=self._main_window,
|
|
|
|
type=gtk.MESSAGE_ERROR,
|
|
|
|
message_format='Error reposting message. Please ' \
|
|
|
|
'try again.',
|
|
|
|
buttons=gtk.BUTTONS_OK)
|
|
|
|
error_win.run()
|
|
|
|
error_win.hide()
|
|
|
|
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.')
|
|
|
|
message = self._grid.get_model().get_value(self._favourite_iter, 0)
|
|
|
|
if message.favourite:
|
|
|
|
display = 'Message unfavourited.'
|
|
|
|
else:
|
|
|
|
display = 'Message favourited.'
|
|
|
|
self._update_statusbar(display)
|
|
|
|
message.favourite = not message.favourite
|
|
|
|
self._favourite_iter = 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,
|
|
|
|
message_format='Error changing favourite 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['main'] = 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'))
|
|
|
|
self._images['avatar'] = default_pixmap
|
|
|
|
|
|
|
|
# This is the ugly bit for speeding up things and making
|
|
|
|
# interthread communication.
|
|
|
|
self._delete_iter = None
|
|
|
|
self._reply_message_id = None
|
|
|
|
self._favourite_iter = None
|
|
|
|
self._new_message_count = 0
|
|
|
|
self._new_replies_count = 0
|
|
|
|
|
|
|
|
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()
|
|
|
|
self._statusicon.set_from_pixbuf(self._images['main'])
|
|
|
|
else:
|
|
|
|
self._statusicon = None
|
|
|
|
|
|
|
|
self._main_window = self._create_main_window()
|
|
|
|
self._main_window.show_all()
|
|
|
|
|
|
|
|
self._threads = _ThreadManager()
|
|
|
|
|
|
|
|
# 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."""
|
|
|
|
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',
|
|
|
|
default=True,
|
|
|
|
conflict_group='interface')
|
|
|
|
# Most of the options 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='max_status_display',
|
|
|
|
help='Maximum number of elements to keep internally',
|
|
|
|
type='int',
|
|
|
|
metavar='MESSAGES',
|
|
|
|
default=60,
|
|
|
|
conflict_group='interface',
|
|
|
|
is_cmd_option=False) # TODO: Should it be config only?
|
|
|
|
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)
|