|
|
|
@ -38,82 +38,14 @@ from mitterlib.ui.helpers import timesince
|
|
|
|
|
|
|
|
|
|
# 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 Interface(object): |
|
|
|
|
"""Linux/GTK interface for Mitter.""" |
|
|
|
|
|
|
|
|
|
NAMESPACE = 'pygtk' |
|
|
|
|
|
|
|
|
|
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.""" |
|
|
|
|
|
|
|
|
@ -143,823 +75,6 @@ class Interface(object):
|
|
|
|
|
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 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 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 |
|
|
|
|
# ------------------------------------------------------------ |
|
|
|
@ -1063,7 +178,7 @@ class Interface(object):
|
|
|
|
|
message_renderer.set_property('width', 10) |
|
|
|
|
|
|
|
|
|
message_column = gtk.TreeViewColumn('Message', |
|
|
|
|
message_renderer, text=1) |
|
|
|
|
message_renderer) |
|
|
|
|
message_column.set_cell_data_func(message_renderer, |
|
|
|
|
self._cell_renderer_message) |
|
|
|
|
self.grid.append_column(message_column) |
|
|
|
@ -1088,7 +203,7 @@ class Interface(object):
|
|
|
|
|
|
|
|
|
|
refresh_action = gtk.Action('Refresh', '_Refresh', |
|
|
|
|
'Update the listing', gtk.STOCK_REFRESH) |
|
|
|
|
refresh_action.connect('activate', self.refresh) |
|
|
|
|
#refresh_action.connect('activate', self.refresh) |
|
|
|
|
|
|
|
|
|
quit_action = gtk.Action('Quit', '_Quit', |
|
|
|
|
'Exit Mitter', gtk.STOCK_QUIT) |
|
|
|
@ -1096,7 +211,7 @@ class Interface(object):
|
|
|
|
|
|
|
|
|
|
settings_action = gtk.Action('Settings', '_Settings', |
|
|
|
|
'Settings', gtk.STOCK_PREFERENCES) |
|
|
|
|
settings_action.connect('activate', self.show_settings) |
|
|
|
|
#settings_action.connect('activate', self.show_settings) |
|
|
|
|
|
|
|
|
|
update_action = gtk.Action('Update', '_Update', 'Update your status', |
|
|
|
|
gtk.STOCK_ADD) |
|
|
|
@ -1106,19 +221,19 @@ class Interface(object):
|
|
|
|
|
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) |
|
|
|
|
#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) |
|
|
|
|
#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) |
|
|
|
|
#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) |
|
|
|
|
|
|
|
|
@ -1140,8 +255,8 @@ class Interface(object):
|
|
|
|
|
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(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') |
|
|
|
|
|
|
|
|
|