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.
1449 lines
51 KiB
1449 lines
51 KiB
16 years ago
|
#!/usr/bin/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 pygtk
|
||
|
pygtk.require('2.0')
|
||
|
import gtk
|
||
|
import gobject
|
||
|
|
||
|
gobject.threads_init()
|
||
|
gtk.gdk.threads_init()
|
||
|
|
||
|
import datetime
|
||
|
import re
|
||
|
import timesince
|
||
|
import logging
|
||
|
|
||
|
import mitterlib as util
|
||
|
|
||
|
from notify import Notify
|
||
|
from mitterlib.constants import gpl_3, version
|
||
|
from mitterlib.ui.utils import str_len
|
||
|
|
||
|
NAMESPACE = 'pygtk'
|
||
|
|
||
|
|
||
|
def options(options):
|
||
|
"""Add the options for this interface."""
|
||
|
options.add_group(NAMESPACE, 'GTK+ Interface')
|
||
|
options.add_option('--refresh-interval',
|
||
|
group=NAMESPACE,
|
||
|
option='refresh_interval',
|
||
|
help='Refresh interval',
|
||
|
type='int',
|
||
|
metavar='MINUTES',
|
||
|
default=5,
|
||
|
conflict_group='interface')
|
||
|
# Most of the options for non-cmd-options are useless, but I'm keeping
|
||
|
# them as documentation.
|
||
|
options.add_option(
|
||
|
group=NAMESPACE,
|
||
|
option='width',
|
||
|
help='Window width',
|
||
|
type='int',
|
||
|
metavar='PIXELS',
|
||
|
default=450,
|
||
|
conflict_group='interface',
|
||
|
is_cmd_option=False)
|
||
|
options.add_option(
|
||
|
group=NAMESPACE,
|
||
|
option='height',
|
||
|
help='Window height',
|
||
|
type='int',
|
||
|
metavar='PIXELS',
|
||
|
default=300,
|
||
|
conflict_group='interface',
|
||
|
is_cmd_option=False)
|
||
|
options.add_option(
|
||
|
group=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=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=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?
|
||
|
|
||
|
|
||
|
# Constants
|
||
|
|
||
|
URL_RE = re.compile(
|
||
|
r'((?:(?:https?|ftp)://|www[-\w]*\.)[^\s\n\r]+[-\w+&@#%=~])', re.I)
|
||
|
|
||
|
_log = logging.getLogger('ui.pygtk')
|
||
|
|
||
|
class Columns:
|
||
|
(PIC, NAME, MESSAGE, USERNAME, ID, DATETIME, ALL_DATA) = range(7)
|
||
|
|
||
|
|
||
|
class _MainWindow(gtk.Window):
|
||
|
"""PyGTK main window."""
|
||
|
|
||
|
def __init__(self, controller):
|
||
|
super(_MainWindow, self).__init__()
|
||
|
|
||
|
self._controller = controller
|
||
|
self.connect('destroy', self._controller.destroy)
|
||
|
self.connect('delete-event', self._controller.delete_event)
|
||
|
|
||
|
grid = self._create_grid(None) # Where is the store?
|
||
|
(menu, toolbar, accelerators) = self._create_menu_and_toolbar()
|
||
|
update_field = self._create_update_box()
|
||
|
statusbar = self._create_statusbar()
|
||
|
|
||
|
update_box = gtk.VPaned()
|
||
|
update_box.pack1(grid, resize=True, shrink=False)
|
||
|
update_box.pack2(update_field, resize=False, shrink=True)
|
||
|
|
||
|
box = gtk.VBox(False, 1)
|
||
|
box.pack_start(menu, False, True, 0)
|
||
|
box.pack_start(update_box, True, True, 0)
|
||
|
box.pack_start(statusbar, False, False, 0)
|
||
|
self.add(box)
|
||
|
self.add_accel_group(accelerators)
|
||
|
|
||
|
return
|
||
|
|
||
|
def _create_grid(self, grid_store):
|
||
|
"""Add the displaying grid."""
|
||
|
# self.grid_store = gtk.ListStore(
|
||
|
# str,
|
||
|
# str,
|
||
|
# str,
|
||
|
# str,
|
||
|
# str,
|
||
|
# object,
|
||
|
# object)
|
||
|
|
||
|
# self.grid_store.set_sort_func(Columns.DATETIME, self._sort_by_time)
|
||
|
# self.grid_store.set_sort_column_id(Columns.DATETIME,
|
||
|
# gtk.SORT_DESCENDING)
|
||
|
|
||
|
self.grid = gtk.TreeView(grid_store)
|
||
|
self.grid.set_property('headers-visible', False)
|
||
|
self.grid.set_rules_hint(True) # change color for each row
|
||
|
|
||
|
user_renderer = gtk.CellRendererPixbuf()
|
||
|
user_column = gtk.TreeViewColumn('User', user_renderer)
|
||
|
user_column.set_cell_data_func(user_renderer,
|
||
|
self._cell_renderer_user)
|
||
|
self.grid.append_column(user_column)
|
||
|
|
||
|
message_renderer = gtk.CellRendererText()
|
||
|
message_renderer.set_property('wrap-mode', gtk.WRAP_WORD)
|
||
|
message_renderer.set_property('wrap-width', 200)
|
||
|
message_renderer.set_property('width', 10)
|
||
|
|
||
|
message_column = gtk.TreeViewColumn('Message',
|
||
|
message_renderer, text=1)
|
||
|
message_column.set_cell_data_func(message_renderer,
|
||
|
self._cell_renderer_message)
|
||
|
self.grid.append_column(message_column)
|
||
|
|
||
|
self.grid.set_resize_mode(gtk.RESIZE_IMMEDIATE)
|
||
|
#self.grid.connect('cursor-changed', self.check_post)
|
||
|
#self.grid.connect('row-activated', self.open_post)
|
||
|
#self.grid.connect('button-press-event', self.click_post)
|
||
|
#self.grid.connect('popup-menu',
|
||
|
# lambda view: self.show_post_popup(view, None))
|
||
|
|
||
|
scrolled_window = gtk.ScrolledWindow()
|
||
|
scrolled_window.set_policy(gtk.POLICY_NEVER, gtk.POLICY_ALWAYS)
|
||
|
scrolled_window.add(self.grid)
|
||
|
|
||
|
return scrolled_window
|
||
|
|
||
|
def _create_menu_and_toolbar(self):
|
||
|
"""Create the main menu and the toolbar."""
|
||
|
|
||
|
# tasks (used by the menu and toolbar)
|
||
|
|
||
|
refresh_action = gtk.Action('Refresh', '_Refresh',
|
||
|
'Update the listing', gtk.STOCK_REFRESH)
|
||
|
refresh_action.connect('activate', self.refresh)
|
||
|
|
||
|
quit_action = gtk.Action('Quit', '_Quit',
|
||
|
'Exit Mitter', gtk.STOCK_QUIT)
|
||
|
quit_action.connect('activate', self.quit)
|
||
|
|
||
|
settings_action = gtk.Action('Settings', '_Settings',
|
||
|
'Settings', gtk.STOCK_PREFERENCES)
|
||
|
settings_action.connect('activate', self.show_settings)
|
||
|
|
||
|
update_action = gtk.Action('Update', '_Update', 'Update your status',
|
||
|
gtk.STOCK_ADD)
|
||
|
update_action.set_property('sensitive', False)
|
||
|
update_action.connect('activate', self._update_status)
|
||
|
|
||
|
delete_action = gtk.Action('Delete', '_Delete', 'Delete a post',
|
||
|
gtk.STOCK_DELETE)
|
||
|
delete_action.set_property('sensitive', False)
|
||
|
delete_action.connect('activate', self.delete_tweet)
|
||
|
|
||
|
about_action = gtk.Action('About', '_About', 'About Mitter',
|
||
|
gtk.STOCK_ABOUT)
|
||
|
about_action.connect('activate', self.show_about)
|
||
|
|
||
|
shrink_url_action = gtk.Action('ShrinkURL', 'Shrink _URL',
|
||
|
'Shrink selected URL', gtk.STOCK_EXECUTE)
|
||
|
shrink_url_action.connect('activate', self.shrink_url)
|
||
|
|
||
|
mute_action = gtk.ToggleAction('MuteNotify', '_Mute Notifications',
|
||
|
'Mutes notifications on new tweets', gtk.STOCK_MEDIA_PAUSE)
|
||
|
mute_action.set_active(False)
|
||
|
|
||
|
post_action = gtk.Action('Posts', '_Posts', 'Post management', None)
|
||
|
|
||
|
file_action = gtk.Action('File', '_File', 'File', None)
|
||
|
edit_action = gtk.Action('Edit', '_Edit', 'Edit', None)
|
||
|
help_action = gtk.Action('Help', '_Help', 'Help', None)
|
||
|
|
||
|
# action group (will have all the actions, 'cause we are not actually
|
||
|
# grouping them, but Gtk requires them that way)
|
||
|
|
||
|
self.action_group = gtk.ActionGroup('MainMenu')
|
||
|
self.action_group.add_action_with_accel(refresh_action, 'F5')
|
||
|
# None = use the default accelerator, based on the STOCK used.
|
||
|
self.action_group.add_action_with_accel(quit_action, None)
|
||
|
self.action_group.add_action(settings_action)
|
||
|
self.action_group.add_action(delete_action)
|
||
|
self.action_group.add_action(post_action)
|
||
|
self.action_group.add_action(file_action)
|
||
|
self.action_group.add_action(edit_action)
|
||
|
self.action_group.add_action(help_action)
|
||
|
self.action_group.add_action(about_action)
|
||
|
self.action_group.add_action_with_accel(shrink_url_action, '<Ctrl>u')
|
||
|
self.action_group.add_action_with_accel(mute_action, '<Ctrl>m')
|
||
|
self.action_group.add_action_with_accel(update_action,
|
||
|
'<Ctrl>Return')
|
||
|
|
||
|
# definition of the UI
|
||
|
|
||
|
uimanager = gtk.UIManager()
|
||
|
uimanager.insert_action_group(self.action_group, 0)
|
||
|
ui = '''
|
||
|
<ui>
|
||
|
<toolbar name="MainToolbar">
|
||
|
<toolitem action="Refresh" />
|
||
|
<separator />
|
||
|
<toolitem action="Delete" />
|
||
|
<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="Update" />
|
||
|
<menuitem action="Delete" />
|
||
|
<menuitem action="ShrinkURL" />
|
||
|
<menuitem action="MuteNotify" />
|
||
|
<separator />
|
||
|
<menuitem action="Settings" />
|
||
|
</menu>
|
||
|
<menu action="Help">
|
||
|
<menuitem action="About" />
|
||
|
</menu>
|
||
|
</menubar>
|
||
|
</ui>
|
||
|
'''
|
||
|
uimanager.add_ui_from_string(ui)
|
||
|
|
||
|
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()
|
||
|
text_buffer = self._update_text.get_buffer()
|
||
|
text_buffer.connect('changed', self._controller._count_chars)
|
||
|
|
||
|
update_button = gtk.Button(stock=gtk.STOCK_ADD)
|
||
|
update_button.connect('clicked', self._controller._update_status)
|
||
|
|
||
|
update_box = gtk.HBox(False, 0)
|
||
|
update_box.pack_start(self._update_text, expand=True, fill=True,
|
||
|
padding=0)
|
||
|
update_box.pack_start(update_button, expand=False, fill=False,
|
||
|
padding=0)
|
||
|
|
||
|
info_box = gtk.HBox(False, 0)
|
||
|
self._char_count = gtk.Label()
|
||
|
self._char_count.set_text('(140)')
|
||
|
info_box.pack_start(gtk.Label('What are you doing?'))
|
||
|
info_box.pack_start(self._char_count)
|
||
|
|
||
|
update_area = gtk.VBox(True, 0)
|
||
|
update_area.pack_start(info_box)
|
||
|
update_area.pack_start(update_box)
|
||
|
|
||
|
return update_area
|
||
|
|
||
|
def _create_statusbar(self):
|
||
|
"""Create the statusbar."""
|
||
|
statusbar = gtk.Statusbar()
|
||
|
# TODO: Probaly set the context in the object.
|
||
|
return statusbar
|
||
|
|
||
|
# ------------------------------------------------------------
|
||
|
# 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."""
|
||
|
|
||
|
pic = store.get_value(position, Columns.PIC)
|
||
|
if not pic in self._user_pics:
|
||
|
cell.set_property('pixbuf', self._default_pixmap)
|
||
|
|
||
|
# just make sure we download this pic too.
|
||
|
self.queue_pic(pic)
|
||
|
else:
|
||
|
cell.set_property('pixbuf', self._user_pics[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."""
|
||
|
|
||
|
user = store.get_value(position, Columns.NAME)
|
||
|
message = store.get_value(position, Columns.MESSAGE)
|
||
|
time = store.get_value(position, Columns.DATETIME)
|
||
|
username = store.get_value(position, Columns.USERNAME)
|
||
|
|
||
|
time = timesince.timesince(time)
|
||
|
|
||
|
# unescape escaped entities that pango is okay with
|
||
|
message = re.sub(r'&(?!(amp;|gt;|lt;|quot;|apos;))', r'&', message)
|
||
|
|
||
|
# highlight URLs
|
||
|
message = url_re.sub(r'<span foreground="blue">\1</span>',
|
||
|
message)
|
||
|
|
||
|
# use a different highlight for the current user
|
||
|
message = re.sub(r'(@'+self.twitter.username+')',
|
||
|
r'<span foreground="#FF6633">\1</span>',
|
||
|
message)
|
||
|
|
||
|
markup = '<b>%s</b> <small>(%s)</small>:\n%s\n<small>%s</small>' % \
|
||
|
(user, username, message, time)
|
||
|
cell.set_property('markup', markup)
|
||
|
|
||
|
return
|
||
|
|
||
|
class _GtkController(object):
|
||
|
"""The interface controller."""
|
||
|
|
||
|
def __init__(self):
|
||
|
super(_GtkController, self).__init__()
|
||
|
return
|
||
|
|
||
|
def destroy(self, widget, user_data=None):
|
||
|
"""Called when the window is destroyed."""
|
||
|
_log.debug('Window destroy')
|
||
|
gtk.main_quit()
|
||
|
return True
|
||
|
|
||
|
def delete_event(self, widget, event, user_param=None):
|
||
|
_log.debug('Window delete')
|
||
|
gtk.main_quit()
|
||
|
return True
|
||
|
|
||
|
|
||
|
class Interface(object):
|
||
|
"""Linux/GTK interface for Mitter."""
|
||
|
|
||
|
def systray_cb(self, widget, user_param=None):
|
||
|
if self.window.get_property('visible') and self.window.is_active():
|
||
|
x, y = self.window.get_position()
|
||
|
self.prefs['position_x'] = x
|
||
|
self.prefs['position_y'] = y
|
||
|
self.window.hide()
|
||
|
else:
|
||
|
self.window.move(
|
||
|
self.prefs['position_x'],
|
||
|
self.prefs['position_y'])
|
||
|
self.window.deiconify()
|
||
|
self.window.present()
|
||
|
|
||
|
def create_settings_dialog(self):
|
||
|
"""Creates the settings dialog."""
|
||
|
|
||
|
self.settings_window = gtk.Dialog(title="Settings",
|
||
|
parent=self.window, flags=gtk.DIALOG_MODAL |
|
||
|
gtk.DIALOG_DESTROY_WITH_PARENT,
|
||
|
buttons=(gtk.STOCK_CANCEL, 0, gtk.STOCK_OK, 1))
|
||
|
self.settings_box = gtk.Table(rows=4, columns=2, homogeneous=False)
|
||
|
|
||
|
username_label = gtk.Label('Username:')
|
||
|
password_label = gtk.Label('Password:')
|
||
|
refresh_label = gtk.Label('Refresh interval (minutes):')
|
||
|
https_label = gtk.Label('Use secure connections (HTTPS):')
|
||
|
|
||
|
labels = [username_label, password_label, refresh_label, https_label]
|
||
|
for label in labels:
|
||
|
label.set_alignment(0, 0.5)
|
||
|
label.set_padding(2, 0)
|
||
|
|
||
|
self.username_field = gtk.Entry()
|
||
|
self.password_field = gtk.Entry()
|
||
|
self.password_field.set_visibility(False)
|
||
|
|
||
|
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.prefs['refresh_interval'])
|
||
|
self.refresh_interval_field.set_increments(1, 5)
|
||
|
|
||
|
self.https_field = gtk.CheckButton()
|
||
|
self.https_field.set_active(self.https)
|
||
|
|
||
|
self.settings_box.attach(username_label, 0, 1, 0, 1)
|
||
|
self.settings_box.attach(self.username_field, 1, 2, 0, 1)
|
||
|
self.settings_box.attach(password_label, 0, 1, 1, 2)
|
||
|
self.settings_box.attach(self.password_field, 1, 2, 1, 2)
|
||
|
self.settings_box.attach(refresh_label, 0, 1, 2, 3)
|
||
|
self.settings_box.attach(self.refresh_interval_field, 1, 2, 2, 3)
|
||
|
self.settings_box.attach(https_label, 0, 1, 3, 4)
|
||
|
self.settings_box.attach(self.https_field, 1, 2, 3, 4)
|
||
|
|
||
|
self.settings_box.show_all()
|
||
|
self.settings_window.vbox.pack_start(self.settings_box, True,
|
||
|
True, 0)
|
||
|
self.settings_window.connect('close', self.close_dialog)
|
||
|
self.settings_window.connect('response', self.update_preferences)
|
||
|
|
||
|
return
|
||
|
|
||
|
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-2008 Mitter Contributors')
|
||
|
about_window.set_license(gpl_3)
|
||
|
about_window.set_website('http://mitter.googlecode.com')
|
||
|
about_window.set_website_label('Mitter on GoogleCode')
|
||
|
about_window.set_authors(['Julio Biason', 'Deepak Sarda', \
|
||
|
'Gerald Kaszuba'])
|
||
|
about_window.connect('close', self.close_dialog)
|
||
|
about_window.run()
|
||
|
about_window.hide()
|
||
|
|
||
|
|
||
|
# ------------------------------------------------------------
|
||
|
# Widget creation functions
|
||
|
# ------------------------------------------------------------
|
||
|
|
||
|
# ------------------------------------------------------------
|
||
|
# Grid cell content callback
|
||
|
# ------------------------------------------------------------
|
||
|
|
||
|
# Non-widget attached callbacks
|
||
|
def set_auto_refresh(self):
|
||
|
"""Configure auto-refresh of tweets every `interval` minutes"""
|
||
|
|
||
|
if self._refresh_id:
|
||
|
gobject.source_remove(self._refresh_id)
|
||
|
|
||
|
self._refresh_id = gobject.timeout_add(
|
||
|
self.prefs['refresh_interval']*60*1000,
|
||
|
self.refresh, None)
|
||
|
|
||
|
return
|
||
|
|
||
|
def update_friends_list(self):
|
||
|
"""Fetch the user's list of twitter friends and add it
|
||
|
to the friends_store for @reply autocompletion"""
|
||
|
|
||
|
_log.debug('Checking friends list...')
|
||
|
friends = self.twitter.friends_list(self.post_update_friends_list)
|
||
|
return
|
||
|
|
||
|
def post_update_friends_list(self, friends, error):
|
||
|
"""Function called after we fetch the friends list."""
|
||
|
|
||
|
_log.debug('Received the friends list')
|
||
|
|
||
|
if error == 401: # TODO: Constants for this?
|
||
|
# not authorized
|
||
|
_log.error('User is not authorized yet')
|
||
|
return
|
||
|
|
||
|
if error in (500, 502, 503):
|
||
|
_log.error('Twitter asked us to try getting friends list' \
|
||
|
' sometime later')
|
||
|
gobject.timeout_add(5*60*1000, self.update_friends_list)
|
||
|
return
|
||
|
|
||
|
if error:
|
||
|
# any error
|
||
|
# well, we just don't add any friends, then.
|
||
|
_log.error('Error getting friend list, leaving list empty')
|
||
|
return
|
||
|
|
||
|
# I'm not really sure if we need to set the thread locking here (as we
|
||
|
# are just updating the store), but better safe than sorry!
|
||
|
|
||
|
gtk.gdk.threads_enter()
|
||
|
# Sometimes due to twitter API quirks and moon phases, the friends
|
||
|
# list ends up getting populated more than once. So watch for
|
||
|
# duplicates....
|
||
|
known_friends = [row[0] for row in self.friends_store]
|
||
|
_log.debug('known_friends: %s' % " ".join(known_friends))
|
||
|
for friend in friends:
|
||
|
try:
|
||
|
screen_name = '@' + friend['screen_name'] + ': '
|
||
|
if screen_name not in known_friends:
|
||
|
_log.debug('Adding "%s" to the list' % (screen_name))
|
||
|
self.friends_store.append([screen_name])
|
||
|
except Exception, e:
|
||
|
# No `error` does not always mean twitter sent us good data
|
||
|
_log.error('Error processing friend list. %s' % str(e))
|
||
|
|
||
|
gtk.gdk.threads_leave()
|
||
|
_log.debug('friends list processing complete')
|
||
|
return
|
||
|
|
||
|
def prune_grid_store(self):
|
||
|
"""Prune the grid_store by removing the oldest rows."""
|
||
|
|
||
|
if len(self.grid_store) <= MAX_STATUS_DISPLAY:
|
||
|
return True # Required by gobject.idle_add() for this to be called
|
||
|
# again
|
||
|
|
||
|
_log.debug("prune_grid_store called")
|
||
|
|
||
|
gtk.gdk.threads_enter()
|
||
|
|
||
|
self.grid.freeze_child_notify()
|
||
|
self.grid.set_model(None)
|
||
|
|
||
|
# Since I don't know how to get the last row in grid_store,
|
||
|
# I'll reverse the list and then pop out the first row instead.
|
||
|
|
||
|
self.grid_store.set_sort_column_id(Columns.DATETIME,
|
||
|
gtk.SORT_ASCENDING)
|
||
|
|
||
|
iter = self.grid_store.get_iter_first()
|
||
|
|
||
|
while (len(self.grid_store) > MAX_STATUS_DISPLAY) and iter:
|
||
|
_log.debug("popping off tweet with id %s" %
|
||
|
self.grid_store.get_value(iter, Columns.ID))
|
||
|
self.grid_store.remove(iter) # iter is auto set to next row
|
||
|
|
||
|
|
||
|
self.grid_store.set_sort_column_id(Columns.DATETIME,
|
||
|
gtk.SORT_DESCENDING)
|
||
|
self.grid.set_model(self.grid_store)
|
||
|
self.grid.thaw_child_notify()
|
||
|
|
||
|
gtk.gdk.threads_leave()
|
||
|
return True
|
||
|
|
||
|
# Main window callbacks
|
||
|
def size_request(self, widget, requisition, data=None):
|
||
|
"""Callback when the window changes its sizes. We use it to set the
|
||
|
proper word-wrapping for the message column."""
|
||
|
|
||
|
self.prefs['width'], self.prefs['height'] = self.window.get_size()
|
||
|
|
||
|
# this is based on a mail of Kristian Rietveld, on gtk maillist
|
||
|
|
||
|
if not len(self.grid_store):
|
||
|
# nothing to rearrange
|
||
|
return
|
||
|
|
||
|
column = self.message_column
|
||
|
iter = self.grid_store.get_iter_first()
|
||
|
path = self.grid_store.get_path(iter)
|
||
|
|
||
|
column_rectangle = self.grid.get_cell_area(path, column)
|
||
|
|
||
|
width = column_rectangle.width
|
||
|
_log.debug('Width=%d' % (width))
|
||
|
|
||
|
# there should be only
|
||
|
renderers = column.get_cell_renderers()
|
||
|
for render in renderers:
|
||
|
_log.debug('Render update')
|
||
|
render.set_property('wrap-width', width)
|
||
|
|
||
|
while iter:
|
||
|
path = self.grid_store.get_path(iter)
|
||
|
self.grid_store.row_changed(path, iter)
|
||
|
iter = self.grid_store.iter_next(iter)
|
||
|
|
||
|
return
|
||
|
|
||
|
def quit(self, widget, user_data=None):
|
||
|
"""Callback when the window is destroyed (e.g. when the user closes
|
||
|
the application."""
|
||
|
|
||
|
# this is really annoying: if the threads are locked doing some IO
|
||
|
# requests, the application will not quit. Displaying this message is
|
||
|
# the only option we have right now.
|
||
|
|
||
|
_log.debug('quit callback invoked. exiting now...')
|
||
|
gtk.main_quit()
|
||
|
|
||
|
def notify_reset(self, widget, event, user_data=None):
|
||
|
if getattr(event, 'in_', False):
|
||
|
self._main_window.set_urgency_hint(False)
|
||
|
if self._systray:
|
||
|
self._systray.set_tooltip('Mitter: Click to toggle ' \
|
||
|
'window visibility.')
|
||
|
self._systray.set_from_file(self._app_icon)
|
||
|
self.unread_tweets = 0
|
||
|
return
|
||
|
|
||
|
def notify(self, new_tweets=0):
|
||
|
"""Set the window hint as urgent, so Mitter window will flash,
|
||
|
notifying the user about the new messages. Also send a notification
|
||
|
message with one of the new tweets."""
|
||
|
self.window.set_urgency_hint(True)
|
||
|
if self._systray and self.unread_tweets > 0:
|
||
|
self._systray.set_tooltip('Mitter: %s new' % self.unread_tweets)
|
||
|
self._systray.set_from_file(self._app_icon_alert)
|
||
|
|
||
|
if self.action_group.get_action('MuteNotify').get_active():
|
||
|
_log.debug('notifications are currently muted')
|
||
|
return
|
||
|
|
||
|
if new_tweets and len(self.grid_store) > 0:
|
||
|
iter = self.grid_store.get_iter_first()
|
||
|
while iter:
|
||
|
sender = self.grid_store.get_value(iter, Columns.USERNAME)
|
||
|
if sender == self.username_field.get_text():
|
||
|
iter = self.grid_store.iter_next(iter)
|
||
|
continue
|
||
|
else:
|
||
|
tweet = self.grid_store.get_value(iter, Columns.MESSAGE)
|
||
|
_log.debug('notify_broadcast with this tweet: %s' %
|
||
|
tweet)
|
||
|
break
|
||
|
|
||
|
if new_tweets > 1:
|
||
|
msg = '<b>%d</b> unread tweets including ' \
|
||
|
'this from <i>%s</i>:<br/>%s' % (self.unread_tweets,
|
||
|
sender, tweet)
|
||
|
else:
|
||
|
msg = 'One new tweet from <i>%s</i>:<br/>%s' % (sender,
|
||
|
tweet)
|
||
|
|
||
|
if self.systray:
|
||
|
gtk.gdk.threads_enter()
|
||
|
screen, rect, orientation = self.systray.get_geometry()
|
||
|
gtk.gdk.threads_leave()
|
||
|
self.notify_broadcast(msg, rect.x, rect.y)
|
||
|
return
|
||
|
|
||
|
# settings callbacks
|
||
|
def show_settings(self, widget, user_data=None):
|
||
|
"""Create and display the settings window."""
|
||
|
|
||
|
self.settings_window.show()
|
||
|
self.settings_window.run()
|
||
|
|
||
|
return
|
||
|
|
||
|
def close_dialog(self, user_data=None):
|
||
|
"""Hide the dialog window."""
|
||
|
|
||
|
return True
|
||
|
|
||
|
def update_preferences(self, widget, response_id=0, user_data=None):
|
||
|
"""
|
||
|
Update the user preferences when the user press the "OK" button in the
|
||
|
settings window."""
|
||
|
|
||
|
if response_id == 1:
|
||
|
self.statusbar.push(self.statusbar_context,
|
||
|
'Saving your profile...')
|
||
|
|
||
|
self.save_interface_prefs()
|
||
|
|
||
|
# update the (internal) twitter prefences too!
|
||
|
|
||
|
self.twitter.username = self.username_field.get_text()
|
||
|
self.twitter.password = self.password_field.get_text()
|
||
|
self.twitter.https = self.https_field.get_active()
|
||
|
refresh_interval = self.refresh_interval_field.get_value_as_int()
|
||
|
self.prefs['refresh_interval'] = refresh_interval
|
||
|
|
||
|
# update the list
|
||
|
|
||
|
self.refresh(None)
|
||
|
self.update_friends_list()
|
||
|
self.statusbar.pop(self.statusbar_context)
|
||
|
|
||
|
# update auto-refresh
|
||
|
|
||
|
self.set_auto_refresh()
|
||
|
|
||
|
self.settings_window.hide()
|
||
|
|
||
|
return True
|
||
|
|
||
|
# update status
|
||
|
def update_status(self, user_data=None):
|
||
|
"""Update the user status on Twitter."""
|
||
|
|
||
|
status = self.update_text.get_text()
|
||
|
status = status.strip()
|
||
|
if not str_len(status):
|
||
|
return
|
||
|
|
||
|
self.update_text.set_sensitive(False)
|
||
|
self.statusbar.push(self.statusbar_context, 'Updating your status...')
|
||
|
|
||
|
if str_len(status) > 140:
|
||
|
error_message = 'Your message has more than 140 characters and' \
|
||
|
' Twitter may truncate it. It would still be visible ' \
|
||
|
'on the website. Do you still wish to go ahead?'
|
||
|
if str_len(status) > 160:
|
||
|
error_message = 'Your message has more than 160 characters ' \
|
||
|
'and it is very likely Twitter will refuse it. You ' \
|
||
|
'can try shortening your URLs before posting. Do ' \
|
||
|
'you still wish to go ahead?'
|
||
|
|
||
|
error_dialog = gtk.MessageDialog(parent=self.window,
|
||
|
type=gtk.MESSAGE_QUESTION,
|
||
|
flags=gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
|
||
|
message_format="Your status update message is too long.",
|
||
|
buttons=gtk.BUTTONS_YES_NO)
|
||
|
error_dialog.format_secondary_text(error_message)
|
||
|
|
||
|
response = error_dialog.run()
|
||
|
error_dialog.destroy()
|
||
|
if response == gtk.RESPONSE_NO:
|
||
|
self.statusbar.pop(self.statusbar_context)
|
||
|
self.update_text.set_sensitive(True)
|
||
|
self.window.set_focus(self.update_text)
|
||
|
return
|
||
|
|
||
|
data = self.twitter.update(status, self.post_update_status)
|
||
|
|
||
|
def post_update_status(self, data, error):
|
||
|
"""Function called after we receive the answer from the update
|
||
|
status."""
|
||
|
|
||
|
if error:
|
||
|
gtk.gdk.threads_enter()
|
||
|
error_dialog = gtk.MessageDialog(parent=self.window,
|
||
|
type=gtk.MESSAGE_ERROR,
|
||
|
message_format='Error updating status. Please try again.',
|
||
|
buttons=gtk.BUTTONS_OK)
|
||
|
error_dialog.connect("response", lambda *a:
|
||
|
error_dialog.destroy())
|
||
|
error_dialog.run()
|
||
|
gtk.gdk.threads_leave()
|
||
|
else:
|
||
|
if data:
|
||
|
# i wonder if this will really work
|
||
|
self.post_refresh([data], None, False)
|
||
|
else:
|
||
|
self.refresh(None, False)
|
||
|
|
||
|
gtk.gdk.threads_enter()
|
||
|
self.update_text.set_text("")
|
||
|
gtk.gdk.threads_leave()
|
||
|
|
||
|
gtk.gdk.threads_enter()
|
||
|
self.statusbar.pop(self.statusbar_context)
|
||
|
self.update_text.set_sensitive(True)
|
||
|
self.window.set_focus(self.update_text)
|
||
|
gtk.gdk.threads_leave()
|
||
|
|
||
|
return True
|
||
|
|
||
|
def shrink_url(self, widget, user_data=None):
|
||
|
bounds = self.update_text.get_selection_bounds()
|
||
|
if not bounds:
|
||
|
return
|
||
|
else:
|
||
|
start, end = bounds
|
||
|
|
||
|
longurl = self.update_text.get_chars(start, end).strip()
|
||
|
if not longurl:
|
||
|
return
|
||
|
|
||
|
_log.debug('shrink url request for: %s' % longurl)
|
||
|
|
||
|
self.update_text.set_sensitive(False)
|
||
|
self.statusbar.push(self.statusbar_context, 'Shrinking URL...')
|
||
|
|
||
|
self.twitter.download('http://is.gd/api.php?longurl=' + longurl,
|
||
|
self.post_shrink_url, longurl=longurl,
|
||
|
start=start, end=end)
|
||
|
|
||
|
def post_shrink_url(self, url, error, longurl, start, end):
|
||
|
if error:
|
||
|
_log.error("Exception in shrinking url. ' \
|
||
|
'Error code: %s" % error)
|
||
|
# error dialog
|
||
|
gtk.gdk.threads_enter()
|
||
|
error_dialog = gtk.MessageDialog(parent=self.window,
|
||
|
type=gtk.MESSAGE_ERROR,
|
||
|
message_format='Failed to shrink the URL %s' % longurl,
|
||
|
buttons=gtk.BUTTONS_OK)
|
||
|
error_dialog.connect("response", lambda *a:
|
||
|
error_dialog.destroy())
|
||
|
error_dialog.run()
|
||
|
gtk.gdk.threads_leave()
|
||
|
else:
|
||
|
_log.debug('Got shrunk url: %s' % url)
|
||
|
char = self.update_text.get_chars(start-1, start)
|
||
|
if start and not char.isspace():
|
||
|
url = ' '+url
|
||
|
char = self.update_text.get_chars(end, end+1)
|
||
|
if not char.isspace():
|
||
|
url = url+' '
|
||
|
|
||
|
gtk.gdk.threads_enter()
|
||
|
self.update_text.delete_text(start, end)
|
||
|
self.update_text.insert_text(url, start)
|
||
|
self.update_text.set_position(start+len(url))
|
||
|
gtk.gdk.threads_leave()
|
||
|
|
||
|
gtk.gdk.threads_enter()
|
||
|
self.statusbar.pop(self.statusbar_context)
|
||
|
self.update_text.set_sensitive(True)
|
||
|
self.update_text.grab_focus()
|
||
|
gtk.gdk.threads_leave()
|
||
|
|
||
|
# post related callbacks
|
||
|
def reply_tweet(self, widget, user_data=None):
|
||
|
"""Reply by putting the username in your input"""
|
||
|
cursor = self.grid.get_cursor()
|
||
|
if not cursor:
|
||
|
return
|
||
|
|
||
|
path = cursor[0]
|
||
|
iter = self.grid_store.get_iter(path)
|
||
|
username = self.grid_store.get_value(iter, Columns.USERNAME)
|
||
|
text_insert = '@%s: ' % (username)
|
||
|
|
||
|
_log.debug('Inserting reply text: %s' % (text_insert))
|
||
|
|
||
|
status = self.update_text.get_text()
|
||
|
status = text_insert + status
|
||
|
self.update_text.set_text(status)
|
||
|
self.window.set_focus(self.update_text)
|
||
|
self.update_text.set_position(len(status))
|
||
|
|
||
|
def retweet(self, widget, user_data=None):
|
||
|
"""Retweet by putting the string rt and username in your input"""
|
||
|
|
||
|
cursor = self.grid.get_cursor()
|
||
|
if not cursor:
|
||
|
return
|
||
|
|
||
|
path = cursor[0]
|
||
|
iter = self.grid_store.get_iter(path)
|
||
|
username = self.grid_store.get_value(iter, Columns.USERNAME)
|
||
|
msg = self.grid_store.get_value(iter, Columns.MESSAGE)
|
||
|
text_insert = 'RT @%s: %s' % (username, msg)
|
||
|
|
||
|
_log.debug('Inserting retweet text: %s' % (text_insert))
|
||
|
|
||
|
status = text_insert + self.update_text.get_text()
|
||
|
self.update_text.set_text(status)
|
||
|
self.window.set_focus(self.update_text)
|
||
|
self.update_text.set_position(str_len(status))
|
||
|
|
||
|
def delete_tweet(self, widget, user_data=None):
|
||
|
"""Delete a twit."""
|
||
|
|
||
|
cursor = self.grid.get_cursor()
|
||
|
if not cursor:
|
||
|
return
|
||
|
|
||
|
path = cursor[0]
|
||
|
iter = self.grid_store.get_iter(path)
|
||
|
tweet_id = int(self.grid_store.get_value(iter, Columns.ID))
|
||
|
_log.debug('Deleting tweet: %d' % (tweet_id))
|
||
|
|
||
|
self.statusbar.push(self.statusbar_context, 'Deleting tweet...')
|
||
|
|
||
|
self.twitter.tweet_destroy(tweet_id, self.post_delete_tweet,
|
||
|
tweet=tweet_id)
|
||
|
|
||
|
return
|
||
|
|
||
|
def post_delete_tweet(self, data, error, tweet):
|
||
|
"""Function called after we delete a tweet on the server."""
|
||
|
|
||
|
if error:
|
||
|
gtk.gdk.threads_enter()
|
||
|
error_dialog = gtk.MessageDialog(parent=self.window,
|
||
|
type=gtk.MESSAGE_ERROR,
|
||
|
message_format='Error deleting tweet. Please try again.',
|
||
|
buttons=gtk.BUTTONS_OK)
|
||
|
error_dialog.connect("response", lambda *a:
|
||
|
error_dialog.destroy())
|
||
|
error_dialog.run()
|
||
|
gtk.gdk.threads_leave()
|
||
|
else:
|
||
|
# locate that tweet in the store and remove it.
|
||
|
iter = self.grid_store.get_iter_first()
|
||
|
tweet = int(tweet)
|
||
|
while iter:
|
||
|
id = self.grid_store.get_value(iter, Columns.ID)
|
||
|
if int(id) == tweet:
|
||
|
self.grid_store.remove(iter)
|
||
|
break
|
||
|
iter = self.grid_store.iter_next(iter)
|
||
|
|
||
|
# update the interface
|
||
|
gtk.gdk.threads_enter()
|
||
|
self.statusbar.pop(self.statusbar_context)
|
||
|
self.grid.queue_draw()
|
||
|
gtk.gdk.threads_leave()
|
||
|
|
||
|
return
|
||
|
|
||
|
def check_post(self, treeview, user_data=None):
|
||
|
"""Callback when one of the rows is selected."""
|
||
|
cursor = treeview.get_cursor()
|
||
|
if not cursor:
|
||
|
return
|
||
|
|
||
|
path = cursor[0]
|
||
|
iter = self.grid_store.get_iter(path)
|
||
|
username = self.grid_store.get_value(iter, Columns.USERNAME)
|
||
|
|
||
|
delete_action = self.action_group.get_action('Delete')
|
||
|
|
||
|
if username == self.username_field.get_text():
|
||
|
delete_action.set_property('sensitive', True)
|
||
|
else:
|
||
|
delete_action.set_property('sensitive', False)
|
||
|
|
||
|
return
|
||
|
|
||
|
def open_post(self, treeview, path, view_column, user_data=None):
|
||
|
"""Callback when one of the rows in activated."""
|
||
|
|
||
|
iter = self.grid_store.get_iter(path)
|
||
|
username = self.grid_store.get_value(iter, Columns.USERNAME)
|
||
|
tweet_id = self.grid_store.get_value(iter, Columns.ID)
|
||
|
message = self.grid_store.get_value(iter, Columns.MESSAGE)
|
||
|
urls = url_re.search(message)
|
||
|
if urls:
|
||
|
# message contains a link; go to the link instead
|
||
|
url = urls.groups()[0]
|
||
|
else:
|
||
|
url = 'http://twitter.com/%s/statuses/%s/' % (username, tweet_id)
|
||
|
|
||
|
self.open_url(path, url)
|
||
|
|
||
|
def click_post(self, treeview, event, user_data=None):
|
||
|
"""Callback when a mouse click event occurs on one of the rows."""
|
||
|
|
||
|
if event.button != 3:
|
||
|
# Only right clicks are processed
|
||
|
return False
|
||
|
|
||
|
x = int(event.x)
|
||
|
y = int(event.y)
|
||
|
|
||
|
pth = treeview.get_path_at_pos(x, y)
|
||
|
if not pth:
|
||
|
# The click wasn't on a row
|
||
|
return False
|
||
|
|
||
|
path, col, cell_x, cell_y = pth
|
||
|
treeview.grab_focus()
|
||
|
treeview.set_cursor(path, col, 0)
|
||
|
|
||
|
self.show_post_popup(treeview, event)
|
||
|
return True
|
||
|
|
||
|
def show_post_popup(self, treeview, event, user_data=None):
|
||
|
"""Shows the popup context menu in the treeview"""
|
||
|
|
||
|
cursor = treeview.get_cursor()
|
||
|
if not cursor:
|
||
|
return
|
||
|
|
||
|
path = cursor[0]
|
||
|
row_iter = self.grid_store.get_iter(path)
|
||
|
|
||
|
popup_menu = gtk.Menu()
|
||
|
popup_menu.set_screen(self.window.get_screen())
|
||
|
|
||
|
# An open submenu with various choices underneath
|
||
|
open_menu_items = []
|
||
|
|
||
|
tweet = self.grid_store.get_value(row_iter, Columns.ALL_DATA)
|
||
|
|
||
|
urls = url_re.findall(tweet['text'])
|
||
|
for url in urls:
|
||
|
if len(url) > 20:
|
||
|
item_name = url[:20] + '...'
|
||
|
else:
|
||
|
item_name = url
|
||
|
item = gtk.MenuItem(item_name)
|
||
|
item.connect('activate', self.open_url, url)
|
||
|
open_menu_items.append(item)
|
||
|
|
||
|
if tweet['in_reply_to_status_id']:
|
||
|
# I wish twitter made it easy to construct target url
|
||
|
# without having to make another API call
|
||
|
reply_to = re.search(r'@(?P<user>\w+)', tweet['text'])
|
||
|
if reply_to:
|
||
|
url = 'http://twitter.com/%s/statuses/%s' % (
|
||
|
reply_to.group('user'),
|
||
|
tweet['in_reply_to_status_id'])
|
||
|
item = gtk.MenuItem('In reply to')
|
||
|
item.connect('activate', self.open_url, url)
|
||
|
open_menu_items.append(item)
|
||
|
|
||
|
item = gtk.MenuItem('This tweet')
|
||
|
username = self.grid_store.get_value(row_iter, Columns.USERNAME)
|
||
|
tweet_id = self.grid_store.get_value(row_iter, Columns.ID)
|
||
|
url = 'http://twitter.com/%s/statuses/%s/' % (username, tweet_id)
|
||
|
item.connect('activate', self.open_url, url)
|
||
|
open_menu_items.append(item)
|
||
|
|
||
|
open_menu = gtk.Menu()
|
||
|
for item in open_menu_items:
|
||
|
open_menu.append(item)
|
||
|
|
||
|
open_item = gtk.MenuItem("Open")
|
||
|
open_item.set_submenu(open_menu)
|
||
|
popup_menu.append(open_item)
|
||
|
|
||
|
# Reply, only if it's not yourself
|
||
|
item = gtk.MenuItem("Reply")
|
||
|
item.connect('activate', self.reply_tweet, "Reply")
|
||
|
if username == self.username_field.get_text():
|
||
|
item.set_property('sensitive', False)
|
||
|
popup_menu.append(item)
|
||
|
|
||
|
# Retweet, only if it's not yourself
|
||
|
item = gtk.MenuItem("Retweet")
|
||
|
item.connect('activate', self.retweet, "Retweet")
|
||
|
if username == self.username_field.get_text():
|
||
|
item.set_property('sensitive', False)
|
||
|
popup_menu.append(item)
|
||
|
|
||
|
item = gtk.MenuItem("Delete")
|
||
|
item.connect('activate', self.delete_tweet, "Delete")
|
||
|
if username != self.username_field.get_text():
|
||
|
item.set_property('sensitive', False)
|
||
|
|
||
|
popup_menu.append(item)
|
||
|
|
||
|
popup_menu.show_all()
|
||
|
|
||
|
if event:
|
||
|
b = event.button
|
||
|
t = event.time
|
||
|
else:
|
||
|
b = 1
|
||
|
t = 0
|
||
|
|
||
|
popup_menu.popup(None, None, None, b, t)
|
||
|
|
||
|
return True
|
||
|
|
||
|
# action callbacks
|
||
|
# (yes, settings should be here, but there are more settings-related
|
||
|
# callbacks, so let's keep them together somewhere else)
|
||
|
def open_url(self, source, url):
|
||
|
"""Simply opens specified url in new browser tab. We need source
|
||
|
parameter so that this function can be used as an event callback"""
|
||
|
|
||
|
_log.debug('opening url: %s' % url)
|
||
|
import webbrowser
|
||
|
webbrowser.open_new_tab(url)
|
||
|
self.window.set_focus(self.update_text)
|
||
|
|
||
|
def refresh(self, widget, notify=True):
|
||
|
"""Update the list of twits."""
|
||
|
|
||
|
if self.last_update:
|
||
|
self.statusbar.pop(self.statusbar_context)
|
||
|
self.last_update = datetime.datetime.now()
|
||
|
|
||
|
_log.debug('Updating list of tweets...')
|
||
|
self.statusbar.push(self.statusbar_context,
|
||
|
'Updating list of tweets...')
|
||
|
|
||
|
self.twitter.friends_timeline(self.post_refresh, notify=notify)
|
||
|
|
||
|
return True # required by gobject.timeout_add
|
||
|
|
||
|
def post_refresh(self, data, error, notify):
|
||
|
"""Function called when the system retrieves the list of new
|
||
|
tweets."""
|
||
|
|
||
|
_log.debug('Data: %s' % (str(data)))
|
||
|
|
||
|
if error == 401:
|
||
|
# Not authorized, popup the settings window
|
||
|
gtk.gdk.threads_enter()
|
||
|
error_dialog = gtk.MessageDialog(parent=self.window,
|
||
|
type=gtk.MESSAGE_ERROR,
|
||
|
message_format='Autorization error, check your login ' \
|
||
|
'information in the prefrences',
|
||
|
buttons=gtk.BUTTONS_OK)
|
||
|
error_dialog.connect("response", lambda *a:
|
||
|
error_dialog.destroy())
|
||
|
error_dialog.run()
|
||
|
gtk.gdk.threads_leave()
|
||
|
return
|
||
|
|
||
|
if not data:
|
||
|
gtk.gdk.threads_enter()
|
||
|
self.statusbar.pop(self.statusbar_context)
|
||
|
self.show_last_update()
|
||
|
_log.debug('No new data')
|
||
|
gtk.gdk.threads_leave()
|
||
|
return
|
||
|
|
||
|
known_tweets = [row[Columns.ID] for row in self.grid_store]
|
||
|
need_notify = False
|
||
|
new_tweets = 0
|
||
|
new_tweets_list = []
|
||
|
|
||
|
for tweet in data:
|
||
|
id = tweet['id']
|
||
|
if str(id) in known_tweets:
|
||
|
_log.debug('Tweet %s is already in the list' % (id))
|
||
|
continue
|
||
|
|
||
|
created_at = tweet['created_at']
|
||
|
display_name = tweet['user']['name']
|
||
|
username = tweet['user']['screen_name']
|
||
|
user_pic = tweet['user']['profile_image_url']
|
||
|
message = tweet['text']
|
||
|
|
||
|
new_tweets_list.append((user_pic, display_name, message, username,
|
||
|
id, created_at, tweet))
|
||
|
self.queue_pic(user_pic)
|
||
|
|
||
|
_log.debug('New tweet with id %s from %s' % (id, username))
|
||
|
if not username == self.username_field.get_text():
|
||
|
# we don't want to be notified about tweets from ourselves,
|
||
|
# but from everyone else it is fine.
|
||
|
new_tweets += 1
|
||
|
|
||
|
# add the new tweets in the store
|
||
|
gtk.gdk.threads_enter()
|
||
|
for data in new_tweets_list:
|
||
|
self.grid_store.append(data)
|
||
|
|
||
|
self.statusbar.pop(self.statusbar_context)
|
||
|
|
||
|
# there is new stuff, so we move to the top
|
||
|
|
||
|
p = self.grid_store.get_path(self.grid_store.get_iter_first())
|
||
|
self.grid.scroll_to_cell(p)
|
||
|
self.show_last_update()
|
||
|
_log.debug('Tweets updated')
|
||
|
gtk.gdk.threads_leave()
|
||
|
|
||
|
if new_tweets and notify:
|
||
|
self.unread_tweets += new_tweets
|
||
|
self.notify(new_tweets)
|
||
|
|
||
|
self.refresh_rate_limit()
|
||
|
self.prune_grid_store()
|
||
|
|
||
|
# ------------------------------------------------------------
|
||
|
# Helper functions
|
||
|
# ------------------------------------------------------------
|
||
|
def clear_list(self):
|
||
|
"""Clear the list, so we can add more items."""
|
||
|
|
||
|
self.grid_store.clear()
|
||
|
|
||
|
return
|
||
|
|
||
|
def save_interface_prefs(self):
|
||
|
"""Using the save callback, save all this interface preferences."""
|
||
|
|
||
|
self.prefs['refresh_interval'] = \
|
||
|
self.refresh_interval_field.get_value_as_int()
|
||
|
|
||
|
x, y = self.window.get_position()
|
||
|
self.prefs['position_x'] = x
|
||
|
self.prefs['position_y'] = y
|
||
|
|
||
|
self.save_callback(self.username_field.get_text(),
|
||
|
self.password_field.get_text(),
|
||
|
self.https_field.get_active(),
|
||
|
NAMESPACE, self.prefs)
|
||
|
|
||
|
return
|
||
|
|
||
|
def refresh_rate_limit(self):
|
||
|
"""Request the rate limit and check if we are doing okay."""
|
||
|
self.twitter.rate_limit_status(self.post_refresh_rate_limit)
|
||
|
return
|
||
|
|
||
|
def post_refresh_rate_limit(self, data, error):
|
||
|
"""Callback for the refresh_rate_limit."""
|
||
|
if error or not data:
|
||
|
_log.error('Error fetching rate limit')
|
||
|
return
|
||
|
|
||
|
# Check if we are running low on our limit
|
||
|
reset_time = datetime.datetime.fromtimestamp(
|
||
|
int(data['reset_time_in_seconds']))
|
||
|
|
||
|
if reset_time < datetime.datetime.now():
|
||
|
# Clock differences can cause this
|
||
|
return
|
||
|
|
||
|
time_delta = reset_time - datetime.datetime.now()
|
||
|
mins_till_reset = time_delta.seconds/60 # Good enough!
|
||
|
needed_hits = mins_till_reset/self.prefs['refresh_interval']
|
||
|
remaining_hits = int(data['remaining_hits'])
|
||
|
|
||
|
_log.debug('remaining_hits: %s. reset in %s mins.'
|
||
|
% (remaining_hits, mins_till_reset))
|
||
|
|
||
|
if needed_hits > remaining_hits:
|
||
|
gtk.gdk.threads_enter()
|
||
|
error_dialog = gtk.MessageDialog(parent=self.window,
|
||
|
type=gtk.MESSAGE_WARNING,
|
||
|
flags=gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
|
||
|
message_format='Refresh rate too high',
|
||
|
buttons=gtk.BUTTONS_OK)
|
||
|
error_dialog.format_secondary_text(
|
||
|
"You have only %d twitter requests left until your " \
|
||
|
"request count is reset in %d minutes. But at your " \
|
||
|
"current refresh rate (every %d minutes), you will " \
|
||
|
"exhaust your limit within %d minutes. You should " \
|
||
|
"consider increasing the refresh interval in Mitter's " \
|
||
|
"Settings dialog." % (remaining_hits, mins_till_reset,
|
||
|
self.prefs['refresh_interval'],
|
||
|
remaining_hits * self.prefs['refresh_interval']))
|
||
|
error_dialog.connect("response", lambda *a:
|
||
|
error_dialog.destroy())
|
||
|
error_dialog.run()
|
||
|
gtk.gdk.threads_leave()
|
||
|
|
||
|
def show_last_update(self):
|
||
|
"""Add the last update time in the status bar."""
|
||
|
|
||
|
last_update = self.last_update.strftime('%H:%M')
|
||
|
next_update = (self.last_update +
|
||
|
datetime.timedelta(minutes=self.prefs[
|
||
|
'refresh_interval'])).strftime('%H:%M')
|
||
|
|
||
|
message = 'Last update %s, next update %s' % (last_update,
|
||
|
next_update)
|
||
|
self.statusbar.push(self.statusbar_context, message)
|
||
|
return
|
||
|
|
||
|
def queue_pic(self, pic):
|
||
|
"""Check if the pic is in the queue or already downloaded. If it is
|
||
|
not in any of those, add it to the download queue."""
|
||
|
if pic in self.user_pics:
|
||
|
return
|
||
|
|
||
|
if pic in self.pic_queue:
|
||
|
return
|
||
|
|
||
|
self.pic_queue.add(pic)
|
||
|
self.twitter.download(pic, self.post_pic_download, id=pic)
|
||
|
return
|
||
|
|
||
|
def post_pic_download(self, data, error, id):
|
||
|
"""Function called once we downloaded the user pic."""
|
||
|
|
||
|
_log.debug('Received pic %s' % (id))
|
||
|
|
||
|
if error or not data:
|
||
|
_log.debug('Error with the pic, not loading')
|
||
|
return
|
||
|
|
||
|
loader = gtk.gdk.PixbufLoader()
|
||
|
loader.write(data)
|
||
|
loader.close()
|
||
|
|
||
|
self.user_pics[id] = loader.get_pixbuf()
|
||
|
self.pic_queue.discard(id)
|
||
|
|
||
|
# finally, request the grid to redraw itself
|
||
|
gtk.gdk.threads_enter()
|
||
|
self.grid.queue_draw()
|
||
|
gtk.gdk.threads_leave()
|
||
|
|
||
|
return
|
||
|
# ------------------------------------------------------------
|
||
|
# Helper functions
|
||
|
# ------------------------------------------------------------
|
||
|
def _sort_by_time(self, model, iter1, iter2, data=None):
|
||
|
"""The sort function where we sort by the datetime.datetime object"""
|
||
|
|
||
|
d1 = model.get_value(iter1, Columns.DATETIME)
|
||
|
d2 = model.get_value(iter2, Columns.DATETIME)
|
||
|
|
||
|
# Why do we get called with None values?!
|
||
|
|
||
|
if not d1:
|
||
|
return 1
|
||
|
if not d2:
|
||
|
return -1
|
||
|
|
||
|
if d1 < d2:
|
||
|
return -1
|
||
|
elif d1 > d2:
|
||
|
return 1
|
||
|
return 0
|
||
|
|
||
|
# ------------------------------------------------------------
|
||
|
# Widget creation functions
|
||
|
# ------------------------------------------------------------
|
||
|
def _systray_setup(self):
|
||
|
if not (self._app_icon and self._app_icon_alert):
|
||
|
self._systray = None
|
||
|
return
|
||
|
|
||
|
self._systray = gtk.StatusIcon()
|
||
|
self._systray.set_from_file(self._app_icon)
|
||
|
self._systray.connect('activate', self.systray_cb)
|
||
|
self._systray.set_tooltip('Mitter: Click to toggle window visibility.')
|
||
|
self._systray.set_visible(True)
|
||
|
return
|
||
|
|
||
|
# ------------------------------------------------------------
|
||
|
# 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."""
|
||
|
|
||
|
start = text_buffer.get_start_iter()
|
||
|
end = text_buffer.get_end_iter()
|
||
|
|
||
|
text = text_buffer.get_text(start, end, include_hidden_chars=False)
|
||
|
|
||
|
self._char_count.set_text('(%d)' % (140 - len(text)))
|
||
|
|
||
|
return True
|
||
|
|
||
|
def _update_status(self):
|
||
|
"""Update your status."""
|
||
|
_log.debug('Updating status.')
|
||
|
status = self._update_text.get_text()
|
||
|
status = status.strip()
|
||
|
if not str_len(status):
|
||
|
return
|
||
|
|
||
|
self.update_text.set_sensitive(False)
|
||
|
self.statusbar.push(self.statusbar_context, 'Updating your status...')
|
||
|
|
||
|
# ------------------------------------------------------------
|
||
|
# Required functions for all interfaces
|
||
|
# ------------------------------------------------------------
|
||
|
|
||
|
def __init__(self, connection, options):
|
||
|
"""Class initialization."""
|
||
|
|
||
|
self._connection = connection
|
||
|
self._options = options
|
||
|
|
||
|
self._user_pics = {}
|
||
|
self._pic_queue = set()
|
||
|
|
||
|
# Load images
|
||
|
self._app_icon = util.find_image('mitter.png')
|
||
|
self._app_icon_alert = util.find_image('mitter-new.png')
|
||
|
|
||
|
unknown_pixbuf = util.find_image('unknown.png')
|
||
|
if unknown_pixbuf:
|
||
|
self._default_pixmap = gtk.gdk.pixbuf_new_from_file(
|
||
|
unknown_pixbuf)
|
||
|
else:
|
||
|
self._default_pixmap = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB,
|
||
|
has_alpha=False, bits_per_sample=8, width=48, height=48)
|
||
|
|
||
|
self._main_window = _MainWindow(_GtkController())
|
||
|
self._main_window.set_title('Mitter')
|
||
|
self._main_window.set_size_request(10, 10) # very small minimal size
|
||
|
self._main_window.resize(self._options[NAMESPACE]['width'],
|
||
|
self._options[NAMESPACE]['height'])
|
||
|
self._main_window.move(self._options[NAMESPACE]['position_x'],
|
||
|
self._options[NAMESPACE]['position_y'])
|
||
|
|
||
|
if self._app_icon:
|
||
|
self._main_window.set_icon_from_file(self._app_icon)
|
||
|
|
||
|
|
||
|
#self._main_window()
|
||
|
#self._systray_setup()
|
||
|
|
||
|
# self.create_settings_dialog()
|
||
|
# self.username_field.set_text(default_username)
|
||
|
# self.password_field.set_text(default_password)
|
||
|
# self.https_field.set_active(self.https)
|
||
|
|
||
|
# notification helper
|
||
|
# self.notify_broadcast = Notify('mitter').notify
|
||
|
|
||
|
# start auto refresh activity
|
||
|
|
||
|
# self._refresh_id = None
|
||
|
# self.set_auto_refresh()
|
||
|
|
||
|
# self.window.set_focus(self.update_text)
|
||
|
return
|
||
|
|
||
|
def __call__(self):
|
||
|
"""Call function; displays the interface. This method should appear on
|
||
|
every interface."""
|
||
|
|
||
|
self._main_window.show_all()
|
||
|
|
||
|
gtk.main()
|