diff options
Diffstat (limited to 'src/jarabe/journal/listview.py')
-rw-r--r-- | src/jarabe/journal/listview.py | 359 |
1 files changed, 274 insertions, 85 deletions
diff --git a/src/jarabe/journal/listview.py b/src/jarabe/journal/listview.py index 3366def..251388d 100644 --- a/src/jarabe/journal/listview.py +++ b/src/jarabe/journal/listview.py @@ -20,99 +20,106 @@ import time import gobject import gtk +import hippo import gconf import pango from sugar.graphics import style -from sugar.graphics.icon import CellRendererIcon +from sugar.graphics.icon import CanvasIcon, Icon, CellRendererIcon from sugar.graphics.xocolor import XoColor from sugar import util -from jarabe.journal.source import Source from jarabe.journal.listmodel import ListModel from jarabe.journal.palettes import ObjectPalette, BuddyPalette from jarabe.journal import model from jarabe.journal import misc -class ListView(gtk.TreeView): - __gtype_name__ = 'JournalListView' +UPDATE_INTERVAL = 300 - __gsignals__ = { - 'detail-clicked': (gobject.SIGNAL_RUN_FIRST, - gobject.TYPE_NONE, - ([object])), - 'entry-activated': (gobject.SIGNAL_RUN_FIRST, - gobject.TYPE_NONE, - ([str])), - } +MESSAGE_EMPTY_JOURNAL = 0 +MESSAGE_NO_MATCH = 1 + +class TreeView(gtk.TreeView): + __gtype_name__ = 'JournalTreeView' def __init__(self): gtk.TreeView.__init__(self) - self.props.fixed_height_mode = True - self.cell_title = None - self.cell_icon = None - self._title_column = None - self.date_column = None - self._add_columns() + def do_size_request(self, requisition): + # HACK: We tell the model that the view is just resizing so it can avoid + # hitting both D-Bus and disk. + tree_model = self.get_model() + if tree_model is not None: + tree_model.view_is_resizing = True + try: + gtk.TreeView.do_size_request(self, requisition) + finally: + if tree_model is not None: + tree_model.view_is_resizing = False - self.enable_model_drag_source(gtk.gdk.BUTTON1_MASK, - [('text/uri-list', 0, 0), ('journal-object-id', 0, 0)], - gtk.gdk.ACTION_COPY) +class BaseListView(gtk.Bin): + __gtype_name__ = 'JournalBaseListView' - self.cell_title.props.editable = True - self.cell_title.connect('edited', self.__cell_title_edited_cb) + __gsignals__ = { + 'clear-clicked': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([])) + } - self.cell_icon.connect('clicked', self.__icon_clicked_cb) - self.cell_icon.connect('detail-clicked', self.__detail_clicked_cb) + def __init__(self): + self._query = {} + self._model = None + self._progress_bar = None + self._last_progress_bar_pulse = None - cell_detail = CellRendererDetail(self) - cell_detail.connect('clicked', self.__detail_cell_clicked_cb) + gobject.GObject.__init__(self) - column = gtk.TreeViewColumn('') - column.props.sizing = gtk.TREE_VIEW_COLUMN_FIXED - column.props.fixed_width = cell_detail.props.width - column.pack_start(cell_detail) - self.append_column(column) + self.connect('destroy', self.__destroy_cb) - self.connect('notify::hover-selection', - self.__notify_hover_selection_cb) - self.connect('button-release-event', self.__button_release_event_cb) + self._scrolled_window = gtk.ScrolledWindow() + self._scrolled_window.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) + self.add(self._scrolled_window) + self._scrolled_window.show() - def __button_release_event_cb(self, tree_view, event): - if not tree_view.props.hover_selection: - return False + self.tree_view = TreeView() + self.tree_view.props.fixed_height_mode = True + self.tree_view.modify_base(gtk.STATE_NORMAL, + style.COLOR_WHITE.get_gdk_color()) + self._scrolled_window.add(self.tree_view) + self.tree_view.show() - if event.window != tree_view.get_bin_window(): - return False + self.cell_title = None + self.cell_icon = None + self._title_column = None + self.date_column = None + self._add_columns() - pos = tree_view.get_path_at_pos(event.x, event.y) - if pos is None: - return False + self.tree_view.enable_model_drag_source(gtk.gdk.BUTTON1_MASK, + [('text/uri-list', 0, 0), + ('journal-object-id', 0, 0)], + gtk.gdk.ACTION_COPY) - path, column_, x_, y_ = pos - uid = tree_view.get_model()[path][Source.FIELD_UID] - self.emit('entry-activated', uid) + # Auto-update stuff + self._fully_obscured = True + self._dirty = False + self._refresh_idle_handler = None + self._update_dates_timer = None - return False + model.created.connect(self.__model_created_cb) + model.updated.connect(self.__model_updated_cb) + model.deleted.connect(self.__model_deleted_cb) - def __notify_hover_selection_cb(self, widget, pspec): - self.cell_icon.props.show_palette = not self.props.hover_selection + def __model_created_cb(self, sender, **kwargs): + self._set_dirty() - def do_size_request(self, requisition): - # HACK: We tell the model that the view is just resizing so it can avoid - # hitting both D-Bus and disk. - tree_model = self.get_model() - if tree_model is not None: - tree_model.view_is_resizing = True - try: - gtk.TreeView.do_size_request(self, requisition) - finally: - if tree_model is not None: - tree_model.view_is_resizing = False + def __model_updated_cb(self, sender, **kwargs): + self._set_dirty() + + def __model_deleted_cb(self, sender, **kwargs): + self._set_dirty() def _add_columns(self): - cell_favorite = CellRendererFavorite(self) + cell_favorite = CellRendererFavorite(self.tree_view) cell_favorite.connect('clicked', self.__favorite_clicked_cb) column = gtk.TreeViewColumn('') @@ -120,9 +127,9 @@ class ListView(gtk.TreeView): column.props.fixed_width = cell_favorite.props.width column.pack_start(cell_favorite) column.set_cell_data_func(cell_favorite, self.__favorite_set_data_cb) - self.append_column(column) + self.tree_view.append_column(column) - self.cell_icon = CellRendererActivityIcon(self) + self.cell_icon = CellRendererActivityIcon(self.tree_view) column = gtk.TreeViewColumn('') column.props.sizing = gtk.TREE_VIEW_COLUMN_FIXED @@ -130,8 +137,8 @@ class ListView(gtk.TreeView): column.pack_start(self.cell_icon) column.add_attribute(self.cell_icon, 'file-name', ListModel.COLUMN_ICON) column.add_attribute(self.cell_icon, 'xo-color', - ListModel.COLUMN_ICON_COLOR) - self.append_column(column) + ListModel.COLUMN_ICON_COLOR) + self.tree_view.append_column(column) self.cell_title = gtk.CellRendererText() self.cell_title.props.ellipsize = pango.ELLIPSIZE_MIDDLE @@ -145,15 +152,15 @@ class ListView(gtk.TreeView): self._title_column.add_attribute(self.cell_title, 'markup', ListModel.COLUMN_TITLE) self._title_column.connect('clicked', self.__header_clicked_cb) - self.append_column(self._title_column) + self.tree_view.append_column(self._title_column) buddies_column = gtk.TreeViewColumn('') buddies_column.props.sizing = gtk.TREE_VIEW_COLUMN_FIXED - self.append_column(buddies_column) + self.tree_view.append_column(buddies_column) for column_index in [ListModel.COLUMN_BUDDY_1, ListModel.COLUMN_BUDDY_2, ListModel.COLUMN_BUDDY_3]: - cell_icon = CellRendererBuddy(self, + cell_icon = CellRendererBuddy(self.tree_view, column_index=column_index) buddies_column.pack_start(cell_icon) buddies_column.props.fixed_width += cell_icon.props.width @@ -179,7 +186,7 @@ class ListView(gtk.TreeView): self.date_column.pack_start(cell_text) self.date_column.add_attribute(cell_text, 'text', ListModel.COLUMN_DATE) self.date_column.connect('clicked', self.__header_clicked_cb) - self.append_column(self.date_column) + self.tree_view.append_column(self.date_column) def __header_clicked_cb(self, column_clicked): if column_clicked == self._title_column: @@ -199,7 +206,7 @@ class ListView(gtk.TreeView): else: self._query['order_by'] = ['+timestamp'] - self._refresh() + self.refresh() # Need to update the column indicators after the model has been reset if self._query['order_by'] == ['-timestamp']: @@ -230,8 +237,19 @@ class ListView(gtk.TreeView): width, height_ = layout.get_size() return pango.PIXELS(width) + def do_size_allocate(self, allocation): + self.allocation = allocation + self.child.size_allocate(allocation) + + def do_size_request(self, requisition): + requisition.width, requisition.height = self.child.size_request() + + def __destroy_cb(self, widget): + if self._model is not None: + self._model.stop() + def __favorite_set_data_cb(self, column, cell, tree_model, tree_iter): - favorite = self.get_model()[tree_iter][ListModel.COLUMN_FAVORITE] + favorite = self._model[tree_iter][ListModel.COLUMN_FAVORITE] if favorite: client = gconf.client_get_default() color = XoColor(client.get_string('/desktop/sugar/user/color')) @@ -241,7 +259,7 @@ class ListView(gtk.TreeView): cell.props.fill_color = style.COLOR_WHITE.get_svg() def __favorite_clicked_cb(self, cell, path): - row = self.get_model()[path] + row = self._model[path] metadata = model.get(row[ListModel.COLUMN_UID]) if metadata['keep'] == '1': metadata['keep'] = '0' @@ -249,41 +267,212 @@ class ListView(gtk.TreeView): metadata['keep'] = '1' model.write(metadata, update_mtime=False) - def update_dates(self): - if not self.flags() & gtk.REALIZED: + def update_with_query(self, query_dict): + logging.debug('ListView.update_with_query') + self._query = query_dict + + if 'order_by' not in self._query: + self._query['order_by'] = ['+timestamp'] + + self.refresh() + + def refresh(self): + logging.debug('ListView.refresh query %r', self._query) + self._stop_progress_bar() + self._start_progress_bar() + + if self._model is not None: + self._model.stop() + + self._model = ListModel(self._query) + self._model.connect('ready', self.__model_ready_cb) + self._model.connect('progress', self.__model_progress_cb) + self._model.setup() + + def __model_ready_cb(self, tree_model): + self._stop_progress_bar() + + # Cannot set it up earlier because will try to access the model and it + # needs to be ready. + self.tree_view.set_model(self._model) + + if len(tree_model) == 0: + if self._is_query_empty(): + self._show_message(MESSAGE_EMPTY_JOURNAL) + else: + self._show_message(MESSAGE_NO_MATCH) + else: + self._clear_message() + + def _is_query_empty(self): + # FIXME: This is a hack, we shouldn't have to update this every time + # a new search term is added. + if self._query.get('query', '') or self._query.get('mime_type', '') or \ + self._query.get('keep', '') or self._query.get('mtime', '') or \ + self._query.get('activity', ''): + return False + else: + return True + + def __model_progress_cb(self, tree_model): + if time.time() - self._last_progress_bar_pulse > 0.05: + if self._progress_bar is not None: + self._progress_bar.pulse() + self._last_progress_bar_pulse = time.time() + + def _start_progress_bar(self): + alignment = gtk.Alignment(xalign=0.5, yalign=0.5, xscale=0.5) + self.remove(self.child) + self.add(alignment) + alignment.show() + + self._progress_bar = gtk.ProgressBar() + self._progress_bar.props.pulse_step = 0.01 + self._last_progress_bar_pulse = time.time() + alignment.add(self._progress_bar) + self._progress_bar.show() + + def _stop_progress_bar(self): + if self.child != self._progress_bar: return + self.remove(self.child) + self.add(self._scrolled_window) + + def _show_message(self, message): + canvas = hippo.Canvas() + self.remove(self.child) + self.add(canvas) + canvas.show() + + box = hippo.CanvasBox(orientation=hippo.ORIENTATION_VERTICAL, + background_color=style.COLOR_WHITE.get_int(), + yalign=hippo.ALIGNMENT_CENTER, + spacing=style.DEFAULT_SPACING, + padding_bottom=style.GRID_CELL_SIZE) + canvas.set_root(box) + + icon = CanvasIcon(size=style.LARGE_ICON_SIZE, + icon_name='activity-journal', + stroke_color = style.COLOR_BUTTON_GREY.get_svg(), + fill_color = style.COLOR_TRANSPARENT.get_svg()) + box.append(icon) + + if message == MESSAGE_EMPTY_JOURNAL: + text = _('Your Journal is empty') + elif message == MESSAGE_NO_MATCH: + text = _('No matching entries') + else: + raise ValueError('Invalid message') + + text = hippo.CanvasText(text=text, + xalign=hippo.ALIGNMENT_CENTER, + font_desc=style.FONT_BOLD.get_pango_desc(), + color = style.COLOR_BUTTON_GREY.get_int()) + box.append(text) + + if message == MESSAGE_NO_MATCH: + button = gtk.Button(label=_('Clear search')) + button.connect('clicked', self.__clear_button_clicked_cb) + button.props.image = Icon(icon_name='dialog-cancel', + icon_size=gtk.ICON_SIZE_BUTTON) + canvas_button = hippo.CanvasWidget(widget=button, + xalign=hippo.ALIGNMENT_CENTER) + box.append(canvas_button) + + def __clear_button_clicked_cb(self, button): + self.emit('clear-clicked') + + def _clear_message(self): + self.remove(self.child) + self.add(self._scrolled_window) + self._scrolled_window.show() + def update_dates(self): logging.debug('ListView.update_dates') - visible_range = self.get_visible_range() + visible_range = self.tree_view.get_visible_range() if visible_range is None: return - path, end_path = visible_range while True: - x, y, width, height = self.get_cell_area(path, self.date_column) - x, y = self.convert_tree_to_widget_coords(x, y) - self.queue_draw_area(x, y, width, height) + x, y, width, height = self.tree_view.get_cell_area(path, + self.date_column) + x, y = self.tree_view.convert_tree_to_widget_coords(x, y) + self.tree_view.queue_draw_area(x, y, width, height) if path == end_path: break else: - next_iter = self.get_model().iter_next( - self.get_model().get_iter(path)) - path = self.get_model().get_path(next_iter) + next_iter = self._model.iter_next(self._model.get_iter(path)) + path = self._model.get_path(next_iter) + + def _set_dirty(self): + if self._fully_obscured: + self._dirty = True + else: + self.refresh() + + def set_is_visible(self, visible): + logging.debug('canvas_visibility_notify_event_cb %r', visible) + if visible: + self._fully_obscured = False + if self._dirty: + self.refresh() + if self._update_dates_timer is None: + logging.debug('Adding date updating timer') + self._update_dates_timer = \ + gobject.timeout_add_seconds(UPDATE_INTERVAL, + self.__update_dates_timer_cb) + else: + self._fully_obscured = True + if self._update_dates_timer is not None: + logging.debug('Remove date updating timer') + gobject.source_remove(self._update_dates_timer) + self._update_dates_timer = None + + def __update_dates_timer_cb(self): + self.update_dates() + return True + +class ListView(BaseListView): + __gtype_name__ = 'JournalListView' + + __gsignals__ = { + 'detail-clicked': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([object])) + } + + def __init__(self): + BaseListView.__init__(self) + + self.cell_title.props.editable = True + self.cell_title.connect('edited', self.__cell_title_edited_cb) + + self.cell_icon.connect('clicked', self.__icon_clicked_cb) + self.cell_icon.connect('detail-clicked', self.__detail_clicked_cb) + + cell_detail = CellRendererDetail(self.tree_view) + cell_detail.connect('clicked', self.__detail_cell_clicked_cb) + + column = gtk.TreeViewColumn('') + column.props.sizing = gtk.TREE_VIEW_COLUMN_FIXED + column.props.fixed_width = cell_detail.props.width + column.pack_start(cell_detail) + self.tree_view.append_column(column) def __detail_cell_clicked_cb(self, cell, path): - row = self.get_model()[path] + row = self.tree_view.get_model()[path] self.emit('detail-clicked', row[ListModel.COLUMN_UID]) def __detail_clicked_cb(self, cell, uid): self.emit('detail-clicked', uid) def __icon_clicked_cb(self, cell, path): - row = self.get_model()[path] + row = self.tree_view.get_model()[path] metadata = model.get(row[ListModel.COLUMN_UID]) misc.resume(metadata) def __cell_title_edited_cb(self, cell, path, new_text): - row = self.get_model()[path] + row = self._model[path] metadata = model.get(row[ListModel.COLUMN_UID]) metadata['title'] = new_text model.write(metadata, update_mtime=False) |