diff options
author | Tomeu Vizoso <tomeu@sugarlabs.org> | 2009-06-27 16:14:01 (GMT) |
---|---|---|
committer | Tomeu Vizoso <tomeu@sugarlabs.org> | 2009-06-27 16:14:01 (GMT) |
commit | 7fa188ada12b38439d0b609dc989c5576788bb69 (patch) | |
tree | 01fb7d72d7dc393892db85dcaaa21a23e8f4dcee /src | |
parent | 014a004eb2a708bd368f494e98a6c3705495cf76 (diff) |
Refactor journal to use a gtk.TreeView
Diffstat (limited to 'src')
-rw-r--r-- | src/jarabe/journal/Makefile.am | 2 | ||||
-rw-r--r-- | src/jarabe/journal/collapsedentry.py | 397 | ||||
-rw-r--r-- | src/jarabe/journal/journalactivity.py | 13 | ||||
-rw-r--r-- | src/jarabe/journal/listmodel.py | 204 | ||||
-rw-r--r-- | src/jarabe/journal/listview.py | 697 | ||||
-rw-r--r-- | src/jarabe/journal/model.py | 81 | ||||
-rw-r--r-- | src/jarabe/journal/palettes.py | 6 |
7 files changed, 591 insertions, 809 deletions
diff --git a/src/jarabe/journal/Makefile.am b/src/jarabe/journal/Makefile.am index 5f66480..f4bf273 100644 --- a/src/jarabe/journal/Makefile.am +++ b/src/jarabe/journal/Makefile.am @@ -1,13 +1,13 @@ sugardir = $(pythondir)/jarabe/journal sugar_PYTHON = \ __init__.py \ - collapsedentry.py \ detailview.py \ expandedentry.py \ journalactivity.py \ journalentrybundle.py \ journaltoolbox.py \ keepicon.py \ + listmodel.py \ listview.py \ misc.py \ modalalert.py \ diff --git a/src/jarabe/journal/collapsedentry.py b/src/jarabe/journal/collapsedentry.py deleted file mode 100644 index 3eeb087..0000000 --- a/src/jarabe/journal/collapsedentry.py +++ /dev/null @@ -1,397 +0,0 @@ -# Copyright (C) 2007, One Laptop Per Child -# -# 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 2 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, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA - -import logging -from gettext import gettext as _ - -import gobject -import gtk -import hippo -import cjson - -from sugar.graphics.icon import CanvasIcon -from sugar.graphics.xocolor import XoColor -from sugar.graphics import style -from sugar.graphics.entry import CanvasEntry - -from jarabe.journal.keepicon import KeepIcon -from jarabe.journal.palettes import ObjectPalette, BuddyPalette -from jarabe.journal import misc -from jarabe.journal import model - -class BuddyIcon(CanvasIcon): - def __init__(self, buddy, **kwargs): - CanvasIcon.__init__(self, **kwargs) - self._buddy = buddy - - def create_palette(self): - return BuddyPalette(self._buddy) - -class BuddyList(hippo.CanvasBox): - def __init__(self, buddies, width): - hippo.CanvasBox.__init__(self, - orientation=hippo.ORIENTATION_HORIZONTAL, - box_width=width, - xalign=hippo.ALIGNMENT_START) - self.set_buddies(buddies) - - def set_buddies(self, buddies): - for item in self.get_children(): - self.remove(item) - - for buddy in buddies[0:3]: - nick_, color = buddy - icon = BuddyIcon(buddy, - icon_name='computer-xo', - xo_color=XoColor(color), - cache=True) - self.append(icon) - -class EntryIcon(CanvasIcon): - - __gtype_name__ = 'EntryIcon' - - __gsignals__ = { - 'detail-clicked': (gobject.SIGNAL_RUN_FIRST, - gobject.TYPE_NONE, - ([])) - } - - def __init__(self, **kwargs): - CanvasIcon.__init__(self, **kwargs) - self._metadata = None - - def set_metadata(self, metadata): - self._metadata = metadata - self.props.file_name = misc.get_icon_name(metadata) - self.palette = None - - def create_palette(self): - if self.show_palette: - palette = ObjectPalette(self._metadata, detail=True) - palette.connect('detail-clicked', - self.__detail_clicked_cb) - return palette - else: - return None - - def __detail_clicked_cb(self, event): - self.emit('detail-clicked') - - show_palette = gobject.property(type=bool, default=False) - -class BaseCollapsedEntry(hippo.CanvasBox): - __gtype_name__ = 'BaseCollapsedEntry' - - _DATE_COL_WIDTH = style.GRID_CELL_SIZE * 3 - _BUDDIES_COL_WIDTH = style.GRID_CELL_SIZE * 3 - _PROGRESS_COL_WIDTH = style.GRID_CELL_SIZE * 5 - - def __init__(self): - hippo.CanvasBox.__init__(self, - spacing=style.DEFAULT_SPACING, - padding_top=style.DEFAULT_PADDING, - padding_bottom=style.DEFAULT_PADDING, - padding_left=style.DEFAULT_PADDING * 2, - padding_right=style.DEFAULT_PADDING * 2, - box_height=style.GRID_CELL_SIZE, - orientation=hippo.ORIENTATION_HORIZONTAL) - - self._metadata = None - self._is_selected = False - - self.keep_icon = self._create_keep_icon() - self.append(self.keep_icon) - - self.icon = self._create_icon() - self.append(self.icon) - - self.title = self._create_title() - self.append(self.title, hippo.PACK_EXPAND) - - self.buddies_list = self._create_buddies_list() - self.append(self.buddies_list) - - self.date = self._create_date() - self.append(self.date) - - # Progress controls - self.progress_bar = self._create_progress_bar() - self.append(self.progress_bar) - - self.cancel_button = self._create_cancel_button() - self.append(self.cancel_button) - - def _create_keep_icon(self): - keep_icon = KeepIcon(False) - keep_icon.connect('button-release-event', - self.__keep_icon_button_release_event_cb) - return keep_icon - - def _create_date(self): - date = hippo.CanvasText(text='', - xalign=hippo.ALIGNMENT_START, - font_desc=style.FONT_NORMAL.get_pango_desc(), - box_width=self._DATE_COL_WIDTH) - return date - - def _create_icon(self): - icon = EntryIcon(size=style.STANDARD_ICON_SIZE, cache=True) - return icon - - def _create_title(self): - # TODO: We'd prefer to ellipsize in the middle - title = hippo.CanvasText(text='', - xalign=hippo.ALIGNMENT_START, - font_desc=style.FONT_BOLD.get_pango_desc(), - size_mode=hippo.CANVAS_SIZE_ELLIPSIZE_END) - return title - - def _create_buddies_list(self): - return BuddyList([], self._BUDDIES_COL_WIDTH) - - def _create_progress_bar(self): - progress_bar = gtk.ProgressBar() - return hippo.CanvasWidget(widget=progress_bar, - yalign=hippo.ALIGNMENT_CENTER, - box_width=self._PROGRESS_COL_WIDTH) - - def _create_cancel_button(self): - button = CanvasIcon(icon_name='activity-stop', - size=style.SMALL_ICON_SIZE, - box_width=style.GRID_CELL_SIZE) - button.connect('button-release-event', - self._cancel_button_release_event_cb) - return button - - def _decode_buddies(self): - if self.metadata.has_key('buddies') and \ - self.metadata['buddies']: - buddies = cjson.decode(self.metadata['buddies']).values() - else: - buddies = [] - return buddies - - def update_visibility(self): - in_process = self.is_in_progress() - - self.buddies_list.set_visible(not in_process) - self.date.set_visible(not in_process) - - self.progress_bar.set_visible(in_process) - self.cancel_button.set_visible(in_process) - - # TODO: determine the appearance of in-progress entries - def _update_color(self): - if self.is_in_progress(): - self.props.background_color = style.COLOR_WHITE.get_int() - else: - self.props.background_color = style.COLOR_WHITE.get_int() - - def is_in_progress(self): - return self.metadata.has_key('progress') and \ - int(self.metadata['progress']) < 100 - - def get_keep(self): - keep = int(self.metadata.get('keep', 0)) - return keep == 1 - - def __keep_icon_button_release_event_cb(self, button, event): - logging.debug('__keep_icon_button_release_event_cb') - metadata = model.get(self._metadata['uid']) - if self.get_keep(): - metadata['keep'] = 0 - else: - metadata['keep'] = 1 - model.write(metadata, update_mtime=False) - - self.keep_icon.props.keep = self.get_keep() - self._update_color() - - return True - - def _cancel_button_release_event_cb(self, button, event): - logging.debug('_cancel_button_release_event_cb') - model.delete(self._metadata['uid']) - return True - - def set_selected(self, is_selected): - self._is_selected = is_selected - self._update_color() - - def set_metadata(self, metadata): - self._metadata = metadata - self._is_selected = False - - self.keep_icon.props.keep = self.get_keep() - - self.date.props.text = misc.get_date(metadata) - - self.icon.set_metadata(metadata) - if misc.is_activity_bundle(metadata): - self.icon.props.fill_color = style.COLOR_TRANSPARENT.get_svg() - self.icon.props.stroke_color = style.COLOR_BUTTON_GREY.get_svg() - else: - if metadata.has_key('icon-color') and \ - metadata['icon-color']: - self.icon.props.xo_color = XoColor( \ - metadata['icon-color']) - else: - self.icon.props.xo_color = None - - if metadata.get('title', ''): - title_text = metadata['title'] - else: - title_text = _('Untitled') - self.title.props.text = title_text - - self.buddies_list.set_buddies(self._decode_buddies()) - - if metadata.has_key('progress'): - self.progress_bar.props.widget.props.fraction = \ - int(metadata['progress']) / 100.0 - - self.update_visibility() - self._update_color() - - def get_metadata(self): - return self._metadata - - metadata = property(get_metadata, set_metadata) - - def update_date(self): - self.date.props.text = misc.get_date(self._metadata) - -class CollapsedEntry(BaseCollapsedEntry): - __gtype_name__ = 'CollapsedEntry' - - __gsignals__ = { - 'detail-clicked': (gobject.SIGNAL_RUN_FIRST, - gobject.TYPE_NONE, - ([])) - } - - def __init__(self): - BaseCollapsedEntry.__init__(self) - - self.icon.props.show_palette = True - self.icon.connect('button-release-event', - self.__icon_button_release_event_cb) - self.icon.connect('detail-clicked', - self.__detail_clicked_palette_cb) - - self.title.connect('button_release_event', - self.__title_button_release_event_cb) - - self._title_entry = self._create_title_entry() - self.insert_after(self._title_entry, self.title, hippo.PACK_EXPAND) - self._title_entry.set_visible(False) - - self._detail_button = self._create_detail_button() - self._detail_button.connect('motion-notify-event', - self.__detail_button_motion_notify_event_cb) - self.append(self._detail_button) - - if gtk.widget_get_default_direction() == gtk.TEXT_DIR_RTL: - self.reverse() - - def _create_title_entry(self): - title_entry = CanvasEntry() - title_entry.set_background(style.COLOR_WHITE.get_html()) - title_entry.props.widget.connect('focus-out-event', - self.__title_entry_focus_out_event_cb) - title_entry.props.widget.connect('activate', - self.__title_entry_activate_cb) - title_entry.connect('key-press-event', - self.__title_entry_key_press_event_cb) - return title_entry - - def _create_detail_button(self): - button = CanvasIcon(icon_name='go-right', - size=style.SMALL_ICON_SIZE, - box_width=style.GRID_CELL_SIZE * 3 / 5, - fill_color=style.COLOR_BUTTON_GREY.get_svg()) - button.connect('button-release-event', - self.__detail_button_release_event_cb) - return button - - def update_visibility(self): - BaseCollapsedEntry.update_visibility(self) - self._detail_button.set_visible(not self.is_in_progress()) - - def set_metadata(self, metadata): - BaseCollapsedEntry.set_metadata(self, metadata) - self._title_entry.props.text = self.title.props.text - - metadata = property(BaseCollapsedEntry.get_metadata, set_metadata) - - def _detail_clicked(self): - if not self.is_in_progress(): - self.emit('detail-clicked') - - def __detail_clicked_palette_cb(self, entry): - self._detail_clicked() - - def __detail_button_release_event_cb(self, button, event): - logging.debug('_detail_button_release_event_cb') - self._detail_clicked() - return True - - def __detail_button_motion_notify_event_cb(self, button, event): - if event.detail == hippo.MOTION_DETAIL_ENTER: - button.props.fill_color = style.COLOR_TOOLBAR_GREY.get_svg() - elif event.detail == hippo.MOTION_DETAIL_LEAVE: - button.props.fill_color = style.COLOR_BUTTON_GREY.get_svg() - - def __icon_button_release_event_cb(self, button, event): - logging.debug('__icon_button_release_event_cb') - misc.resume(self.metadata) - return True - - def __title_button_release_event_cb(self, button, event): - self.title.set_visible(False) - self._title_entry.set_visible(True) - self._title_entry.props.widget.grab_focus() - - def __title_entry_focus_out_event_cb(self, entry, event): - self._apply_title_change(entry.props.text) - - def __title_entry_activate_cb(self, entry): - self._apply_title_change(entry.props.text) - - def __title_entry_key_press_event_cb(self, entry, event): - if event.key == hippo.KEY_ESCAPE: - self._cancel_title_change() - - def _apply_title_change(self, title): - self._title_entry.set_visible(False) - self.title.set_visible(True) - - if title == '': - self._cancel_title_change() - elif self.title.props.text != title: - self.title.props.text = title - - self._metadata = model.get(self._metadata['uid']) - self._metadata['title'] = title - self._metadata['title_set_by_user'] = '1' - model.write(self._metadata, update_mtime=False) - - def _cancel_title_change(self): - self._title_entry.props.text = self.title.props.text - self._title_entry.set_visible(False) - self.title.set_visible(True) - diff --git a/src/jarabe/journal/journalactivity.py b/src/jarabe/journal/journalactivity.py index 4ca751c..a932347 100644 --- a/src/jarabe/journal/journalactivity.py +++ b/src/jarabe/journal/journalactivity.py @@ -133,7 +133,7 @@ class JournalActivity(Window): self._dbus_service = JournalActivityDBusService(self) - self.iconify() + #self.iconify() self._critical_space_alert = None self._check_available_space() @@ -185,8 +185,8 @@ class JournalActivity(Window): if keyname == 'Escape': self.show_main_view() - def __detail_clicked_cb(self, list_view, entry): - self._show_secondary_view(entry.metadata) + def __detail_clicked_cb(self, list_view, object_id): + self._show_secondary_view(object_id) def __clear_clicked_cb(self, list_view): self._main_toolbox.search_toolbar.clear_query() @@ -207,9 +207,8 @@ class JournalActivity(Window): self.set_canvas(self._main_view) self._main_view.show() - def _show_secondary_view(self, metadata): - # Need to get the full set of properties - metadata = model.get(metadata['uid']) + def _show_secondary_view(self, object_id): + metadata = model.get(object_id) try: self._detail_toolbox.entry_toolbar.set_metadata(metadata) except Exception: @@ -233,7 +232,7 @@ class JournalActivity(Window): if metadata is None: return False else: - self._show_secondary_view(metadata) + self._show_secondary_view(object_id) return True def __volume_changed_cb(self, volume_toolbar, mount_point): diff --git a/src/jarabe/journal/listmodel.py b/src/jarabe/journal/listmodel.py new file mode 100644 index 0000000..a5586b7 --- /dev/null +++ b/src/jarabe/journal/listmodel.py @@ -0,0 +1,204 @@ +# Copyright (C) 2009, Tomeu Vizoso +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging + +import cjson +import gobject +import gtk + +from sugar.graphics.xocolor import XoColor +from sugar.graphics import style +from sugar import util + +from jarabe.journal import model +from jarabe.journal import misc + +DS_DBUS_SERVICE = 'org.laptop.sugar.DataStore' +DS_DBUS_INTERFACE = 'org.laptop.sugar.DataStore' +DS_DBUS_PATH = '/org/laptop/sugar/DataStore' + +class ListModel(gtk.GenericTreeModel, gtk.TreeDragSource): + __gtype_name__ = 'JournalListModel' + + __gsignals__ = { + 'ready': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([])), + 'progress': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([])), + } + + COLUMN_UID = 0 + COLUMN_FAVORITE = 1 + COLUMN_ICON = 2 + COLUMN_ICON_COLOR = 3 + COLUMN_TITLE = 4 + COLUMN_DATE = 5 + COLUMN_PROGRESS = 6 + COLUMN_BUDDY_1 = 7 + COLUMN_BUDDY_2 = 8 + COLUMN_BUDDY_3 = 9 + + _COLUMN_TYPES = {COLUMN_UID: str, + COLUMN_FAVORITE: bool, + COLUMN_ICON: str, + COLUMN_ICON_COLOR: object, + COLUMN_TITLE: str, + COLUMN_DATE: str, + COLUMN_PROGRESS: int, + COLUMN_BUDDY_1: object, + COLUMN_BUDDY_3: object, + COLUMN_BUDDY_2: object} + + _PAGE_SIZE = 10 + + def __init__(self, query): + gobject.GObject.__init__(self) + + self._last_requested_index = None + self._cached_row = None + self._result_set = model.find(query, ListModel._PAGE_SIZE) + self._temp_drag_file_path = None + + # HACK: The view will tell us that it is resizing so the model can + # avoid hitting D-Bus and disk. + self.view_is_resizing = False + + self._result_set.ready.connect(self.__result_set_ready_cb) + self._result_set.progress.connect(self.__result_set_progress_cb) + + def __result_set_ready_cb(self, **kwargs): + self.emit('ready') + + def __result_set_progress_cb(self, **kwargs): + self.emit('progress') + + def setup(self): + self._result_set.setup() + + def stop(self): + self._result_set.stop() + + def get_metadata(self, path): + return model.get(self[path][ListModel.COLUMN_UID]) + + def on_get_n_columns(self): + return len(ListModel._COLUMN_TYPES) + + def on_get_column_type(self, index): + return ListModel._COLUMN_TYPES[index] + + def on_iter_n_children(self, iter): + if iter == None: + return self._result_set.length + else: + return 0 + + def on_get_value(self, index, column): + if self.view_is_resizing: + return None + + if index == self._last_requested_index: + return self._cached_row[column] + + if index >= self._result_set.length: + return None + + self._result_set.seek(index) + metadata = self._result_set.read() + + self._last_requested_index = index + self._cached_row = [] + self._cached_row.append(metadata['uid']) + self._cached_row.append(metadata.get('keep', '0') == '1') + self._cached_row.append(misc.get_icon_name(metadata)) + + if misc.is_activity_bundle(metadata): + xo_color = XoColor('%s,%s' % (style.COLOR_BUTTON_GREY.get_svg(), + style.COLOR_TRANSPARENT.get_svg())) + else: + if metadata.get('icon-color', ''): + xo_color = XoColor(metadata['icon-color']) + else: + xo_color = None + self._cached_row.append(xo_color) + + title = gobject.markup_escape_text(metadata.get('title', None)) + self._cached_row.append('<b>%s</b>' % title) + + timestamp = int(metadata.get('timestamp', 0)) + self._cached_row.append(util.timestamp_to_elapsed_string(timestamp)) + + self._cached_row.append(int(metadata.get('progress', 100))) + + if metadata.get('buddies', ''): + buddies = cjson.decode(metadata['buddies']).values() + else: + buddies = [] + + for n in xrange(0, 3): + if buddies: + nick, color = buddies.pop(0) + self._cached_row.append((nick, XoColor(color))) + else: + self._cached_row.append(None) + + return self._cached_row[column] + + def on_iter_nth_child(self, iter, n): + return n + + def on_get_path(self, iter): + return (iter) + + def on_get_iter(self, path): + return path[0] + + def on_iter_next(self, iter): + if iter != None: + if iter >= self._result_set.length - 1: + return None + return iter + 1 + return None + + def on_get_flags(self): + return gtk.TREE_MODEL_ITERS_PERSIST | gtk.TREE_MODEL_LIST_ONLY + + def on_iter_children(self, iter): + return None + + def on_iter_has_child(self, iter): + return False + + def on_iter_parent(self, iter): + return None + + def do_drag_data_get(self, path, selection): + uid = self[path][ListModel.COLUMN_UID] + if selection.target == 'text/uri-list': + # Get hold of a reference so the temp file doesn't get deleted + self._temp_drag_file_path = model.get_file(uid) + logging.debug('putting %r in selection' % self._temp_drag_file_path) + selection.set(selection.target, 8, self._temp_drag_file_path) + return True + elif selection.target == 'journal-object-id': + selection.set(selection.target, 8, uid) + return True + + return False + diff --git a/src/jarabe/journal/listview.py b/src/jarabe/journal/listview.py index 4578853..0d4547e 100644 --- a/src/jarabe/journal/listview.py +++ b/src/jarabe/journal/listview.py @@ -1,4 +1,4 @@ -# Copyright (C) 2007, One Laptop Per Child +# Copyright (C) 2009, Tomeu Vizoso # # 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 @@ -20,28 +20,48 @@ import sys from gettext import gettext as _ import time -import hippo import gobject import gtk -import dbus +import hippo +import gconf +import pango from sugar.graphics import style -from sugar.graphics.icon import CanvasIcon, Icon +from sugar.graphics.icon import CanvasIcon, Icon, CellRendererIcon +from sugar.graphics.xocolor import XoColor +from sugar import util from jarabe.journal.collapsedentry import CollapsedEntry +from jarabe.journal.listmodel import ListModel +from jarabe.journal.palettes import ObjectPalette, BuddyPalette from jarabe.journal import model - -DS_DBUS_SERVICE = 'org.laptop.sugar.DataStore' -DS_DBUS_INTERFACE = 'org.laptop.sugar.DataStore' -DS_DBUS_PATH = '/org/laptop/sugar/DataStore' +from jarabe.journal import misc UPDATE_INTERVAL = 300 -EMPTY_JOURNAL = _("Your Journal is empty") -NO_MATCH = _("No matching entries ") +MESSAGE_EMPTY_JOURNAL = 0 +MESSAGE_NO_MATCH = 1 -class BaseListView(gtk.HBox): - __gtype_name__ = 'BaseListView' +class TreeView(gtk.TreeView): + __gtype_name__ = 'JournalTreeView' + + def __init__(self): + gtk.TreeView.__init__(self) + + 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. + model = self.get_model() + if model is not None: + model.view_is_resizing = True + try: + gtk.TreeView.do_size_request(self, requisition) + finally: + if model is not None: + model.view_is_resizing = False + +class BaseListView(gtk.Bin): + __gtype_name__ = 'JournalBaseListView' __gsignals__ = { 'clear-clicked': (gobject.SIGNAL_RUN_FIRST, @@ -51,57 +71,34 @@ class BaseListView(gtk.HBox): def __init__(self): self._query = {} - self._result_set = None - self._entries = [] - self._page_size = 0 - self._reflow_sid = 0 - self._do_scroll_hid = None + self._model = None self._progress_bar = None self._last_progress_bar_pulse = None - gtk.HBox.__init__(self) - self.set_flags(gtk.HAS_FOCUS|gtk.CAN_FOCUS) - self.connect('key-press-event', self._key_press_event_cb) - - self._box = hippo.CanvasBox( - orientation=hippo.ORIENTATION_VERTICAL, - background_color=style.COLOR_WHITE.get_int()) + gobject.GObject.__init__(self) - self._canvas = hippo.Canvas() - self._canvas.set_root(self._box) + self.connect('destroy', self.__destroy_cb) - self.pack_start(self._canvas) - self._canvas.show() + 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() - self._vadjustment = gtk.Adjustment(value=0, lower=0, upper=0, - step_incr=1, page_incr=0, - page_size=0) - self._vadjustment.connect('value-changed', - self._vadjustment_value_changed_cb) - self._vadjustment.connect('changed', self._vadjustment_changed_cb) + 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() - self._vscrollbar = gtk.VScrollbar(self._vadjustment) - self.pack_end(self._vscrollbar, expand=False, fill=False) - self._vscrollbar.show() - - self.connect('scroll-event', self._scroll_event_cb) - self.connect('destroy', self.__destroy_cb) + self.cell_title = None + self.cell_icon = None + self._add_columns() - # DND stuff - self._temp_file_path = None - self._pressed_button = None - self._press_start_x = None - self._press_start_y = None - self._last_clicked_entry = None - self._canvas.drag_source_set(0, [], 0) - self._canvas.add_events(gtk.gdk.BUTTON_PRESS_MASK | - gtk.gdk.POINTER_MOTION_HINT_MASK) - self._canvas.connect_after("motion_notify_event", - self._canvas_motion_notify_event_cb) - self._canvas.connect("button_press_event", - self._canvas_button_press_event_cb) - self._canvas.connect("drag_end", self._drag_end_cb) - self._canvas.connect("drag_data_get", self._drag_data_get_cb) + 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) # Auto-update stuff self._fully_obscured = True @@ -109,123 +106,160 @@ class BaseListView(gtk.HBox): self._refresh_idle_handler = None self._update_dates_timer = None - bus = dbus.SessionBus() - datastore = dbus.Interface( - bus.get_object(DS_DBUS_SERVICE, DS_DBUS_PATH), DS_DBUS_INTERFACE) - self._datastore_created_handler = \ - datastore.connect_to_signal('Created', - self.__datastore_created_cb) - self._datastore_updated_handler = \ - datastore.connect_to_signal('Updated', - self.__datastore_updated_cb) - - self._datastore_deleted_handler = \ - datastore.connect_to_signal('Deleted', - self.__datastore_deleted_cb) + model.created.connect(self.__model_created_cb) + model.updated.connect(self.__model_updated_cb) + model.deleted.connect(self.__model_deleted_cb) - def __destroy_cb(self, widget): - self._datastore_created_handler.remove() - self._datastore_updated_handler.remove() - self._datastore_deleted_handler.remove() - - def _vadjustment_changed_cb(self, vadjustment): - if vadjustment.props.upper > self._page_size: - self._vscrollbar.show() - else: - self._vscrollbar.hide() - - def _vadjustment_value_changed_cb(self, vadjustment): - if self._do_scroll_hid is None: - self._do_scroll_hid = gobject.idle_add(self._do_scroll) + def __model_created_cb(self, sender, **kwargs): + self._set_dirty() - def _do_scroll(self): - current_position = int(self._vadjustment.props.value) + def __model_updated_cb(self, sender, **kwargs): + self._set_dirty() - self._result_set.seek(current_position) - metadata_list = self._result_set.read(self._page_size) + def __model_deleted_cb(self, sender, **kwargs): + self._set_dirty() - if self._result_set.length != self._vadjustment.props.upper: - self._vadjustment.props.upper = self._result_set.length - self._vadjustment.changed() + def _add_columns(self): + cell_favorite = CellRendererFavorite(self.tree_view) + cell_favorite.connect('activate', self.__favorite_activate_cb) + + column = gtk.TreeViewColumn('') + column.props.sizing = gtk.TREE_VIEW_COLUMN_FIXED + 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.tree_view.append_column(column) + + self.cell_icon = CellRendererActivityIcon(self.tree_view) + self.cell_icon.connect('activate', self.__icon_activate_cb) + + column = gtk.TreeViewColumn('') + column.props.sizing = gtk.TREE_VIEW_COLUMN_FIXED + column.props.fixed_width = self.cell_icon.props.width + 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.tree_view.append_column(column) + + self.cell_title = gtk.CellRendererText() + self.cell_title.props.ellipsize = pango.ELLIPSIZE_MIDDLE + self.cell_title.props.ellipsize_set = True + + column = gtk.TreeViewColumn(_('Title')) + column.props.sizing = gtk.TREE_VIEW_COLUMN_FIXED + column.props.expand = True + column.pack_start(self.cell_title) + column.add_attribute(self.cell_title, 'markup', ListModel.COLUMN_TITLE) + self.tree_view.append_column(column) + + buddies_column = gtk.TreeViewColumn('') + buddies_column.props.sizing = gtk.TREE_VIEW_COLUMN_FIXED + 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.tree_view, + column_index=column_index) + buddies_column.pack_start(cell_icon) + buddies_column.props.fixed_width += cell_icon.props.width + buddies_column.add_attribute(cell_icon, 'buddy', column_index) + + cell_text = gtk.CellRendererText() + cell_text.props.xalign = 1 + + # Measure the required width for a date in the form of "10 hours, 10 + # minutes ago" + timestamp = time.time() - 10 * 60 - 10 * 60 * 60 + date = util.timestamp_to_elapsed_string(timestamp) + date_width = self._get_width_for_string(date) + + self.date_column = gtk.TreeViewColumn(_('Date')) + self.date_column.props.sizing = gtk.TREE_VIEW_COLUMN_FIXED + self.date_column.props.fixed_width = date_width + self.date_column.set_alignment(1) + self.date_column.props.resizable = True + self.date_column.pack_start(cell_text) + self.date_column.add_attribute(cell_text, 'text', ListModel.COLUMN_DATE) + self.tree_view.append_column(self.date_column) + + def _get_width_for_string(self, text): + # Add some extra margin + text = text + 'aaaaa' + + widget = gtk.Label('') + context = widget.get_pango_context() + layout = pango.Layout(context) + layout.set_text(text) + width, height = layout.get_size() + return pango.PIXELS(width) - self._refresh_view(metadata_list) - self._dirty = False + def do_size_allocate(self, allocation): + self.allocation = allocation + self.child.size_allocate(allocation) - self._do_scroll_hid = None - return False + def do_size_request(self, requisition): + requisition.width, requisition.height = self.child.size_request() - def _refresh_view(self, metadata_list): - logging.debug('ListView %r' % self) - # Indicate when the Journal is empty - if len(metadata_list) == 0: - if self._is_query_empty(): - self._show_message(EMPTY_JOURNAL) - else: - self._show_message(NO_MATCH) - return + def __destroy_cb(self, widget): + if self._model is not None: + self._model.destroy() + + def __favorite_set_data_cb(self, column, cell, tree_model, tree_iter): + favorite = self._model[tree_iter][ListModel.COLUMN_FAVORITE] + if favorite: + client = gconf.client_get_default() + color = XoColor(client.get_string('/desktop/sugar/user/color')) + cell.props.xo_color = color + else: + cell.props.stroke_color = style.COLOR_BUTTON_GREY.get_svg() + cell.props.fill_color = style.COLOR_WHITE.get_svg() + + def __favorite_activate_cb(self, cell, path): + row = self._model[path] + metadata = model.get(row[ListModel.COLUMN_UID]) + if metadata['keep'] == 1: + metadata['keep'] = 0 + else: + metadata['keep'] = 1 + model.write(metadata, update_mtime=False) - # Refresh view and create the entries if they don't exist yet. - for i in range(0, self._page_size): - try: - if i < len(metadata_list): - if i >= len(self._entries): - entry = self.create_entry() - self._box.append(entry) - self._entries.append(entry) - entry.metadata = metadata_list[i] - else: - entry = self._entries[i] - entry.metadata = metadata_list[i] - entry.set_visible(True) - elif i < len(self._entries): - entry = self._entries[i] - entry.set_visible(False) - except Exception: - logging.error('Exception while displaying entry:\n' + \ - ''.join(traceback.format_exception(*sys.exc_info()))) - - def create_entry(self): - """ Create a descendant of BaseCollapsedEntry - """ - raise NotImplementedError + def __icon_activate_cb(self, cell, path): + row = self._model[path] + metadata = model.get(row[ListModel.COLUMN_UID]) + misc.resume(metadata) def update_with_query(self, query_dict): logging.debug('ListView.update_with_query') self._query = query_dict - if self._page_size > 0: - self.refresh() + self.refresh() def refresh(self): logging.debug('ListView.refresh query %r' % self._query) self._stop_progress_bar() self._start_progress_bar() - if self._result_set is not None: - self._result_set.stop() - self._result_set = model.find(self._query, self._page_size) - self._result_set.ready.connect(self.__result_set_ready_cb) - self._result_set.progress.connect(self.__result_set_progress_cb) - self._result_set.setup() + if self._model is not None: + self._model.stop() - def __result_set_ready_cb(self, **kwargs): - if kwargs['sender'] != self._result_set: - return + 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, model): self._stop_progress_bar() - self._vadjustment.props.upper = self._result_set.length - self._vadjustment.changed() + # 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) - self._vadjustment.props.value = min(self._vadjustment.props.value, - self._result_set.length - self._page_size) - if self._result_set.length == 0: + if len(model) == 0: if self._is_query_empty(): - self._show_message(EMPTY_JOURNAL) + self._show_message(MESSAGE_EMPTY_JOURNAL) else: - self._show_message(NO_MATCH) + self._show_message(MESSAGE_NO_MATCH) else: self._clear_message() - self._do_scroll() def _is_query_empty(self): # FIXME: This is a hack, we shouldn't have to update this every time @@ -237,18 +271,16 @@ class BaseListView(gtk.HBox): else: return True - def __result_set_progress_cb(self, **kwargs): + 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): - self.remove(self._canvas) - self.remove(self._vscrollbar) - alignment = gtk.Alignment(xalign=0.5, yalign=0.5, xscale=0.5) - self.pack_start(alignment) + self.remove(self.child) + self.add(alignment) alignment.show() self._progress_bar = gtk.ProgressBar() @@ -258,102 +290,23 @@ class BaseListView(gtk.HBox): self._progress_bar.show() def _stop_progress_bar(self): - for widget in self.get_children(): - self.remove(widget) - self._progress_bar = None - - self.pack_start(self._canvas) - self.pack_end(self._vscrollbar, expand=False, fill=False) - - def _scroll_event_cb(self, hbox, event): - if event.direction == gtk.gdk.SCROLL_UP: - if self._vadjustment.props.value > self._vadjustment.props.lower: - self._vadjustment.props.value -= 1 - elif event.direction == gtk.gdk.SCROLL_DOWN: - max_value = self._result_set.length - self._page_size - if self._vadjustment.props.value < max_value: - self._vadjustment.props.value += 1 - - def do_focus(self, direction): - if not self.is_focus(): - self.grab_focus() - return True - return False - - def _key_press_event_cb(self, widget, event): - keyname = gtk.gdk.keyval_name(event.keyval) - - if keyname == 'Up': - if self._vadjustment.props.value > self._vadjustment.props.lower: - self._vadjustment.props.value -= 1 - elif keyname == 'Down': - max_value = self._result_set.length - self._page_size - if self._vadjustment.props.value < max_value: - self._vadjustment.props.value += 1 - elif keyname == 'Page_Up' or keyname == 'KP_Page_Up': - new_position = max(0, - self._vadjustment.props.value - self._page_size) - if new_position != self._vadjustment.props.value: - self._vadjustment.props.value = new_position - elif keyname == 'Page_Down' or keyname == 'KP_Page_Down': - new_position = min(self._result_set.length - self._page_size, - self._vadjustment.props.value + self._page_size) - if new_position != self._vadjustment.props.value: - self._vadjustment.props.value = new_position - elif keyname == 'Home' or keyname == 'KP_Home': - new_position = 0 - if new_position != self._vadjustment.props.value: - self._vadjustment.props.value = new_position - elif keyname == 'End' or keyname == 'KP_End': - new_position = max(0, self._result_set.length - self._page_size) - if new_position != self._vadjustment.props.value: - self._vadjustment.props.value = new_position - else: - return False - - return True - - def do_size_allocate(self, allocation): - gtk.HBox.do_size_allocate(self, allocation) - new_page_size = int(allocation.height / style.GRID_CELL_SIZE) - - logging.debug("do_size_allocate: %r" % new_page_size) - - if new_page_size != self._page_size: - self._page_size = new_page_size - self._queue_reflow() - - def _queue_reflow(self): - if not self._reflow_sid: - self._reflow_sid = gobject.idle_add(self._reflow_idle_cb) - - def _reflow_idle_cb(self): - self._box.clear() - self._entries = [] - - self._vadjustment.props.page_size = self._page_size - self._vadjustment.props.page_increment = self._page_size - self._vadjustment.changed() - - if self._result_set is not None: - self._result_set.stop() - self._result_set = model.find(self._query, self._page_size) - - max_value = max(0, self._result_set.length - self._page_size) - if self._vadjustment.props.value > max_value: - self._vadjustment.props.value = max_value - else: - self._do_scroll() - - self._reflow_sid = 0 + 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) - self._canvas.set_root(box) + canvas.set_root(box) icon = CanvasIcon(size=style.LARGE_ICON_SIZE, icon_name='activity-journal', @@ -361,141 +314,65 @@ class BaseListView(gtk.HBox): fill_color = style.COLOR_TRANSPARENT.get_svg()) box.append(icon) - text = hippo.CanvasText(text=message, + 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) - 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) + 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._canvas.set_root(self._box) - - # TODO: Dnd methods. This should be merged somehow inside hippo-canvas. - def _canvas_motion_notify_event_cb(self, widget, event): - if not self._pressed_button: - return True - - # if the mouse button is not pressed, no drag should occurr - if not event.state & gtk.gdk.BUTTON1_MASK: - self._pressed_button = None - return True - - logging.debug("motion_notify_event_cb") - - if event.is_hint: - x, y, state_ = event.window.get_pointer() - else: - x = event.x - y = event.y - - if widget.drag_check_threshold(int(self._press_start_x), - int(self._press_start_y), - int(x), - int(y)): - context_ = widget.drag_begin([('text/uri-list', 0, 0), - ('journal-object-id', 0, 0)], - gtk.gdk.ACTION_COPY, - 1, - event) - return True - - def _drag_end_cb(self, widget, drag_context): - logging.debug("drag_end_cb") - self._pressed_button = None - self._press_start_x = None - self._press_start_y = None - self._last_clicked_entry = None - - # Release and delete the temp file - self._temp_file_path = None - - def _drag_data_get_cb(self, widget, context, selection, target_type, - event_time): - logging.debug("drag_data_get_cb: requested target " + selection.target) - - metadata = self._last_clicked_entry.metadata - if selection.target == 'text/uri-list': - # Get hold of a reference so the temp file doesn't get deleted - self._temp_file_path = model.get_file(metadata['uid']) - selection.set(selection.target, 8, self._temp_file_path) - elif selection.target == 'journal-object-id': - selection.set(selection.target, 8, metadata['uid']) - - def _canvas_button_press_event_cb(self, widget, event): - logging.debug("button_press_event_cb") - - if event.button == 1 and event.type == gtk.gdk.BUTTON_PRESS: - self._last_clicked_entry = \ - self._get_entry_at_coords(event.x, event.y) - if self._last_clicked_entry: - self._pressed_button = event.button - self._press_start_x = event.x - self._press_start_y = event.y - - return False - - def _get_entry_at_coords(self, x, y): - for entry in self._box.get_children(): - entry_x, entry_y = entry.get_context().translate_to_widget(entry) - entry_width, entry_height = entry.get_allocation() - - if (x >= entry_x ) and (x <= entry_x + entry_width) and \ - (y >= entry_y ) and (y <= entry_y + entry_height): - return entry - return None + self.remove(self.child) + self.add(self._scrolled_window) + self._scrolled_window.show() def update_dates(self): logging.debug('ListView.update_dates') - for entry in self._entries: - if entry.get_visible(): - entry.update_date() - - def __datastore_created_cb(self, uid): - self._set_dirty() - - def __datastore_updated_cb(self, uid): - self._set_dirty() - - def __datastore_deleted_cb(self, uid): - self._set_dirty() + 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.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._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._schedule_refresh() - - def _schedule_refresh(self): - if self._refresh_idle_handler is None: - logging.debug('Add refresh idle callback') - self._refresh_idle_handler = \ - gobject.idle_add(self.__refresh_idle_cb) - - def __refresh_idle_cb(self): - self.refresh() - if self._refresh_idle_handler is not None: - logging.debug('Remove refresh idle callback') - gobject.source_remove(self._refresh_idle_handler) - self._refresh_idle_handler = None - return False + 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._schedule_refresh() + self.refresh() if self._update_dates_timer is None: logging.debug('Adding date updating timer') self._update_dates_timer = \ @@ -513,7 +390,7 @@ class BaseListView(gtk.HBox): return True class ListView(BaseListView): - __gtype_name__ = 'ListView' + __gtype_name__ = 'JournalListView' __gsignals__ = { 'detail-clicked': (gobject.SIGNAL_RUN_FIRST, @@ -524,11 +401,117 @@ class ListView(BaseListView): def __init__(self): BaseListView.__init__(self) - def create_entry(self): - entry = CollapsedEntry() - entry.connect('detail-clicked', self.__entry_activated_cb) - return entry + self.cell_title.props.editable = True + self.cell_icon.connect('detail-clicked', self.__detail_clicked_cb) + + cell_detail = CellRendererDetail(self.tree_view) + cell_detail.connect('activate', self.__detail_activate_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_activate_cb(self, cell, 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) + +class CellRendererFavorite(CellRendererIcon): + __gtype_name__ = 'JournalCellRendererFavorite' + + def __init__(self, tree_view): + CellRendererIcon.__init__(self, tree_view) + + self.props.width = style.GRID_CELL_SIZE + self.props.height = style.GRID_CELL_SIZE + self.props.size = style.SMALL_ICON_SIZE + self.props.icon_name = 'emblem-favorite' + self.props.mode = gtk.CELL_RENDERER_MODE_ACTIVATABLE + self.props.prelit_stroke_color = style.COLOR_BUTTON_GREY.get_svg() + self.props.prelit_fill_color = style.COLOR_BUTTON_GREY.get_svg() + +class CellRendererDetail(CellRendererIcon): + __gtype_name__ = 'JournalCellRendererDetail' + + def __init__(self, tree_view): + CellRendererIcon.__init__(self, tree_view) + + self.props.width = style.GRID_CELL_SIZE + self.props.height = style.GRID_CELL_SIZE + self.props.size = style.SMALL_ICON_SIZE + self.props.icon_name = 'go-right' + self.props.mode = gtk.CELL_RENDERER_MODE_ACTIVATABLE + self.props.stroke_color = style.COLOR_TRANSPARENT.get_svg() + self.props.fill_color = style.COLOR_BUTTON_GREY.get_svg() + self.props.prelit_stroke_color = style.COLOR_TRANSPARENT.get_svg() + self.props.prelit_fill_color = style.COLOR_BLACK.get_svg() + +class CellRendererActivityIcon(CellRendererIcon): + __gtype_name__ = 'JournalCellRendererActivityIcon' + + __gsignals__ = { + 'detail-clicked': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([str])), + } + + def __init__(self, tree_view): + CellRendererIcon.__init__(self, tree_view) + + self.props.width = style.GRID_CELL_SIZE + self.props.height = style.GRID_CELL_SIZE + self.props.size = style.STANDARD_ICON_SIZE + self.props.mode = gtk.CELL_RENDERER_MODE_ACTIVATABLE + + self.tree_view = tree_view + + def create_palette(self): + tree_model = self.tree_view.get_model() + metadata = tree_model.get_metadata(self.props.palette_invoker.path) + + palette = ObjectPalette(metadata, detail=True) + palette.connect('detail-clicked', + self.__detail_clicked_cb) + return palette + + def __detail_clicked_cb(self, palette, uid): + self.emit('detail-clicked', uid) + +class CellRendererBuddy(CellRendererIcon): + __gtype_name__ = 'JournalCellRendererBuddy' + + def __init__(self, tree_view, column_index): + CellRendererIcon.__init__(self, tree_view) + + self.props.width = style.STANDARD_ICON_SIZE + self.props.height = style.STANDARD_ICON_SIZE + self.props.size = style.STANDARD_ICON_SIZE + self.props.mode = gtk.CELL_RENDERER_MODE_ACTIVATABLE + + self.tree_view = tree_view + self._model_column_index = column_index + + def create_palette(self): + model = self.tree_view.get_model() + row = model[self.props.palette_invoker.path] + + if row[self._model_column_index] is not None: + nick, xo_color = row[self._model_column_index] + return BuddyPalette((nick, xo_color.to_string())) + else: + return None + + def set_buddy(self, buddy): + if buddy is None: + self.props.icon_name = None + else: + nick_, xo_color = buddy + self.props.icon_name = 'computer-xo' + self.props.xo_color = xo_color - def __entry_activated_cb(self, entry): - self.emit('detail-clicked', entry) + buddy = gobject.property(type=object, setter=set_buddy) diff --git a/src/jarabe/journal/model.py b/src/jarabe/journal/model.py index b6d2bde..8f0fa22 100644 --- a/src/jarabe/journal/model.py +++ b/src/jarabe/journal/model.py @@ -41,7 +41,8 @@ PROPERTIES = ['uid', 'title', 'mtime', 'timestamp', 'keep', 'buddies', 'icon-color', 'mime_type', 'progress', 'activity', 'mountpoint', 'activity_id', 'bundle_id'] -PAGES_TO_CACHE = 5 +MIN_PAGES_TO_CACHE = 3 +MAX_PAGES_TO_CACHE = 5 class _Cache(object): @@ -64,11 +65,10 @@ class _Cache(object): self._dict[entry['uid']] = entry def remove_all(self, entries): - entries = entries[:] - for entry in entries: - obj = self._dict[entry['uid']] + for uid in [entry['uid'] for entry in entries]: + obj = self._dict[uid] self._array.remove(obj) - del self._dict[entry['uid']] + del self._dict[uid] def __len__(self): return len(self._array) @@ -83,11 +83,11 @@ class BaseResultSet(object): """Encapsulates the result of a query """ - def __init__(self, query, cache_limit): + def __init__(self, query, page_size): self._total_count = -1 self._position = -1 self._query = query - self._cache_limit = cache_limit + self._page_size = page_size self._offset = 0 self._cache = _Cache() @@ -104,7 +104,7 @@ class BaseResultSet(object): def get_length(self): if self._total_count == -1: query = self._query.copy() - query['limit'] = self._cache_limit + query['limit'] = self._page_size * MIN_PAGES_TO_CACHE entries, self._total_count = self.find(query) self._cache.append_all(entries) self._offset = 0 @@ -118,13 +118,8 @@ class BaseResultSet(object): def seek(self, position): self._position = position - def read(self, max_count): - logging.debug('ResultSet.read position: %r' % self._position) - - if max_count * PAGES_TO_CACHE > self._cache_limit: - raise RuntimeError( - 'max_count (%i) too big for self._cache_limit' - ' (%i).' % (max_count, self._cache_limit)) + def read(self): + #logging.debug('ResultSet.read position: %r' % self._position) if self._position == -1: self.seek(0) @@ -142,31 +137,29 @@ class BaseResultSet(object): last_cached_entry = self._offset + len(self._cache) - if (remaining_forward_entries <= 0 and - remaining_backwards_entries <= 0) or \ - max_count > self._cache_limit: + if remaining_forward_entries <= 0 and remaining_backwards_entries <= 0: # Total cache miss: remake it - offset = max(0, self._position - max_count) + limit = self._page_size * MIN_PAGES_TO_CACHE + offset = max(0, self._position - limit / 2) logging.debug('remaking cache, offset: %r limit: %r' % \ - (offset, max_count * 2)) + (offset, limit)) query = self._query.copy() - query['limit'] = self._cache_limit + query['limit'] = limit query['offset'] = offset entries, self._total_count = self.find(query) - self._cache.remove_all(self._cache) + self._cache.remove_all(self._cache._array) self._cache.append_all(entries) self._offset = offset - - elif remaining_forward_entries < 2 * max_count and \ - last_cached_entry < self._total_count: + + elif remaining_forward_entries <= 0 and remaining_backwards_entries > 0: # Add one page to the end of cache logging.debug('appending one more page, offset: %r' % \ last_cached_entry) query = self._query.copy() - query['limit'] = max_count + query['limit'] = self._page_size query['offset'] = last_cached_entry entries, self._total_count = self.find(query) @@ -174,21 +167,23 @@ class BaseResultSet(object): self._cache.append_all(entries) # apply the cache limit - objects_excess = len(self._cache) - self._cache_limit + cache_limit = self._page_size * MAX_PAGES_TO_CACHE + objects_excess = len(self._cache) - cache_limit if objects_excess > 0: self._offset += objects_excess self._cache.remove_all(self._cache[:objects_excess]) - elif remaining_backwards_entries < 2 * max_count and self._offset > 0: + elif remaining_forward_entries > 0 and \ + remaining_backwards_entries <= 0 and self._offset > 0: # Add one page to the beginning of cache - limit = min(self._offset, max_count) - self._offset = max(0, self._offset - max_count) + limit = min(self._offset, self._page_size) + self._offset = max(0, self._offset - limit) logging.debug('prepending one more page, offset: %r limit: %r' % (self._offset, limit)) query = self._query.copy() - query['limit'] = limit + query['limit'] = self._page_size query['offset'] = self._offset entries, self._total_count = self.find(query) @@ -196,20 +191,19 @@ class BaseResultSet(object): self._cache.prepend_all(entries) # apply the cache limit - objects_excess = len(self._cache) - self._cache_limit + cache_limit = self._page_size * MAX_PAGES_TO_CACHE + objects_excess = len(self._cache) - cache_limit if objects_excess > 0: self._cache.remove_all(self._cache[-objects_excess:]) - else: - logging.debug('cache hit and no need to grow the cache') + #else: + # logging.debug('cache hit and no need to grow the cache') - first_pos = self._position - self._offset - last_pos = self._position - self._offset + max_count - return self._cache[first_pos:last_pos] + return self._cache[self._position - self._offset] class DatastoreResultSet(BaseResultSet): """Encapsulates the result of a query on the datastore """ - def __init__(self, query, cache_limit): + def __init__(self, query, page_size): if query.get('query', '') and not query['query'].startswith('"'): query_text = '' @@ -222,7 +216,7 @@ class DatastoreResultSet(BaseResultSet): query['query'] = query_text - BaseResultSet.__init__(self, query, cache_limit) + BaseResultSet.__init__(self, query, page_size) def find(self, query): entries, total_count = _get_datastore().find(query, PROPERTIES, @@ -236,8 +230,8 @@ class DatastoreResultSet(BaseResultSet): class InplaceResultSet(BaseResultSet): """Encapsulates the result of a query on a mount point """ - def __init__(self, query, cache_limit, mount_point): - BaseResultSet.__init__(self, query, cache_limit) + def __init__(self, query, page_size, mount_point): + BaseResultSet.__init__(self, query, page_size) self._mount_point = mount_point self._file_list = None self._pending_directories = 0 @@ -389,11 +383,10 @@ def find(query, page_size): if mount_points is None or len(mount_points) != 1: raise ValueError('Exactly one mount point must be specified') - cache_limit = page_size * PAGES_TO_CACHE if mount_points[0] == '/': - return DatastoreResultSet(query, cache_limit) + return DatastoreResultSet(query, page_size) else: - return InplaceResultSet(query, cache_limit, mount_points[0]) + return InplaceResultSet(query, page_size, mount_points[0]) def _get_mount_point(path): dir_path = os.path.dirname(path) diff --git a/src/jarabe/journal/palettes.py b/src/jarabe/journal/palettes.py index 2c15591..341a09f 100644 --- a/src/jarabe/journal/palettes.py +++ b/src/jarabe/journal/palettes.py @@ -41,7 +41,7 @@ class ObjectPalette(Palette): __gsignals__ = { 'detail-clicked': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, - ([])) + ([str])), } def __init__(self, metadata, detail=False): @@ -61,7 +61,7 @@ class ObjectPalette(Palette): style.COLOR_TRANSPARENT.get_svg())) if metadata.has_key('title'): - title = metadata['title'] + title = gobject.markup_escape_text(metadata['title']) else: title = _('Untitled') @@ -142,7 +142,7 @@ class ObjectPalette(Palette): model.delete(self._metadata['uid']) def __detail_activate_cb(self, menu_item): - self.emit('detail-clicked') + self.emit('detail-clicked', self._metadata['uid']) def __friend_selected_cb(self, menu_item, buddy): logging.debug('__friend_selected_cb') |