|
|
@ -27,373 +27,31 @@ gtk.gdk.threads_init() |
|
|
|
|
|
|
|
|
|
|
|
import datetime |
|
|
|
import datetime |
|
|
|
import re |
|
|
|
import re |
|
|
|
import timesince |
|
|
|
|
|
|
|
import logging |
|
|
|
import logging |
|
|
|
|
|
|
|
|
|
|
|
import mitterlib as util |
|
|
|
import mitterlib as util |
|
|
|
|
|
|
|
|
|
|
|
from notify import Notify |
|
|
|
|
|
|
|
from mitterlib.constants import gpl_3, version |
|
|
|
from mitterlib.constants import gpl_3, version |
|
|
|
from mitterlib.ui.utils import str_len |
|
|
|
from mitterlib.ui.helpers.utils import str_len |
|
|
|
|
|
|
|
from mitterlib.ui.helpers.notify import Notify |
|
|
|
NAMESPACE = 'pygtk' |
|
|
|
from mitterlib.ui.helpers import timesince |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
|
# Constants |
|
|
|
|
|
|
|
|
|
|
|
URL_RE = re.compile( |
|
|
|
URL_RE = re.compile( |
|
|
|
r'((?:(?:https?|ftp)://|www[-\w]*\.)[^\s\n\r]+[-\w+&@#%=~])', re.I) |
|
|
|
r'((?:(?:https?|ftp)://|www[-\w]*\.)[^\s\n\r]+[-\w+&@#%=~])', re.I) |
|
|
|
|
|
|
|
|
|
|
|
_log = logging.getLogger('ui.pygtk') |
|
|
|
_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): |
|
|
|
class Columns: |
|
|
|
_log.debug('Window delete') |
|
|
|
(PIC, NAME, MESSAGE, USERNAME, ID, DATETIME, ALL_DATA) = range(7) |
|
|
|
gtk.main_quit() |
|
|
|
|
|
|
|
return True |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Interface(object): |
|
|
|
class Interface(object): |
|
|
|
"""Linux/GTK interface for Mitter.""" |
|
|
|
"""Linux/GTK interface for Mitter.""" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
NAMESPACE = 'pygtk' |
|
|
|
|
|
|
|
|
|
|
|
def systray_cb(self, widget, user_param=None): |
|
|
|
def systray_cb(self, widget, user_param=None): |
|
|
|
if self.window.get_property('visible') and self.window.is_active(): |
|
|
|
if self.window.get_property('visible') and self.window.is_active(): |
|
|
|
x, y = self.window.get_position() |
|
|
|
x, y = self.window.get_position() |
|
|
@ -1205,154 +863,405 @@ class Interface(object): |
|
|
|
|
|
|
|
|
|
|
|
return |
|
|
|
return |
|
|
|
|
|
|
|
|
|
|
|
def save_interface_prefs(self): |
|
|
|
def refresh_rate_limit(self): |
|
|
|
"""Using the save callback, save all this interface preferences.""" |
|
|
|
"""Request the rate limit and check if we are doing okay.""" |
|
|
|
|
|
|
|
self.twitter.rate_limit_status(self.post_refresh_rate_limit) |
|
|
|
|
|
|
|
return |
|
|
|
|
|
|
|
|
|
|
|
self.prefs['refresh_interval'] = \ |
|
|
|
def post_refresh_rate_limit(self, data, error): |
|
|
|
self.refresh_interval_field.get_value_as_int() |
|
|
|
"""Callback for the refresh_rate_limit.""" |
|
|
|
|
|
|
|
if error or not data: |
|
|
|
|
|
|
|
_log.error('Error fetching rate limit') |
|
|
|
|
|
|
|
return |
|
|
|
|
|
|
|
|
|
|
|
x, y = self.window.get_position() |
|
|
|
# Check if we are running low on our limit |
|
|
|
self.prefs['position_x'] = x |
|
|
|
reset_time = datetime.datetime.fromtimestamp( |
|
|
|
self.prefs['position_y'] = y |
|
|
|
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 |
|
|
|
|
|
|
|
|
|
|
|
self.save_callback(self.username_field.get_text(), |
|
|
|
if pic in self.pic_queue: |
|
|
|
self.password_field.get_text(), |
|
|
|
return |
|
|
|
self.https_field.get_active(), |
|
|
|
|
|
|
|
NAMESPACE, self.prefs) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self.pic_queue.add(pic) |
|
|
|
|
|
|
|
self.twitter.download(pic, self.post_pic_download, id=pic) |
|
|
|
return |
|
|
|
return |
|
|
|
|
|
|
|
|
|
|
|
def refresh_rate_limit(self): |
|
|
|
def post_pic_download(self, data, error, id): |
|
|
|
"""Request the rate limit and check if we are doing okay.""" |
|
|
|
"""Function called once we downloaded the user pic.""" |
|
|
|
self.twitter.rate_limit_status(self.post_refresh_rate_limit) |
|
|
|
|
|
|
|
|
|
|
|
_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 |
|
|
|
return |
|
|
|
|
|
|
|
|
|
|
|
def post_refresh_rate_limit(self, data, error): |
|
|
|
def _create_main_window(self): |
|
|
|
"""Callback for the refresh_rate_limit.""" |
|
|
|
main_window = gtk.Window() |
|
|
|
if error or not data: |
|
|
|
|
|
|
|
_log.error('Error fetching rate limit') |
|
|
|
main_window.set_title('Mitter') |
|
|
|
return |
|
|
|
main_window.set_size_request(10, 10) # very small minimal size |
|
|
|
|
|
|
|
main_window.resize(self._options[self.NAMESPACE]['width'], |
|
|
|
|
|
|
|
self._options[self.NAMESPACE]['height']) |
|
|
|
|
|
|
|
main_window.move(self._options[self.NAMESPACE]['position_x'], |
|
|
|
|
|
|
|
self._options[self.NAMESPACE]['position_y']) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if self._app_icon: |
|
|
|
|
|
|
|
main_window.set_icon_from_file(self._app_icon) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
main_window.connect('destroy', self.quit_app) |
|
|
|
|
|
|
|
main_window.connect('delete-event', self.quit_app) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
grid = self._create_grid() |
|
|
|
|
|
|
|
(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) |
|
|
|
|
|
|
|
main_window.add(box) |
|
|
|
|
|
|
|
main_window.add_accel_group(accelerators) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return main_window |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _create_grid(self): |
|
|
|
|
|
|
|
"""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(self.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 |
|
|
|
|
|
|
|
|
|
|
|
# Check if we are running low on our limit |
|
|
|
uimanager = gtk.UIManager() |
|
|
|
reset_time = datetime.datetime.fromtimestamp( |
|
|
|
uimanager.insert_action_group(self.action_group, 0) |
|
|
|
int(data['reset_time_in_seconds'])) |
|
|
|
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) |
|
|
|
|
|
|
|
|
|
|
|
if reset_time < datetime.datetime.now(): |
|
|
|
main_menu = uimanager.get_widget('/MainMenu') |
|
|
|
# Clock differences can cause this |
|
|
|
main_toolbar = uimanager.get_widget('/MainToolbar') |
|
|
|
return |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
time_delta = reset_time - datetime.datetime.now() |
|
|
|
return (main_menu, main_toolbar, uimanager.get_accel_group()) |
|
|
|
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.' |
|
|
|
def _create_update_box(self): |
|
|
|
% (remaining_hits, mins_till_reset)) |
|
|
|
"""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._count_chars) |
|
|
|
|
|
|
|
|
|
|
|
if needed_hits > remaining_hits: |
|
|
|
update_button = gtk.Button(stock=gtk.STOCK_ADD) |
|
|
|
gtk.gdk.threads_enter() |
|
|
|
update_button.connect('clicked', self._update_status) |
|
|
|
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): |
|
|
|
update_box = gtk.HBox(False, 0) |
|
|
|
"""Add the last update time in the status bar.""" |
|
|
|
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) |
|
|
|
|
|
|
|
|
|
|
|
last_update = self.last_update.strftime('%H:%M') |
|
|
|
info_box = gtk.HBox(False, 0) |
|
|
|
next_update = (self.last_update + |
|
|
|
self._char_count = gtk.Label() |
|
|
|
datetime.timedelta(minutes=self.prefs[ |
|
|
|
self._char_count.set_text('(140)') |
|
|
|
'refresh_interval'])).strftime('%H:%M') |
|
|
|
info_box.pack_start(gtk.Label('What are you doing?')) |
|
|
|
|
|
|
|
info_box.pack_start(self._char_count) |
|
|
|
|
|
|
|
|
|
|
|
message = 'Last update %s, next update %s' % (last_update, |
|
|
|
update_area = gtk.VBox(True, 0) |
|
|
|
next_update) |
|
|
|
update_area.pack_start(info_box) |
|
|
|
self.statusbar.push(self.statusbar_context, message) |
|
|
|
update_area.pack_start(update_box) |
|
|
|
return |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def queue_pic(self, pic): |
|
|
|
return update_area |
|
|
|
"""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: |
|
|
|
def _create_statusbar(self): |
|
|
|
return |
|
|
|
"""Create the statusbar.""" |
|
|
|
|
|
|
|
statusbar = gtk.Statusbar() |
|
|
|
|
|
|
|
# TODO: Probaly set the context in the object. |
|
|
|
|
|
|
|
return statusbar |
|
|
|
|
|
|
|
|
|
|
|
self.pic_queue.add(pic) |
|
|
|
# ------------------------------------------------------------ |
|
|
|
self.twitter.download(pic, self.post_pic_download, id=pic) |
|
|
|
# Cell rendering functions |
|
|
|
return |
|
|
|
# ------------------------------------------------------------ |
|
|
|
|
|
|
|
def _cell_renderer_user(self, column, cell, store, position): |
|
|
|
|
|
|
|
"""Callback for the user column. Used to created the pixbuf of the |
|
|
|
|
|
|
|
userpic.""" |
|
|
|
|
|
|
|
|
|
|
|
def post_pic_download(self, data, error, id): |
|
|
|
pic = store.get_value(position, Columns.PIC) |
|
|
|
"""Function called once we downloaded the user pic.""" |
|
|
|
if not pic in self._user_pics: |
|
|
|
|
|
|
|
cell.set_property('pixbuf', self._default_pixmap) |
|
|
|
|
|
|
|
|
|
|
|
_log.debug('Received pic %s' % (id)) |
|
|
|
# just make sure we download this pic too. |
|
|
|
|
|
|
|
self.queue_pic(pic) |
|
|
|
|
|
|
|
else: |
|
|
|
|
|
|
|
cell.set_property('pixbuf', self._user_pics[pic]) |
|
|
|
|
|
|
|
|
|
|
|
if error or not data: |
|
|
|
|
|
|
|
_log.debug('Error with the pic, not loading') |
|
|
|
|
|
|
|
return |
|
|
|
return |
|
|
|
|
|
|
|
|
|
|
|
loader = gtk.gdk.PixbufLoader() |
|
|
|
def _cell_renderer_message(self, column, cell, store, position): |
|
|
|
loader.write(data) |
|
|
|
"""Callback for the message column. We need this to adjust the markup |
|
|
|
loader.close() |
|
|
|
property of the cell, as setting it as text won't do any markup |
|
|
|
|
|
|
|
processing.""" |
|
|
|
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 |
|
|
|
user = store.get_value(position, Columns.NAME) |
|
|
|
# ------------------------------------------------------------ |
|
|
|
message = store.get_value(position, Columns.MESSAGE) |
|
|
|
# Helper functions |
|
|
|
time = store.get_value(position, Columns.DATETIME) |
|
|
|
# ------------------------------------------------------------ |
|
|
|
username = store.get_value(position, Columns.USERNAME) |
|
|
|
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) |
|
|
|
time = timesince.timesince(time) |
|
|
|
d2 = model.get_value(iter2, Columns.DATETIME) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Why do we get called with None values?! |
|
|
|
# unescape escaped entities that pango is okay with |
|
|
|
|
|
|
|
message = re.sub(r'&(?!(amp;|gt;|lt;|quot;|apos;))', r'&', message) |
|
|
|
|
|
|
|
|
|
|
|
if not d1: |
|
|
|
# highlight URLs |
|
|
|
return 1 |
|
|
|
message = url_re.sub(r'<span foreground="blue">\1</span>', |
|
|
|
if not d2: |
|
|
|
message) |
|
|
|
return -1 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if d1 < d2: |
|
|
|
# use a different highlight for the current user |
|
|
|
return -1 |
|
|
|
message = re.sub(r'(@'+self.twitter.username+')', |
|
|
|
elif d1 > d2: |
|
|
|
r'<span foreground="#FF6633">\1</span>', |
|
|
|
return 1 |
|
|
|
message) |
|
|
|
return 0 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ------------------------------------------------------------ |
|
|
|
markup = '<b>%s</b> <small>(%s)</small>:\n%s\n<small>%s</small>' % \ |
|
|
|
# Widget creation functions |
|
|
|
(user, username, message, time) |
|
|
|
# ------------------------------------------------------------ |
|
|
|
cell.set_property('markup', markup) |
|
|
|
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 |
|
|
|
return |
|
|
|
|
|
|
|
|
|
|
|
# ------------------------------------------------------------ |
|
|
|
# ------------------------------------------------------------ |
|
|
@ -1383,6 +1292,9 @@ class Interface(object): |
|
|
|
self.update_text.set_sensitive(False) |
|
|
|
self.update_text.set_sensitive(False) |
|
|
|
self.statusbar.push(self.statusbar_context, 'Updating your status...') |
|
|
|
self.statusbar.push(self.statusbar_context, 'Updating your status...') |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def quit_app(self, widget=None): |
|
|
|
|
|
|
|
gtk.main_quit() |
|
|
|
|
|
|
|
|
|
|
|
# ------------------------------------------------------------ |
|
|
|
# ------------------------------------------------------------ |
|
|
|
# Required functions for all interfaces |
|
|
|
# Required functions for all interfaces |
|
|
|
# ------------------------------------------------------------ |
|
|
|
# ------------------------------------------------------------ |
|
|
@ -1408,21 +1320,12 @@ class Interface(object): |
|
|
|
self._default_pixmap = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, |
|
|
|
self._default_pixmap = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, |
|
|
|
has_alpha=False, bits_per_sample=8, width=48, height=48) |
|
|
|
has_alpha=False, bits_per_sample=8, width=48, height=48) |
|
|
|
|
|
|
|
|
|
|
|
self._main_window = _MainWindow(_GtkController()) |
|
|
|
self._main_window = self._create_main_window() |
|
|
|
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._main_window() |
|
|
|
#self._systray_setup() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#self._systray_setup() |
|
|
|
# self.create_settings_dialog() |
|
|
|
# self.create_settings_dialog() |
|
|
|
# self.username_field.set_text(default_username) |
|
|
|
# self.username_field.set_text(default_username) |
|
|
|
# self.password_field.set_text(default_password) |
|
|
|
# self.password_field.set_text(default_password) |
|
|
@ -1446,3 +1349,64 @@ class Interface(object): |
|
|
|
self._main_window.show_all() |
|
|
|
self._main_window.show_all() |
|
|
|
|
|
|
|
|
|
|
|
gtk.main() |
|
|
|
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') |
|
|
|
|
|
|
|
# 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? |
|
|
|
|
|
|
|
|
|
|
|