diff --git a/mitterlib/ui/ui_pygtk.py b/mitterlib/ui/ui_pygtk.py index 6baa2b0..0970bdb 100644 --- a/mitterlib/ui/ui_pygtk.py +++ b/mitterlib/ui/ui_pygtk.py @@ -27,77 +27,14 @@ 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? - +from mitterlib.ui.helpers.utils import str_len +from mitterlib.ui.helpers.notify import Notify +from mitterlib.ui.helpers import timesince # Constants @@ -110,290 +47,11 @@ 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, 'u') - self.action_group.add_action_with_accel(mute_action, 'm') - self.action_group.add_action_with_accel(update_action, - 'Return') - - # definition of the UI - - uimanager = gtk.UIManager() - uimanager.insert_action_group(self.action_group, 0) - 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'\1', - message) - - # use a different highlight for the current user - message = re.sub(r'(@'+self.twitter.username+')', - r'\1', - message) - - markup = '%s (%s):\n%s\n%s' % \ - (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.""" + 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() @@ -1205,23 +863,6 @@ class Interface(object): 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) @@ -1355,6 +996,274 @@ class Interface(object): self._systray.set_visible(True) return + def _create_main_window(self): + main_window = gtk.Window() + + main_window.set_title('Mitter') + 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, 'u') + self.action_group.add_action_with_accel(mute_action, 'm') + self.action_group.add_action_with_accel(update_action, + 'Return') + + # definition of the UI + + uimanager = gtk.UIManager() + uimanager.insert_action_group(self.action_group, 0) + 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._count_chars) + + update_button = gtk.Button(stock=gtk.STOCK_ADD) + update_button.connect('clicked', self._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'\1', + message) + + # use a different highlight for the current user + message = re.sub(r'(@'+self.twitter.username+')', + r'\1', + message) + + markup = '%s (%s):\n%s\n%s' % \ + (user, username, message, time) + cell.set_property('markup', markup) + + return + # ------------------------------------------------------------ # Widget callback functions # ------------------------------------------------------------ @@ -1383,6 +1292,9 @@ class Interface(object): self.update_text.set_sensitive(False) self.statusbar.push(self.statusbar_context, 'Updating your status...') + def quit_app(self, widget=None): + gtk.main_quit() + # ------------------------------------------------------------ # Required functions for all interfaces # ------------------------------------------------------------ @@ -1408,21 +1320,12 @@ class Interface(object): 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._create_main_window() #self._main_window() - #self._systray_setup() + #self._systray_setup() # self.create_settings_dialog() # self.username_field.set_text(default_username) # self.password_field.set_text(default_password) @@ -1446,3 +1349,64 @@ class Interface(object): self._main_window.show_all() 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? +