#!/usr/bin/python # -*- coding: utf-8 -*- # Mitter, a Maemo client for Twitter. # Copyright (C) 2007, 2008 Julio Biason, Deepak Sarda # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import gtk import gobject gobject.threads_init() import logging from mitterlib.ui.helpers.image_helpers import find_image #from mitterlib.constants import gpl_3, version #from mitterlib.ui.helpers.utils import str_len #from mitterlib.ui.helpers.notify import Notify #from mitterlib.ui.helpers import timesince # Constants _log = logging.getLogger('ui.pygtk') class Interface(object): """Linux/GTK interface for Mitter.""" NAMESPACE = 'pygtk' def show_about(self, widget): """Show the about dialog.""" about_window = gtk.AboutDialog() about_window.set_name('Mitter') about_window.set_version(version) about_window.set_copyright('2007-2009 Mitter Contributors') about_window.set_license(gpl_3) about_window.set_website('http://code.google.com/p/mitter') about_window.set_website_label('Mitter on GoogleCode') about_window.set_authors([ 'Main developers:', 'Julio Biason', 'Deepak Sarda', 'Gerald Kaszuba', ' ', 'And patches from:', 'Santiago Gala', 'Sugree Phatanapherom', 'Kristian Rietveld', '"Wiennat"', 'Philip Reynolds', 'Greg McIntyre', '"Alexander"']) about_window.connect('close', self.close_dialog) about_window.run() about_window.hide() # ------------------------------------------------------------ # 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 def _create_main_window(self): """Returns the object with the main window and the attached widgets.""" main_window = gtk.Window(gtk.WINDOW_TOPLEVEL) initial_width = int(self._options[self.NAMESPACE]['width']) initial_height = int(self._options[self.NAMESPACE]['height']) _log.debug('Initial size: %d x %d', initial_width, initial_height) initial_x = int(self._options[self.NAMESPACE]['position_x']) initial_y = int(self._options[self.NAMESPACE]['position_y']) _log.debug('Initial position: %d x %d', initial_x, initial_y) main_window.set_title('Mitter') main_window.set_size_request(10, 10) # very small minimal size main_window.resize(initial_width, initial_height) main_window.move(initial_x, initial_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(object) # Trying to store the NetworkData object only self.grid_store.set_sort_func(0, self._sort_by_time) self.grid_store.set_sort_column_id(0, 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) 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_app) 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.""" data = store.get_value(position, 0) pic = data.avatar 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.""" data = store.get_value(position, 0) message = data.message username = data.username time = timesince.timesince(data.message_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' % \ (data.name, username, message, time) cell.set_property('markup', markup) return # ------------------------------------------------------------ # Widget callback functions # ------------------------------------------------------------ def _count_chars(self, text_buffer): """Count the number of chars in the edit field and update the label that shows the available space.""" start = text_buffer.get_start_iter() end = text_buffer.get_end_iter() text = text_buffer.get_text(start, end, include_hidden_chars=False) self._char_count.set_text('(%d)' % (140 - len(text))) return True def _update_status(self): """Update your status.""" _log.debug('Updating status.') status = self._update_text.get_text() status = status.strip() if not str_len(status): return self.update_text.set_sensitive(False) self.statusbar.push(self.statusbar_context, 'Updating your status...') def quit_app(self, widget=None, user_data=None): """Callback when the window is destroyed or the user selects "Quit".""" (x, y) = self._main_window.get_position() _log.debug('Current position: %d x %d', x, y) self._options[self.NAMESPACE]['position_x'] = x self._options[self.NAMESPACE]['position_y'] = y (width, height) = self._main_window.get_size() _log.debug('Current window size: %d x %d', width, height) self._options[self.NAMESPACE]['width'] = width self._options[self.NAMESPACE]['height'] = height gtk.main_quit() return # ------------------------------------------------------------ # Required functions for all interfaces # ------------------------------------------------------------ def __init__(self, connection, options): """Class initialization.""" self._connection = connection self._options = options self._user_pics = {} self._pic_queue = set() # Load images self._app_icon = find_image('mitter.png') self._app_icon_alert = find_image('mitter-new.png') unknown_pixbuf = find_image('unknown.png') if unknown_pixbuf: self._default_pixmap = gtk.gdk.pixbuf_new_from_file( unknown_pixbuf) else: self._default_pixmap = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, has_alpha=False, bits_per_sample=8, width=48, height=48) self._main_window = self._create_main_window() #self._systray_setup() # self.create_settings_dialog() # self.username_field.set_text(default_username) # self.password_field.set_text(default_password) # self.https_field.set_active(self.https) # notification helper # self.notify_broadcast = Notify('mitter').notify # start auto refresh activity # self._refresh_id = None # self.set_auto_refresh() # self.window.set_focus(self.update_text) return def __call__(self): """Call function; displays the interface. This method should appear on every interface.""" self._main_window.show_all() gtk.main() @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?