diff options
author | Tomeu Vizoso <tomeu@tomeuvizoso.net> | 2008-11-28 18:42:57 (GMT) |
---|---|---|
committer | Tomeu Vizoso <tomeu@tomeuvizoso.net> | 2008-11-28 18:42:57 (GMT) |
commit | b006cfdd12d5f22ccea35ab4f716350058cdf107 (patch) | |
tree | 5f633159b0d6dfdd3390da42a54576a4c7cb2ba2 /src | |
parent | 5ee998c245a05656b527eacb57cebe124a73dddf (diff) |
First try at restoring removable devices support in the journal
Diffstat (limited to 'src')
-rw-r--r-- | src/jarabe/journal/Makefile.am | 2 | ||||
-rw-r--r-- | src/jarabe/journal/collapsedentry.py | 107 | ||||
-rw-r--r-- | src/jarabe/journal/detailview.py | 41 | ||||
-rw-r--r-- | src/jarabe/journal/expandedentry.py | 86 | ||||
-rw-r--r-- | src/jarabe/journal/journalactivity.py | 95 | ||||
-rw-r--r-- | src/jarabe/journal/journalentrybundle.py | 27 | ||||
-rw-r--r-- | src/jarabe/journal/journaltoolbox.py | 58 | ||||
-rw-r--r-- | src/jarabe/journal/listview.py | 37 | ||||
-rw-r--r-- | src/jarabe/journal/misc.py | 93 | ||||
-rw-r--r-- | src/jarabe/journal/model.py | 325 | ||||
-rw-r--r-- | src/jarabe/journal/objectchooser.py | 5 | ||||
-rw-r--r-- | src/jarabe/journal/palettes.py | 32 | ||||
-rw-r--r-- | src/jarabe/journal/query.py | 266 | ||||
-rw-r--r-- | src/jarabe/journal/volumestoolbar.py | 29 | ||||
-rw-r--r-- | src/jarabe/model/volume.py | 26 |
15 files changed, 640 insertions, 589 deletions
diff --git a/src/jarabe/journal/Makefile.am b/src/jarabe/journal/Makefile.am index a8ef90f..5f66480 100644 --- a/src/jarabe/journal/Makefile.am +++ b/src/jarabe/journal/Makefile.am @@ -11,7 +11,7 @@ sugar_PYTHON = \ listview.py \ misc.py \ modalalert.py \ + model.py \ objectchooser.py \ palettes.py \ - query.py \ volumestoolbar.py diff --git a/src/jarabe/journal/collapsedentry.py b/src/jarabe/journal/collapsedentry.py index c2cc9c8..bf29199 100644 --- a/src/jarabe/journal/collapsedentry.py +++ b/src/jarabe/journal/collapsedentry.py @@ -25,12 +25,12 @@ import cjson from sugar.graphics.icon import CanvasIcon from sugar.graphics.xocolor import XoColor from sugar.graphics import style -from sugar.datastore import datastore 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): @@ -41,18 +41,18 @@ class BuddyIcon(CanvasIcon): return BuddyPalette(self._buddy) class BuddyList(hippo.CanvasBox): - def __init__(self, model, width): + def __init__(self, buddies, width): hippo.CanvasBox.__init__(self, orientation=hippo.ORIENTATION_HORIZONTAL, box_width=width, xalign=hippo.ALIGNMENT_START) - self.set_model(model) + self.set_buddies(buddies) - def set_model(self, model): + def set_buddies(self, buddies): for item in self.get_children(): self.remove(item) - for buddy in model[0:3]: + for buddy in buddies[0:3]: nick_, color = buddy icon = BuddyIcon(buddy, icon_name='computer-xo', @@ -63,16 +63,16 @@ class BuddyList(hippo.CanvasBox): class EntryIcon(CanvasIcon): def __init__(self, **kwargs): CanvasIcon.__init__(self, **kwargs) - self._jobject = None + self._metadata = None - def set_jobject(self, jobject): - self._jobject = jobject - self.props.file_name = misc.get_icon_name(jobject) + 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: - return ObjectPalette(self._jobject) + return ObjectPalette(self._metadata) else: return None @@ -95,7 +95,7 @@ class BaseCollapsedEntry(hippo.CanvasBox): box_height=style.GRID_CELL_SIZE, orientation=hippo.ORIENTATION_HORIZONTAL) - self._jobject = None + self._metadata = None self._is_selected = False self.keep_icon = self._create_keep_icon() @@ -163,9 +163,9 @@ class BaseCollapsedEntry(hippo.CanvasBox): return button def _decode_buddies(self): - if self.jobject.metadata.has_key('buddies') and \ - self.jobject.metadata['buddies']: - buddies = cjson.decode(self._jobject.metadata['buddies']).values() + if self.metadata.has_key('buddies') and \ + self.metadata['buddies']: + buddies = cjson.decode(self.metadata['buddies']).values() else: buddies = [] return buddies @@ -187,24 +187,21 @@ class BaseCollapsedEntry(hippo.CanvasBox): self.props.background_color = style.COLOR_WHITE.get_int() def is_in_progress(self): - return self._jobject.metadata.has_key('progress') and \ - int(self._jobject.metadata['progress']) < 100 + return self.metadata.has_key('progress') and \ + int(self.metadata['progress']) < 100 def get_keep(self): - keep = int(self._jobject.metadata.get('keep', 0)) + 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') - jobject = datastore.get(self._jobject.object_id) - try: - if self.get_keep(): - jobject.metadata['keep'] = 0 - else: - jobject.metadata['keep'] = 1 - datastore.write(jobject, update_mtime=False) - finally: - jobject.destroy() + 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() @@ -213,55 +210,55 @@ class BaseCollapsedEntry(hippo.CanvasBox): def _cancel_button_release_event_cb(self, button, event): logging.debug('_cancel_button_release_event_cb') - datastore.delete(self._jobject.object_id) + model.delete(self._metadata['uid']) return True def set_selected(self, is_selected): self._is_selected = is_selected self._update_color() - def set_jobject(self, jobject): - self._jobject = jobject + 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(jobject) + self.date.props.text = misc.get_date(metadata) - self.icon.set_jobject(jobject) - if jobject.is_activity_bundle(): + 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 jobject.metadata.has_key('icon-color') and \ - jobject.metadata['icon-color']: + if metadata.has_key('icon-color') and \ + metadata['icon-color']: self.icon.props.xo_color = XoColor( \ - jobject.metadata['icon-color']) + metadata['icon-color']) else: self.icon.props.xo_color = None - if jobject.metadata.get('title', ''): - title_text = jobject.metadata['title'] + if metadata.get('title', ''): + title_text = metadata['title'] else: title_text = _('Untitled') self.title.props.text = title_text - self.buddies_list.set_model(self._decode_buddies()) + self.buddies_list.set_buddies(self._decode_buddies()) - if jobject.metadata.has_key('progress'): + if metadata.has_key('progress'): self.progress_bar.props.widget.props.fraction = \ - int(jobject.metadata['progress']) / 100.0 + int(metadata['progress']) / 100.0 self.update_visibility() self._update_color() - def get_jobject(self): - return self._jobject + def get_metadata(self): + return self._metadata - jobject = property(get_jobject, set_jobject) + metadata = property(get_metadata, set_metadata) def update_date(self): - self.date.props.text = misc.get_date(self._jobject) + self.date.props.text = misc.get_date(self._metadata) class CollapsedEntry(BaseCollapsedEntry): __gtype_name__ = 'CollapsedEntry' @@ -318,11 +315,11 @@ class CollapsedEntry(BaseCollapsedEntry): BaseCollapsedEntry.update_visibility(self) self._detail_button.set_visible(not self.is_in_progress()) - def set_jobject(self, jobject): - BaseCollapsedEntry.set_jobject(self, jobject) + def set_metadata(self, metadata): + BaseCollapsedEntry.set_metadata(self, metadata) self._title_entry.props.text = self.title.props.text - jobject = property(BaseCollapsedEntry.get_jobject, set_jobject) + metadata = property(BaseCollapsedEntry.get_metadata, set_metadata) def __detail_button_release_event_cb(self, button, event): logging.debug('_detail_button_release_event_cb') @@ -338,7 +335,7 @@ class CollapsedEntry(BaseCollapsedEntry): def __icon_button_release_event_cb(self, button, event): logging.debug('__icon_button_release_event_cb') - misc.resume(self.jobject) + misc.resume(self.metadata) return True def __title_button_release_event_cb(self, button, event): @@ -364,20 +361,12 @@ class CollapsedEntry(BaseCollapsedEntry): self._cancel_title_change() elif self.title.props.text != title: self.title.props.text = title - self._jobject.metadata['title'] = title - self._jobject.metadata['title_set_by_user'] = '1' - datastore.write(self._jobject, update_mtime=False, - reply_handler=self._datastore_write_cb, - error_handler=self._datastore_write_error_cb) + 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) - def _datastore_write_cb(self): - pass - - def _datastore_write_error_cb(self, error): - logging.error('CollapsedEntry._datastore_write_error_cb: %r' % error) - diff --git a/src/jarabe/journal/detailview.py b/src/jarabe/journal/detailview.py index 5748d6f..363e152 100644 --- a/src/jarabe/journal/detailview.py +++ b/src/jarabe/journal/detailview.py @@ -23,24 +23,19 @@ import hippo from sugar.graphics import style from sugar.graphics.icon import CanvasIcon -from sugar.datastore import datastore from jarabe.journal.expandedentry import ExpandedEntry +from jarabe.journal import model class DetailView(gtk.VBox): __gtype_name__ = 'DetailView' - __gproperties__ = { - 'jobject' : (object, None, None, - gobject.PARAM_READWRITE) - } - __gsignals__ = { 'go-back-clicked': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])) } def __init__(self, **kwargs): - self._jobject = None + self._metadata = None self._expanded_entry = None canvas = hippo.Canvas() @@ -76,29 +71,23 @@ class DetailView(gtk.VBox): self._expanded_entry.remove_all() import gc gc.collect() - if self._jobject: - self._expanded_entry = ExpandedEntry(self._jobject.object_id) - self._root.append(self._expanded_entry, hippo.PACK_EXPAND) + self._expanded_entry = ExpandedEntry(self._metadata) + self._root.append(self._expanded_entry, hippo.PACK_EXPAND) def refresh(self): logging.debug('DetailView.refresh') - if self._jobject: - self._jobject = datastore.get(self._jobject.object_id) - self._update_view() - - def do_set_property(self, pspec, value): - if pspec.name == 'jobject': - self._jobject = value - self._update_view() - else: - raise AssertionError - - def do_get_property(self, pspec): - if pspec.name == 'jobject': - return self._jobject - else: - raise AssertionError + self._metadata = model.get(self._metadata['uid']) + self._update_view() + + def get_metadata(self): + return self._metadata + + def set_metadata(self, metadata): + self._metadata = metadata + self._update_view() + metadata = gobject.property( + type=object, getter=get_metadata, setter=set_metadata) class BackBar(hippo.CanvasBox): def __init__(self): diff --git a/src/jarabe/journal/expandedentry.py b/src/jarabe/journal/expandedentry.py index 9f99d3a..1e0b890 100644 --- a/src/jarabe/journal/expandedentry.py +++ b/src/jarabe/journal/expandedentry.py @@ -28,11 +28,11 @@ from sugar.graphics import style from sugar.graphics.icon import CanvasIcon from sugar.graphics.xocolor import XoColor from sugar.graphics.entry import CanvasEntry -from sugar.datastore import datastore from jarabe.journal.keepicon import KeepIcon from jarabe.journal.palettes import ObjectPalette, BuddyPalette from jarabe.journal import misc +from jarabe.journal import model class Separator(hippo.CanvasBox, hippo.CanvasItem): def __init__(self, orientation): @@ -63,11 +63,11 @@ class CanvasTextView(hippo.CanvasWidget): self.props.widget = scrolled_window class BuddyList(hippo.CanvasBox): - def __init__(self, model): + def __init__(self, buddies): hippo.CanvasBox.__init__(self, xalign=hippo.ALIGNMENT_START, orientation=hippo.ORIENTATION_HORIZONTAL) - for buddy in model: + for buddy in buddies: nick_, color = buddy hbox = hippo.CanvasBox(orientation=hippo.ORIENTATION_HORIZONTAL) icon = CanvasIcon(icon_name='computer-xo', @@ -78,13 +78,13 @@ class BuddyList(hippo.CanvasBox): self.append(hbox) class ExpandedEntry(hippo.CanvasBox): - def __init__(self, object_id): + def __init__(self, metadata): hippo.CanvasBox.__init__(self) self.props.orientation = hippo.ORIENTATION_VERTICAL self.props.background_color = style.COLOR_WHITE.get_int() self.props.padding_top = style.DEFAULT_SPACING * 3 - self._jobject = datastore.get(object_id) + self._metadata = metadata self._update_title_sid = None # Create header @@ -147,33 +147,33 @@ class ExpandedEntry(hippo.CanvasBox): second_column.append(self._buddy_list) def _create_keep_icon(self): - keep = int(self._jobject.metadata.get('keep', 0)) == 1 + keep = int(self._metadata.get('keep', 0)) == 1 keep_icon = KeepIcon(keep) keep_icon.connect('activated', self._keep_icon_activated_cb) return keep_icon def _create_icon(self): - icon = CanvasIcon(file_name=misc.get_icon_name(self._jobject)) + icon = CanvasIcon(file_name=misc.get_icon_name(self._metadata)) icon.connect_after('button-release-event', self._icon_button_release_event_cb) - if self._jobject.is_activity_bundle(): + if misc.is_activity_bundle(self._metadata): icon.props.fill_color = style.COLOR_TRANSPARENT.get_svg() icon.props.stroke_color = style.COLOR_BUTTON_GREY.get_svg() else: - if self._jobject.metadata.has_key('icon-color') and \ - self._jobject.metadata['icon-color']: + if self._metadata.has_key('icon-color') and \ + self._metadata['icon-color']: icon.props.xo_color = XoColor( \ - self._jobject.metadata['icon-color']) + self._metadata['icon-color']) - icon.set_palette(ObjectPalette(self._jobject)) + icon.set_palette(ObjectPalette(self._metadata)) return icon def _create_title(self): title = CanvasEntry() title.set_background(style.COLOR_WHITE.get_html()) - title.props.text = self._jobject.metadata.get('title', _('Untitled')) + title.props.text = self._metadata.get('title', _('Untitled')) title.props.widget.connect('focus-out-event', self._title_focus_out_event_cb) return title @@ -181,7 +181,7 @@ class ExpandedEntry(hippo.CanvasBox): def _create_date(self): date = hippo.CanvasText(xalign=hippo.ALIGNMENT_START, font_desc=style.FONT_NORMAL.get_pango_desc(), - text = misc.get_date(self._jobject)) + text = misc.get_date(self._metadata)) return date def _create_preview(self): @@ -189,16 +189,16 @@ class ExpandedEntry(hippo.CanvasBox): height = style.zoom(240) box = hippo.CanvasBox() - if self._jobject.metadata.has_key('preview') and \ - len(self._jobject.metadata['preview']) > 4: + if self._metadata.has_key('preview') and \ + len(self._metadata['preview']) > 4: - if self._jobject.metadata['preview'][1:4] == 'PNG': - preview_data = self._jobject.metadata['preview'] + if self._metadata['preview'][1:4] == 'PNG': + preview_data = self._metadata['preview'] else: # TODO: We are close to be able to drop this. import base64 preview_data = base64.b64decode( - self._jobject.metadata['preview']) + self._metadata['preview']) png_file = StringIO.StringIO(preview_data) try: @@ -249,9 +249,9 @@ class ExpandedEntry(hippo.CanvasBox): vbox.append(text) - if self._jobject.metadata.has_key('buddies') and \ - self._jobject.metadata['buddies']: - buddies = cjson.decode(self._jobject.metadata['buddies']).values() + if self._metadata.has_key('buddies') and \ + self._metadata['buddies']: + buddies = cjson.decode(self._metadata['buddies']).values() vbox.append(BuddyList(buddies)) return vbox else: @@ -272,7 +272,7 @@ class ExpandedEntry(hippo.CanvasBox): vbox.append(text) - description = self._jobject.metadata.get('description', '') + description = self._metadata.get('description', '') text_view = CanvasTextView(description, box_height=style.GRID_CELL_SIZE * 2) vbox.append(text_view, hippo.PACK_EXPAND) @@ -298,7 +298,7 @@ class ExpandedEntry(hippo.CanvasBox): vbox.append(text) - tags = self._jobject.metadata.get('tags', '') + tags = self._metadata.get('tags', '') text_view = CanvasTextView(tags, box_height=style.GRID_CELL_SIZE * 2) vbox.append(text_view, hippo.PACK_EXPAND) @@ -313,12 +313,6 @@ class ExpandedEntry(hippo.CanvasBox): self._update_title_sid = gobject.timeout_add(1000, self._update_title_cb) - def _datastore_write_cb(self): - pass - - def _datastore_write_error_cb(self, error): - logging.error('ExpandedEntry._datastore_write_error_cb: %r' % error) - def _title_focus_out_event_cb(self, entry, event): self._update_entry() @@ -331,53 +325,51 @@ class ExpandedEntry(hippo.CanvasBox): def _update_entry(self): needs_update = False - old_title = self._jobject.metadata.get('title', None) + old_title = self._metadata.get('title', None) if old_title != self._title.props.text: self._icon.palette.props.primary_text = self._title.props.text - self._jobject.metadata['title'] = self._title.props.text - self._jobject.metadata['title_set_by_user'] = '1' + self._metadata['title'] = self._title.props.text + self._metadata['title_set_by_user'] = '1' needs_update = True - old_tags = self._jobject.metadata.get('tags', None) + old_tags = self._metadata.get('tags', None) new_tags = self._tags.text_view_widget.props.buffer.props.text if old_tags != new_tags: - self._jobject.metadata['tags'] = new_tags + self._metadata['tags'] = new_tags needs_update = True - old_description = self._jobject.metadata.get('description', None) + old_description = self._metadata.get('description', None) new_description = \ self._description.text_view_widget.props.buffer.props.text if old_description != new_description: - self._jobject.metadata['description'] = new_description + self._metadata['description'] = new_description needs_update = True if needs_update: - datastore.write(self._jobject, update_mtime=False, - reply_handler=self._datastore_write_cb, - error_handler=self._datastore_write_error_cb) + model.write(self._metadata, update_mtime=False) self._update_title_sid = None def get_keep(self): - return self._jobject.metadata.has_key('keep') and \ - self._jobject.metadata['keep'] == 1 + return self._metadata.has_key('keep') and \ + self._metadata['keep'] == 1 def _keep_icon_activated_cb(self, keep_icon): if self.get_keep(): - self._jobject.metadata['keep'] = 0 + self._metadata['keep'] = 0 else: - self._jobject.metadata['keep'] = 1 - datastore.write(self._jobject, update_mtime=False) + self._metadata['keep'] = 1 + model.write(self._metadata, update_mtime=False) keep_icon.props.keep = self.get_keep() def _icon_button_release_event_cb(self, button, event): logging.debug('_icon_button_release_event_cb') - misc.resume(self._jobject) + misc.resume(self._metadata) return True def _preview_box_button_release_event_cb(self, button, event): logging.debug('_preview_box_button_release_event_cb') - misc.resume(self._jobject) + misc.resume(self._metadata) return True diff --git a/src/jarabe/journal/journalactivity.py b/src/jarabe/journal/journalactivity.py index 0513382..5ab99ef 100644 --- a/src/jarabe/journal/journalactivity.py +++ b/src/jarabe/journal/journalactivity.py @@ -28,7 +28,6 @@ import os from sugar.graphics.window import Window from sugar.bundle.bundle import ZipExtractException, RegistrationException -from sugar.datastore import datastore from sugar import env from sugar.activity import activityfactory from sugar import wm @@ -42,10 +41,7 @@ from jarabe.journal import misc from jarabe.journal.journalentrybundle import JournalEntryBundle from jarabe.journal.objectchooser import ObjectChooser from jarabe.journal.modalalert import ModalAlert - -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 model J_DBUS_SERVICE = 'org.laptop.Journal' J_DBUS_INTERFACE = 'org.laptop.Journal' @@ -126,6 +122,7 @@ class JournalActivity(Window): self._detail_view = None self._main_toolbox = None self._detail_toolbox = None + self._volumes_toolbar = None self._setup_main_view() self._setup_secondary_view() @@ -139,12 +136,9 @@ class JournalActivity(Window): self.connect('key-press-event', self._key_press_event_cb) self.connect('focus-in-event', self._focus_in_event_cb) - bus = dbus.SessionBus() - data_store = dbus.Interface( - bus.get_object(DS_DBUS_SERVICE, DS_DBUS_PATH), DS_DBUS_INTERFACE) - data_store.connect_to_signal('Created', self.__data_store_created_cb) - data_store.connect_to_signal('Updated', self.__data_store_updated_cb) - data_store.connect_to_signal('Deleted', self.__data_store_deleted_cb) + model.created.connect(self.__model_created_cb) + model.updated.connect(self.__model_updated_cb) + model.deleted.connect(self.__model_deleted_cb) self._dbus_service = JournalActivityDBusService(self) @@ -172,13 +166,14 @@ class JournalActivity(Window): self._main_view.pack_start(self._list_view) self._list_view.show() - volumes_toolbar = VolumesToolbar() - volumes_toolbar.connect('volume-changed', self._volume_changed_cb) - self._main_view.pack_start(volumes_toolbar, expand=False) + self._volumes_toolbar = VolumesToolbar() + self._volumes_toolbar.connect('volume-changed', + self.__volume_changed_cb) + self._main_view.pack_start(self._volumes_toolbar, expand=False) search_toolbar = self._main_toolbox.search_toolbar search_toolbar.connect('query-changed', self._query_changed_cb) - search_toolbar.set_volume_id(datastore.mounts()[0]['id']) + search_toolbar.set_mount_point('/') def _setup_secondary_view(self): self._secondary_view = gtk.VBox() @@ -199,7 +194,7 @@ class JournalActivity(Window): self.show_main_view() def __detail_clicked_cb(self, list_view, entry): - self._show_secondary_view(entry.jobject) + self._show_secondary_view(entry.metadata) def __go_back_clicked_cb(self, detail_view): self.show_main_view() @@ -217,9 +212,9 @@ class JournalActivity(Window): self.set_canvas(self._main_view) self._main_view.show() - def _show_secondary_view(self, jobject): + def _show_secondary_view(self, metadata): try: - self._detail_toolbox.entry_toolbar.set_jobject(jobject) + self._detail_toolbox.entry_toolbar.set_metadata(metadata) except Exception: logging.error('Exception while displaying entry:\n' + \ ''.join(traceback.format_exception(*sys.exc_info()))) @@ -228,7 +223,7 @@ class JournalActivity(Window): self._detail_toolbox.show() try: - self._detail_view.props.jobject = jobject + self._detail_view.props.metadata = metadata except Exception: logging.error('Exception while displaying entry:\n' + \ ''.join(traceback.format_exception(*sys.exc_info()))) @@ -237,52 +232,40 @@ class JournalActivity(Window): self._secondary_view.show() def show_object(self, object_id): - jobject = datastore.get(object_id) - if jobject is None: + metadata = model.get(object_id) + if metadata is None: return False else: - self._show_secondary_view(jobject) + self._show_secondary_view(metadata) return True - def _volume_changed_cb(self, volume_toolbar, volume_id): - logging.debug('Selected volume: %r.' % volume_id) - self._main_toolbox.search_toolbar.set_volume_id(volume_id) + def __volume_changed_cb(self, volume_toolbar, volume): + logging.debug('Selected volume: %r.' % volume.udi) + self._main_toolbox.search_toolbar.set_mount_point(volume.mount_point) self._main_toolbox.set_current_toolbar(0) - def __data_store_created_cb(self, uid): - jobject = datastore.get(uid) - if jobject is None: - return - try: - self._check_for_bundle(jobject) - finally: - jobject.destroy() + def __model_created_cb(self, object_id): + self._check_for_bundle(object_id) self._main_toolbox.search_toolbar.refresh_filters() self._check_available_space() - def __data_store_updated_cb(self, uid): - jobject = datastore.get(uid) - if jobject is None: - return - try: - self._check_for_bundle(jobject) - finally: - jobject.destroy() + def __model_updated_cb(self, object_id): + self._check_for_bundle(object_id) self._check_available_space() - def __data_store_deleted_cb(self, uid): + def __model_deleted_cb(self, object_id): if self.canvas == self._secondary_view and \ - uid == self._detail_view.props.jobject.object_id: + object_id == self._detail_view.props.metadata['uid']: self.show_main_view() def _focus_in_event_cb(self, window, event): self.search_grab_focus() self._list_view.update_dates() - def _check_for_bundle(self, jobject): + def _check_for_bundle(self, object_id): registry = bundleregistry.get_registry() - bundle = misc.get_bundle(jobject) + bundle = misc.get_bundle(object_id) if bundle is None: return @@ -292,11 +275,12 @@ class JournalActivity(Window): registry.install(bundle) except (ZipExtractException, RegistrationException), e: logging.warning('Could not install bundle %s: %r' % - (jobject.file_path, e)) + (bundle.get_path(), e)) return - if jobject.metadata['mime_type'] == JournalEntryBundle.MIME_TYPE: - datastore.delete(jobject.object_id) + metadata = model.get(object_id) + if metadata['mime_type'] == JournalEntryBundle.MIME_TYPE: + model.delete(object_id) def search_grab_focus(self): search_toolbar = self._main_toolbox.search_toolbar @@ -336,7 +320,18 @@ class JournalActivity(Window): self.present() self._critical_space_alert = None + def set_active_volume(self, mount_point): + self._volumes_toolbar.set_active_volume(mount_point) + +_journal = None + +def get_journal(): + global _journal + if _journal is None: + _journal = JournalActivity() + _journal.show() + return _journal + def start(): - journal = JournalActivity() - journal.show() + get_journal() diff --git a/src/jarabe/journal/journalentrybundle.py b/src/jarabe/journal/journalentrybundle.py index b3efe92..5d4086c 100644 --- a/src/jarabe/journal/journalentrybundle.py +++ b/src/jarabe/journal/journalentrybundle.py @@ -19,11 +19,12 @@ import tempfile import shutil import cjson - import dbus -from sugar.datastore import datastore + from sugar.bundle.bundle import Bundle, MalformedBundleException +from jarabe.journal import model + class JournalEntryBundle(Bundle): """A Journal entry bundle @@ -50,20 +51,14 @@ class JournalEntryBundle(Bundle): self._unzip(install_dir) try: metadata = self._read_metadata(bundle_dir) - jobject = datastore.create() - try: - for key, value in metadata.iteritems(): - jobject.metadata[key] = value - - preview = self._read_preview(uid, bundle_dir) - if preview is not None: - jobject.metadata['preview'] = dbus.ByteArray(preview) - - jobject.metadata['uid'] = '' - jobject.file_path = os.path.join(bundle_dir, uid) - datastore.write(jobject) - finally: - jobject.destroy() + metadata['uid'] = '' + + preview = self._read_preview(uid, bundle_dir) + if preview is not None: + metadata['preview'] = dbus.ByteArray(preview) + + file_path = os.path.join(bundle_dir, uid) + model.write(metadata, file_path) finally: shutil.rmtree(bundle_dir, ignore_errors=True) diff --git a/src/jarabe/journal/journaltoolbox.py b/src/jarabe/journal/journaltoolbox.py index 637965f..beda184 100644 --- a/src/jarabe/journal/journaltoolbox.py +++ b/src/jarabe/journal/journaltoolbox.py @@ -33,11 +33,11 @@ from sugar.graphics.xocolor import XoColor from sugar.graphics import iconentry from sugar.graphics import style from sugar import mime -from sugar.datastore import datastore from jarabe.model import bundleregistry from jarabe.model import volume from jarabe.journal import misc +from jarabe.journal import model _AUTOSEARCH_TIMEOUT = 1000 @@ -79,7 +79,7 @@ class SearchToolbar(gtk.Toolbar): def __init__(self): gtk.Toolbar.__init__(self) - self._volume_id = None + self._mount_point = None self._search_entry = iconentry.IconEntry() self._search_entry.set_icon_from_name(iconentry.ICON_ENTRY_PRIMARY, @@ -159,8 +159,8 @@ class SearchToolbar(gtk.Toolbar): def _build_query(self): query = {} - if self._volume_id: - query['mountpoints'] = [self._volume_id] + if self._mount_point: + query['mountpoints'] = [self._mount_point] if self._what_search_combo.props.value: value = self._what_search_combo.props.value generic_type = mime.get_generic_type(value) @@ -238,8 +238,8 @@ class SearchToolbar(gtk.Toolbar): self._search_entry.activate() return False - def set_volume_id(self, volume_id): - self._volume_id = volume_id + def set_mount_point(self, mount_point): + self._mount_point = mount_point new_query = self._build_query() if self._query != new_query: self._query = new_query @@ -257,7 +257,7 @@ class SearchToolbar(gtk.Toolbar): registry = bundleregistry.get_registry() appended_separator = False - for service_name in datastore.get_unique_values('activity'): + for service_name in model.get_unique_values('activity'): activity_info = registry.get_bundle(service_name) if not activity_info is None: if not appended_separator: @@ -310,7 +310,7 @@ class EntryToolbar(gtk.Toolbar): def __init__(self): gtk.Toolbar.__init__(self) - self._jobject = None + self._metadata = None self._resume = ToolButton('activity-start') self._resume.connect('clicked', self._resume_clicked_cb) @@ -340,43 +340,41 @@ class EntryToolbar(gtk.Toolbar): self.add(erase_button) erase_button.show() - def set_jobject(self, jobject): - self._jobject = jobject + def set_metadata(self, metadata): + self._metadata = metadata self._refresh_copy_palette() self._refresh_resume_palette() def _resume_clicked_cb(self, button): - if self._jobject: - misc.resume(self._jobject) + misc.resume(self._metadata) def _copy_clicked_cb(self, button): clipboard = gtk.Clipboard() clipboard.set_with_data([('text/uri-list', 0, 0)], - self._clipboard_get_func_cb, - self._clipboard_clear_func_cb) + self.__clipboard_get_func_cb, + self.__clipboard_clear_func_cb) - def _clipboard_get_func_cb(self, clipboard, selection_data, info, data): - selection_data.set_uris(['file://' + self._jobject.file_path]) + def __clipboard_get_func_cb(self, clipboard, selection_data, info, data): + file_path = model.get_file(self._metadata['uid']) + selection_data.set_uris(['file://' + file_path]) - def _clipboard_clear_func_cb(self, clipboard, data): + def __clipboard_clear_func_cb(self, clipboard, data): + #TODO: should we remove here the temp file created before? pass def _erase_button_clicked_cb(self, button): registry = bundleregistry.get_registry() - if self._jobject: - bundle = misc.get_bundle(self._jobject) - if bundle is not None and registry.is_installed(bundle): - registry.uninstall(bundle) - datastore.delete(self._jobject.object_id) + bundle = misc.get_bundle(self._metadata['uid']) + if bundle is not None and registry.is_installed(bundle): + registry.uninstall(bundle) + model.delete(self._metadata['uid']) def _resume_menu_item_activate_cb(self, menu_item, service_name): - if self._jobject: - misc.resume(self._jobject, service_name) + misc.resume(self._metadata, service_name) def _copy_menu_item_activate_cb(self, menu_item, vol): - if self._jobject: - datastore.copy(self._jobject, vol.id) + model.copy(self._metadata, vol.mount_point) def _refresh_copy_palette(self): palette = self._copy.get_palette() @@ -387,7 +385,7 @@ class EntryToolbar(gtk.Toolbar): volumes_manager = volume.get_volumes_manager() for vol in volumes_manager.get_volumes(): - if self._jobject.metadata['mountpoint'] == vol.id: + if self._metadata['mountpoint'] == vol.mount_point: continue menu_item = MenuItem(vol.name) menu_item.set_image(Icon(icon_name=vol.icon_name, @@ -397,9 +395,9 @@ class EntryToolbar(gtk.Toolbar): vol) palette.menu.append(menu_item) menu_item.show() - + def _refresh_resume_palette(self): - if self._jobject.metadata.get('activity_id', ''): + if self._metadata.get('activity_id', ''): # TRANS: Action label for resuming an activity. self._resume.set_tooltip(_('Resume')) else: @@ -412,7 +410,7 @@ class EntryToolbar(gtk.Toolbar): palette.menu.remove(menu_item) menu_item.destroy() - for activity_info in misc.get_activities(self._jobject): + for activity_info in misc.get_activities(self._metadata): menu_item = MenuItem(activity_info.get_name()) menu_item.set_image(Icon(file=activity_info.get_icon(), icon_size=gtk.ICON_SIZE_MENU)) diff --git a/src/jarabe/journal/listview.py b/src/jarabe/journal/listview.py index befc7f4..a34184a 100644 --- a/src/jarabe/journal/listview.py +++ b/src/jarabe/journal/listview.py @@ -28,7 +28,7 @@ from sugar.graphics import style from sugar.graphics.icon import CanvasIcon from jarabe.journal.collapsedentry import CollapsedEntry -from jarabe.journal import query +from jarabe.journal import model DS_DBUS_SERVICE = 'org.laptop.sugar.DataStore' DS_DBUS_INTERFACE = 'org.laptop.sugar.DataStore' @@ -118,9 +118,6 @@ class BaseListView(gtk.HBox): self._datastore_updated_handler.remove() self._datastore_deleted_handler.remove() - if self._result_set: - self._result_set.destroy() - def _vadjustment_changed_cb(self, vadjustment): if vadjustment.props.upper > self._page_size: self._vscrollbar.show() @@ -141,38 +138,38 @@ class BaseListView(gtk.HBox): self._last_value = value self._result_set.seek(value) - jobjects = self._result_set.read(self._page_size) + metadata_list = self._result_set.read(self._page_size) if self._result_set.length != self._vadjustment.props.upper: self._vadjustment.props.upper = self._result_set.length self._vadjustment.changed() - self._refresh_view(jobjects) + self._refresh_view(metadata_list) self._dirty = False logging.debug('_do_scroll %r %r\n' % (value, (time.time() - t))) return False - def _refresh_view(self, jobjects): + def _refresh_view(self, metadata_list): logging.debug('ListView %r' % self) # Indicate when the Journal is empty - if len(jobjects) == 0: + if len(metadata_list) == 0: self._show_message(EMPTY_JOURNAL) return # Refresh view and create the entries if they don't exist yet. for i in range(0, self._page_size): try: - if i < len(jobjects): + if i < len(metadata_list): if i >= len(self._entries): entry = self.create_entry() self._box.append(entry) self._entries.append(entry) - entry.jobject = jobjects[i] + entry.metadata = metadata_list[i] else: entry = self._entries[i] - entry.jobject = jobjects[i] + entry.metadata = metadata_list[i] entry.set_visible(True) elif i < len(self._entries): entry = self._entries[i] @@ -193,9 +190,8 @@ class BaseListView(gtk.HBox): self.refresh() def refresh(self): - if self._result_set: - self._result_set.destroy() - self._result_set = query.find(self._query) + logging.debug('ListView.refresh query %r' % self._query) + self._result_set = model.find(self._query) self._vadjustment.props.upper = self._result_set.length self._vadjustment.changed() @@ -283,7 +279,7 @@ class BaseListView(gtk.HBox): self._vadjustment.changed() if self._result_set is None: - self._result_set = query.find(self._query) + self._result_set = model.find(self._query) max_value = max(0, self._result_set.length - self._page_size) if self._vadjustment.props.value > max_value: @@ -353,11 +349,13 @@ class BaseListView(gtk.HBox): event_time): logging.debug("drag_data_get_cb: requested target " + selection.target) - jobject = self._last_clicked_entry.jobject + metadata = self._last_clicked_entry.metadata if selection.target == 'text/uri-list': - selection.set(selection.target, 8, jobject.file_path) + #TODO: figure out the best place to get rid of that temp file + file_path = model.get_file(metadata) + selection.set(selection.target, 8, file_path) elif selection.target == 'journal-object-id': - selection.set(selection.target, 8, jobject.object_id) + selection.set(selection.target, 8, metadata['uid']) def _canvas_button_press_event_cb(self, widget, event): logging.debug("button_press_event_cb") @@ -385,7 +383,8 @@ class BaseListView(gtk.HBox): def update_dates(self): logging.debug('ListView.update_dates') for entry in self._entries: - entry.update_date() + if entry.get_visible(): + entry.update_date() def __datastore_created_cb(self, uid): self._set_dirty() diff --git a/src/jarabe/journal/misc.py b/src/jarabe/journal/misc.py index 19322ad..3c9f83a 100644 --- a/src/jarabe/journal/misc.py +++ b/src/jarabe/journal/misc.py @@ -33,6 +33,7 @@ from sugar import util from jarabe.model import bundleregistry from jarabe.journal.journalentrybundle import JournalEntryBundle +from jarabe.journal import model def _get_icon_file_name(icon_name): icon_theme = gtk.icon_theme_get_default() @@ -47,30 +48,31 @@ def _get_icon_file_name(icon_name): _icon_cache = util.LRU(50) -def get_icon_name(jobject): - - cache_key = (jobject.object_id, jobject.metadata.get('timestamp', None)) +def get_icon_name(metadata): + cache_key = (metadata['uid'], metadata.get('timestamp', None)) if cache_key in _icon_cache: return _icon_cache[cache_key] file_name = None - if jobject.is_activity_bundle() and jobject.file_path: + #TODO: figure out the best place to get rid of that temp file + file_path = model.get_file(metadata['uid']) + if is_activity_bundle(metadata) and os.path.exists(file_path): try: - bundle = ActivityBundle(jobject.file_path) + bundle = ActivityBundle(file_path) file_name = bundle.get_icon() except Exception: logging.warning('Could not read bundle:\n' + \ ''.join(traceback.format_exception(*sys.exc_info()))) file_name = _get_icon_file_name('application-octet-stream') - if not file_name and jobject.metadata['activity']: - service_name = jobject.metadata['activity'] + if not file_name and metadata['activity']: + service_name = metadata['activity'] activity_info = bundleregistry.get_registry().get_bundle(service_name) if activity_info: file_name = activity_info.get_icon() - mime_type = jobject.metadata['mime_type'] + mime_type = metadata['mime_type'] if not file_name and mime_type: icon_name = mime.get_mime_icon(mime_type) if icon_name: @@ -83,26 +85,27 @@ def get_icon_name(jobject): return file_name -def get_date(jobject): +def get_date(metadata): """ Convert from a string in iso format to a more human-like format. """ - if jobject.metadata.has_key('timestamp'): - timestamp = float(jobject.metadata['timestamp']) + if metadata.has_key('timestamp'): + timestamp = float(metadata['timestamp']) return util.timestamp_to_elapsed_string(timestamp) - elif jobject.metadata.has_key('mtime'): - ti = time.strptime(jobject.metadata['mtime'], "%Y-%m-%dT%H:%M:%S") + elif metadata.has_key('mtime'): + ti = time.strptime(metadata['mtime'], "%Y-%m-%dT%H:%M:%S") return util.timestamp_to_elapsed_string(time.mktime(ti)) else: return _('No date') -def get_bundle(jobject): +def get_bundle(metadata): try: - if jobject.is_activity_bundle() and jobject.file_path: - return ActivityBundle(jobject.file_path) - elif jobject.is_content_bundle() and jobject.file_path: - return ContentBundle(jobject.file_path) - elif jobject.metadata['mime_type'] == JournalEntryBundle.MIME_TYPE \ - and jobject.file_path: - return JournalEntryBundle(jobject.file_path) + #TODO: figure out the best place to get rid of that temp file + file_path = model.get_file(metadata['uid']) + if is_activity_bundle(metadata) and os.path.exists(file_path): + return ActivityBundle(file_path) + elif is_content_bundle(metadata) and os.path.exists(file_path): + return ContentBundle(file_path) + elif is_journal_bundle(metadata) and os.path.exists(file_path): + return JournalEntryBundle(file_path) else: return None except MalformedBundleException, e: @@ -117,16 +120,16 @@ def _get_activities_for_mime(mime_type): result.extend(registry.get_activities_for_type(parent_mime)) return result -def get_activities(jobject): +def get_activities(metadata): activities = [] - bundle_id = jobject.metadata.get('activity', '') + bundle_id = metadata.get('activity', '') if bundle_id: activity_info = bundleregistry.get_registry().get_bundle(bundle_id) if activity_info: activities.append(activity_info) - mime_type = jobject.metadata.get('mime_type', '') + mime_type = metadata.get('mime_type', '') if mime_type: activities_info = _get_activities_for_mime(mime_type) for activity_info in activities_info: @@ -135,13 +138,15 @@ def get_activities(jobject): return activities -def resume(jobject, bundle_id=None): +def resume(metadata, bundle_id=None): registry = bundleregistry.get_registry() - if jobject.is_activity_bundle() and not bundle_id: + if is_activity_bundle(metadata) and bundle_id is None: logging.debug('Creating activity bundle') - bundle = ActivityBundle(jobject.file_path) + #TODO: figure out the best place to get rid of that temp file + file_path = model.get_file(metadata['uid']) + bundle = ActivityBundle(file_path) if not registry.is_installed(bundle): logging.debug('Installing activity bundle') registry.install(bundle) @@ -158,10 +163,12 @@ def resume(jobject, bundle_id=None): logging.error('Bundle %r is not installed.', bundle.get_bundle_id()) - elif jobject.is_content_bundle() and not bundle_id: + elif is_content_bundle(metadata) and bundle_id is None: logging.debug('Creating content bundle') - bundle = ContentBundle(jobject.file_path) + #TODO: figure out the best place to get rid of that temp file + file_path = model.get_file(metadata['uid']) + bundle = ContentBundle(file_path) if not bundle.is_installed(): logging.debug('Installing content bundle') bundle.install() @@ -178,22 +185,40 @@ def resume(jobject, bundle_id=None): activityfactory.create_with_uri(activity_bundle, bundle.get_start_uri()) else: if bundle_id is None: - activities = get_activities(jobject) + activities = get_activities(metadata) if not activities: logging.warning('No activity can open this object, %s.' % - jobject.metadata.get('mime_type', None)) + metadata.get('mime_type', None)) return bundle_id = activities[0].get_bundle_id() bundle = registry.get_bundle(bundle_id) - activity_id = jobject.metadata['activity_id'] - object_id = jobject.object_id + activity_id = metadata['activity_id'] + + if metadata['mountpoint'] == '/': + object_id = metadata['uid'] + else: + object_id = model.copy(metadata, '/') - if activity_id: + if activity_id is None: handle = ActivityHandle(object_id=object_id, activity_id=activity_id) activityfactory.create(bundle, handle) else: activityfactory.create_with_object_id(bundle, object_id) +def is_activity_bundle(metadata): + return metadata['mime_type'] in \ + [ActivityBundle.MIME_TYPE, ActivityBundle.DEPRECATED_MIME_TYPE] + +def is_content_bundle(metadata): + return metadata['mime_type'] == ContentBundle.MIME_TYPE + +def is_journal_bundle(metadata): + return metadata['mime_type'] == JournalEntryBundle.MIME_TYPE + +def is_bundle(metadata): + return is_activity_bundle(metadata) or is_content_bundle(metadata) or \ + is_journal_bundle(metadata) + diff --git a/src/jarabe/journal/model.py b/src/jarabe/journal/model.py new file mode 100644 index 0000000..4e23533 --- /dev/null +++ b/src/jarabe/journal/model.py @@ -0,0 +1,325 @@ +# Copyright (C) 2007-2008, 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 +import os +from datetime import datetime +import time + +import dbus +import gconf + +from sugar import dispatch +from sugar import mime + +DS_DBUS_SERVICE = 'org.laptop.sugar.DataStore' +DS_DBUS_INTERFACE = 'org.laptop.sugar.DataStore' +DS_DBUS_PATH = '/org/laptop/sugar/DataStore' + +# Properties the journal cares about. +PROPERTIES = ['uid', 'title', 'mtime', 'timestamp', 'keep', 'buddies', + 'icon-color', 'mime_type', 'progress', 'activity', 'mountpoint', + 'activity_id'] + +class _Cache(object): + + __gtype_name__ = 'model_Cache' + + def __init__(self, entries=None): + self._array = [] + self._dict = {} + if entries is not None: + self.append_all(entries) + + def prepend_all(self, entries): + for entry in entries[::-1]: + self._array.insert(0, entry) + self._dict[entry['uid']] = entry + + def append_all(self, entries): + for entry in entries: + self._array.append(entry) + self._dict[entry['uid']] = entry + + def remove_all(self, entries): + entries = entries[:] + for entry in entries: + obj = self._dict[entry['uid']] + self._array.remove(obj) + del self._dict[entry['uid']] + + def __len__(self): + return len(self._array) + + def __getitem__(self, key): + if isinstance(key, basestring): + return self._dict[key] + else: + return self._array[key] + +class ResultSet(object): + """Encapsulates the result of a query + """ + + _CACHE_LIMIT = 80 + + def __init__(self, query): + self._total_count = -1 + self._position = -1 + self._query = query + + self._offset = 0 + self._cache = _Cache() + + def get_length(self): + if self._total_count == -1: + query = self._query.copy() + query['limit'] = ResultSet._CACHE_LIMIT + entries, self._total_count = self._find(query) + self._cache.append_all(entries) + self._offset = 0 + return self._total_count + + length = property(get_length) + + def _find(self, query): + mount_points = query.get('mountpoints', ['/']) + if mount_points is None or len(mount_points) != 1: + raise ValueError('Exactly one mount point must be specified') + if mount_points[0] == '/': + return _get_datastore().find(query, PROPERTIES, byte_arrays=True) + else: + return _query_mount_point(mount_points[0], query) + + def seek(self, position): + self._position = position + + def read(self, max_count): + logging.debug('ResultSet.read position: %r' % self._position) + + if max_count * 5 > ResultSet._CACHE_LIMIT: + raise RuntimeError( + 'max_count (%i) too big for ResultSet._CACHE_LIMIT' + ' (%i).' % (max_count, ResultSet._CACHE_LIMIT)) + + if self._position == -1: + self.seek(0) + + if self._position < self._offset: + remaining_forward_entries = 0 + else: + remaining_forward_entries = self._offset + len(self._cache) - \ + self._position + + if self._position > self._offset + len(self._cache): + remaining_backwards_entries = 0 + else: + remaining_backwards_entries = self._position - self._offset + + last_cached_entry = self._offset + len(self._cache) + + if (remaining_forward_entries <= 0 and + remaining_backwards_entries <= 0) or \ + max_count > ResultSet._CACHE_LIMIT: + + # Total cache miss: remake it + offset = max(0, self._position - max_count) + logging.debug('remaking cache, offset: %r limit: %r' % \ + (offset, max_count * 2)) + query = self._query.copy() + query['limit'] = ResultSet._CACHE_LIMIT + query['offset'] = offset + entries, self._total_count = self._find(query) + + self._cache.remove_all(self._cache) + self._cache.append_all(entries) + self._offset = offset + + elif remaining_forward_entries < 2 * max_count and \ + last_cached_entry < self._total_count: + + # 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['offset'] = last_cached_entry + entries, self._total_count = self._find(query) + + # update cache + self._cache.append_all(entries) + + # apply the cache limit + objects_excess = len(self._cache) - ResultSet._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: + + # Add one page to the beginning of cache + limit = min(self._offset, max_count) + self._offset = max(0, self._offset - max_count) + + logging.debug('prepending one more page, offset: %r limit: %r' % + (self._offset, limit)) + query = self._query.copy() + query['limit'] = limit + query['offset'] = self._offset + entries, self._total_count = self._find(query) + + # update cache + self._cache.prepend_all(entries) + + # apply the cache limit + objects_excess = len(self._cache) - ResultSet._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') + + first_pos = self._position - self._offset + last_pos = self._position - self._offset + max_count + return self._cache[first_pos:last_pos] + +def _get_file_metadata(path): + stat = os.stat(path) + client = gconf.client_get_default() + return {'uid': path, + 'title': os.path.basename(path), + 'timestamp': stat.st_mtime, + 'mime_type': mime.get_for_file(path), + 'activity': '', + 'activity_id': '', + 'icon-color': client.get_string('/desktop/sugar/user/color')} + +def _get_all_files(dir_path, mount_point): + files = [] + for entry in os.listdir(dir_path): + full_path = os.path.join(dir_path, entry) + if os.path.isdir(full_path): + files.extend(_get_all_files(full_path, mount_point)) + elif os.path.isfile(full_path): + metadata = _get_file_metadata(full_path) + metadata['mountpoint'] = mount_point + files.append(metadata) + return files + +def _query_mount_point(mount_point, query): + t = time.time() + + files = _get_all_files(mount_point, mount_point) + offset = int(query.get('offset', 0)) + limit = int(query.get('limit', len(files))) + result = files[offset:offset + limit], len(files) + + logging.debug('_query_mount_point took %f s.' % (time.time() - t)) + + return result + +_datastore = None +def _get_datastore(): + global _datastore + if _datastore is None: + bus = dbus.SessionBus() + remote_object = bus.get_object(DS_DBUS_SERVICE, DS_DBUS_PATH) + _datastore = dbus.Interface(remote_object, DS_DBUS_INTERFACE) + + return _datastore + +def find(query): + """Returns a ResultSet + """ + if 'order_by' not in query: + query['order_by'] = ['-mtime'] + return ResultSet(query) + +def _get_mount_point(path): + dir_path = os.path.dirname(path) + while True: + if os.path.ismount(dir_path): + return dir_path + else: + dir_path = dir_path.rsplit(os.sep, 1)[0] + +def get(object_id): + """Returns the metadata for an object + """ + if os.path.exists(object_id): + metadata = _get_file_metadata(object_id) + metadata['mountpoint'] = _get_mount_point(object_id) + else: + metadata = _get_datastore().get_properties(object_id, byte_arrays=True) + metadata['mountpoint'] = '/' + return metadata + +def get_file(object_id): + """Returns the file for an object + """ + if os.path.exists(object_id): + return object_id + else: + return _get_datastore().get_filename(object_id) + +def get_unique_values(key): + """Returns a list with the different values a property has taken + """ + return [] + +def delete(object_id): + """Removes an object from persistent storage + """ + pass + +def copy(metadata, mount_point): + """Copies an object to another mount point + """ + metadata = get(metadata['uid']) + + #TODO: figure out the best place to get rid of that temp file + file_path = get_file(metadata['uid']) + + metadata['mountpoint'] = mount_point + del metadata['uid'] + + return write(metadata, file_path) + +def write(metadata, file_path='', update_mtime=True): + """Creates or updates an entry for that id + """ + if update_mtime: + metadata['mtime'] = datetime.now().isoformat() + metadata['timestamp'] = int(time.time()) + + if metadata['mountpoint'] == '/': + if metadata.get('uid', ''): + object_id = _get_datastore().update(metadata['uid'], + dbus.Dictionary(metadata), + file_path, + True) + else: + object_id = _get_datastore().create(dbus.Dictionary(metadata), + file_path, + True) + else: + pass + + return object_id + +created = dispatch.Signal() +updated = dispatch.Signal() +deleted = dispatch.Signal() + diff --git a/src/jarabe/journal/objectchooser.py b/src/jarabe/journal/objectchooser.py index 947141d..fcaed55 100644 --- a/src/jarabe/journal/objectchooser.py +++ b/src/jarabe/journal/objectchooser.py @@ -23,7 +23,6 @@ import hippo from sugar.graphics import style from sugar.graphics.toolbutton import ToolButton -from sugar.datastore import datastore from jarabe.journal.listview import ListView from jarabe.journal.collapsedentry import BaseCollapsedEntry @@ -84,7 +83,7 @@ class ObjectChooser(gtk.Window): vbox.pack_start(self._list_view) self._list_view.show() - self._toolbar.set_volume_id(datastore.mounts()[0]['id']) + self._toolbar.set_mount_point('/') width = gtk.gdk.screen_width() - style.GRID_CELL_SIZE * 2 height = gtk.gdk.screen_height() - style.GRID_CELL_SIZE * 2 @@ -95,7 +94,7 @@ class ObjectChooser(gtk.Window): # TODO: Should we disconnect the signal here? def __entry_activated_cb(self, list_view, entry): - self._selected_object_id = entry.jobject.object_id + self._selected_object_id = entry.metadata['uid'] self.emit('response', gtk.RESPONSE_ACCEPT) def __delete_event_cb(self, chooser, event): diff --git a/src/jarabe/journal/palettes.py b/src/jarabe/journal/palettes.py index 9ca1190..1f41032 100644 --- a/src/jarabe/journal/palettes.py +++ b/src/jarabe/journal/palettes.py @@ -24,37 +24,37 @@ from sugar.graphics import style from sugar.graphics.palette import Palette from sugar.graphics.menuitem import MenuItem from sugar.graphics.icon import Icon -from sugar.datastore import datastore from sugar.graphics.xocolor import XoColor from jarabe.model import bundleregistry from jarabe.journal import misc +from jarabe.journal import model class ObjectPalette(Palette): - def __init__(self, jobject): + def __init__(self, metadata): - self._jobject = jobject + self._metadata = metadata activity_icon = Icon(icon_size=gtk.ICON_SIZE_LARGE_TOOLBAR) - activity_icon.props.file = misc.get_icon_name(jobject) - if jobject.metadata.has_key('icon-color') and \ - jobject.metadata['icon-color']: + activity_icon.props.file = misc.get_icon_name(metadata) + if metadata.has_key('icon-color') and \ + metadata['icon-color']: activity_icon.props.xo_color = \ - XoColor(jobject.metadata['icon-color']) + XoColor(metadata['icon-color']) else: activity_icon.props.xo_color = \ XoColor('%s,%s' % (style.COLOR_BUTTON_GREY.get_svg(), style.COLOR_TRANSPARENT.get_svg())) - if jobject.metadata.has_key('title'): - title = jobject.metadata['title'] + if metadata.has_key('title'): + title = metadata['title'] else: title = _('Untitled') Palette.__init__(self, primary_text=title, icon=activity_icon) - if jobject.metadata.get('activity_id', ''): + if metadata.get('activity_id', ''): resume_label = _('Resume') else: resume_label = _('Start') @@ -81,7 +81,7 @@ class ObjectPalette(Palette): menu_item.show() def __start_activate_cb(self, menu_item): - misc.resume(self._jobject) + misc.resume(self._metadata) def __copy_activate_cb(self, menu_item): clipboard = gtk.Clipboard() @@ -90,19 +90,21 @@ class ObjectPalette(Palette): self.__clipboard_clear_func_cb) def __clipboard_get_func_cb(self, clipboard, selection_data, info, data): - logging.debug('__clipboard_get_func_cb %r' % self._jobject.file_path) - selection_data.set_uris(['file://' + self._jobject.file_path]) + file_path = model.get_file(self._metadata['uid']) + logging.debug('__clipboard_get_func_cb %r' % file_path) + selection_data.set_uris(['file://' + file_path]) def __clipboard_clear_func_cb(self, clipboard, data): + #TODO: should we remove here the temp file created before? pass def __erase_activate_cb(self, menu_item): registry = bundleregistry.get_registry() - bundle = misc.get_bundle(self._jobject) + bundle = misc.get_bundle(self._metadata) if bundle is not None and registry.is_installed(bundle): registry.uninstall(bundle) - datastore.delete(self._jobject.object_id) + model.delete(self._metadata['uid']) class BuddyPalette(Palette): def __init__(self, buddy): diff --git a/src/jarabe/journal/query.py b/src/jarabe/journal/query.py deleted file mode 100644 index 04d9b16..0000000 --- a/src/jarabe/journal/query.py +++ /dev/null @@ -1,266 +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 sugar.datastore import datastore - -# Properties the journal cares about. -PROPERTIES = ['uid', 'title', 'mtime', 'timestamp', 'keep', 'buddies', - 'icon-color', 'mime_type', 'progress', 'activity', 'mountpoint', - 'activity_id'] - -class _Cache(object): - - __gtype_name__ = 'query_Cache' - - def __init__(self, jobjects=None): - self._array = [] - self._dict = {} - if jobjects is not None: - self.append_all(jobjects) - - def prepend_all(self, jobjects): - for jobject in jobjects[::-1]: - self._array.insert(0, jobject) - self._dict[jobject.object_id] = jobject - - def append_all(self, jobjects): - for jobject in jobjects: - self._array.append(jobject) - self._dict[jobject.object_id] = jobject - - def remove_all(self, jobjects): - jobjects = jobjects[:] - for jobject in jobjects: - obj = self._dict[jobject.object_id] - self._array.remove(obj) - del self._dict[obj.object_id] - obj.destroy() - - def __len__(self): - return len(self._array) - - def __getitem__(self, key): - if isinstance(key, basestring): - return self._dict[key] - else: - return self._array[key] - - def destroy(self): - self._destroy_jobjects(self._array) - self._array = [] - self._dict = {} - - def _destroy_jobjects(self, jobjects): - for jobject in jobjects: - jobject.destroy() - -class ResultSet(object): - - _CACHE_LIMIT = 80 - - def __init__(self, query, sorting): - self._total_count = -1 - self._position = -1 - self._query = query - self._sorting = sorting - - self._offset = 0 - self._cache = _Cache() - - def destroy(self): - self._cache.destroy() - - def get_length(self): - if self._total_count == -1: - jobjects, self._total_count = datastore.find(self._query, - sorting=self._sorting, - limit=ResultSet._CACHE_LIMIT, - properties=PROPERTIES) - self._cache.append_all(jobjects) - self._offset = 0 - return self._total_count - - length = property(get_length) - - def seek(self, position): - self._position = position - - def read(self, max_count): - logging.debug('ResultSet.read position: %r' % self._position) - - if max_count * 5 > ResultSet._CACHE_LIMIT: - raise RuntimeError( - 'max_count (%i) too big for ResultSet._CACHE_LIMIT' - ' (%i).' % (max_count, ResultSet._CACHE_LIMIT)) - - if self._position == -1: - self.seek(0) - - if self._position < self._offset: - remaining_forward_entries = 0 - else: - remaining_forward_entries = self._offset + len(self._cache) - \ - self._position - - if self._position > self._offset + len(self._cache): - remaining_backwards_entries = 0 - else: - remaining_backwards_entries = self._position - self._offset - - last_cached_entry = self._offset + len(self._cache) - - if (remaining_forward_entries <= 0 and - remaining_backwards_entries <= 0) or \ - max_count > ResultSet._CACHE_LIMIT: - - # Total cache miss: remake it - offset = max(0, self._position - max_count) - logging.debug('remaking cache, offset: %r limit: %r' % \ - (offset, max_count * 2)) - jobjects, self._total_count = datastore.find(self._query, - sorting=self._sorting, - offset=offset, - limit=ResultSet._CACHE_LIMIT, - properties=PROPERTIES) - - self._cache.remove_all(self._cache) - self._cache.append_all(jobjects) - self._offset = offset - - elif remaining_forward_entries < 2 * max_count and \ - last_cached_entry < self._total_count: - - # Add one page to the end of cache - logging.debug('appending one more page, offset: %r' % \ - last_cached_entry) - jobjects, self._total_count = datastore.find(self._query, - sorting=self._sorting, - offset=last_cached_entry, - limit=max_count, - properties=PROPERTIES) - # update cache - self._cache.append_all(jobjects) - - # apply the cache limit - objects_excess = len(self._cache) - ResultSet._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: - - # Add one page to the beginning of cache - limit = min(self._offset, max_count) - self._offset = max(0, self._offset - max_count) - - logging.debug('prepending one more page, offset: %r limit: %r' % - (self._offset, limit)) - jobjects, self._total_count = datastore.find(self._query, - sorting=self._sorting, - offset=self._offset, - limit=limit, - properties=PROPERTIES) - - # update cache - self._cache.prepend_all(jobjects) - - # apply the cache limit - objects_excess = len(self._cache) - ResultSet._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') - - first_pos = self._position - self._offset - last_pos = self._position - self._offset + max_count - return self._cache[first_pos:last_pos] - -def find(query, sorting=None): - if sorting is None: - sorting = ['-mtime'] - result_set = ResultSet(query, sorting) - return result_set - -def test(): - TOTAL_ITEMS = 1000 - SCREEN_SIZE = 10 - - def mock_debug(string): - print "\tDEBUG: %s" % string - logging.debug = mock_debug - - def mock_find(query, sorting=None, limit=None, offset=None, - properties=None): - if properties is None: - properties = [] - - print "mock_find %r %r" % (offset, (offset + limit)) - - if limit is None or offset is None: - raise RuntimeError("Unimplemented test.") - - result = [] - for index in range(offset, offset + limit): - obj = datastore.DSObject(index, datastore.DSMetadata({}), '') - result.append(obj) - - return result, TOTAL_ITEMS - datastore.find = mock_find - - result_set = find({}) - - print "Get first page" - objects = result_set.read(SCREEN_SIZE) - print [obj.object_id for obj in objects] - assert range(0, SCREEN_SIZE) == [obj.object_id for obj in objects] - print "" - - print "Scroll to 5th item" - result_set.seek(5) - objects = result_set.read(SCREEN_SIZE) - print [obj.object_id for obj in objects] - assert range(5, SCREEN_SIZE + 5) == [obj.object_id for obj in objects] - print "" - - print "Scroll back to beginning" - result_set.seek(0) - objects = result_set.read(SCREEN_SIZE) - print [obj.object_id for obj in objects] - assert range(0, SCREEN_SIZE) == [obj.object_id for obj in objects] - print "" - - print "Hit PgDn five times" - for i in range(0, 5): - result_set.seek((i + 1) * SCREEN_SIZE) - objects = result_set.read(SCREEN_SIZE) - print [obj.object_id for obj in objects] - assert range((i + 1) * SCREEN_SIZE, (i + 2) * SCREEN_SIZE) == \ - [obj.object_id for obj in objects] - print "" - - print "Hit PgUp five times" - for i in range(0, 5)[::-1]: - result_set.seek(i * SCREEN_SIZE) - objects = result_set.read(SCREEN_SIZE) - print [obj.object_id for obj in objects] - assert range(i * SCREEN_SIZE, (i + 1) * SCREEN_SIZE) == \ - [obj.object_id for obj in objects] - print "" - -if __name__ == "__main__": - test() diff --git a/src/jarabe/journal/volumestoolbar.py b/src/jarabe/journal/volumestoolbar.py index b29f325..50b9aa7 100644 --- a/src/jarabe/journal/volumestoolbar.py +++ b/src/jarabe/journal/volumestoolbar.py @@ -20,11 +20,11 @@ from gettext import gettext as _ import gobject import gtk -from sugar.datastore import datastore from sugar.graphics.radiotoolbutton import RadioToolButton from sugar.graphics.palette import Palette from jarabe.model import volume +from jarabe.journal import model class VolumesToolbar(gtk.Toolbar): __gtype_name__ = 'VolumesToolbar' @@ -32,7 +32,7 @@ class VolumesToolbar(gtk.Toolbar): __gsignals__ = { 'volume-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, - ([str])) + ([object])) } def __init__(self): @@ -43,9 +43,7 @@ class VolumesToolbar(gtk.Toolbar): self.connect('destroy', self.__destroy_cb) - # TODO: It's unclear now how removable devices will be handled in the - # Journal. Disable for now. - #gobject.idle_add(self._set_up_volumes) + gobject.idle_add(self._set_up_volumes) def __destroy_cb(self, widget): volumes_manager = volume.get_volumes_manager() @@ -91,7 +89,7 @@ class VolumesToolbar(gtk.Toolbar): self._volume_buttons.append(button) - if vol.can_unmount: + if vol.can_eject: menu_item = gtk.MenuItem(_('Unmount')) menu_item.connect('activate', self._unmount_activated_cb, vol) palette.menu.append(menu_item) @@ -102,7 +100,7 @@ class VolumesToolbar(gtk.Toolbar): def _button_toggled_cb(self, button, vol): if button.props.active: - self.emit('volume-changed', vol.id) + self.emit('volume-changed', vol) def _unmount_activated_cb(self, menu_item, vol): logging.debug('VolumesToolbar._unmount_activated_cb: %r', vol.udi) @@ -110,7 +108,7 @@ class VolumesToolbar(gtk.Toolbar): def _remove_button(self, vol): for button in self.get_children(): - if button.volume.id == vol.id: + if button.volume.udi == vol.udi: self._volume_buttons.remove(button) self.remove(button) self.get_children()[0].props.active = True @@ -118,6 +116,15 @@ class VolumesToolbar(gtk.Toolbar): if len(self.get_children()) < 2: self.hide() return + logging.error('Couldnt find volume with udi %r' % vol.udi) + + def set_active_volume(self, mount_point): + for button in self.get_children(): + logging.error('udi %r' % button.volume.mount_point) + if button.volume.mount_point == mount_point: + button.props.active = True + return + logging.error('Couldnt find volume with mount_point %r' % mount_point) class VolumeButton(RadioToolButton): def __init__(self, vol, group): @@ -134,5 +141,7 @@ class VolumeButton(RadioToolButton): def _drag_data_received_cb(self, widget, drag_context, x, y, selection_data, info, timestamp): - jobject = datastore.get(selection_data.data) - datastore.copy(jobject, self.volume.id) + object_id = selection_data.data + metadata = model.get(object_id) + model.copy(metadata, self.volume.mount_point) + diff --git a/src/jarabe/model/volume.py b/src/jarabe/model/volume.py index 6afa6a6..0d3d9a5 100644 --- a/src/jarabe/model/volume.py +++ b/src/jarabe/model/volume.py @@ -84,17 +84,6 @@ class VolumesManager(gobject.GObject): # Ignore volumes without a filesystem. if device.GetProperty('volume.fsusage') != 'filesystem': return False - # Ignore root. - if device.GetProperty('volume.mount_point') == '/': - return False - - storage_udi = device.GetProperty('block.storage_device') - obj = bus.get_object(HAL_SERVICE_NAME, storage_udi) - storage_device = dbus.Interface(obj, HAL_DEVICE_IFACE) - - # Ignore non-removable storage. - if not storage_device.GetProperty('storage.hotpluggable'): - return False return True @@ -188,6 +177,7 @@ class VolumesManager(gobject.GObject): self._remove_volume(udi) def _add_volume(self, udi): + logging.debug('_add_volume %r' % udi) bus = dbus.SystemBus() device_object = bus.get_object(HAL_SERVICE_NAME, udi) device = dbus.Interface(device_object, HAL_DEVICE_IFACE) @@ -198,11 +188,17 @@ class VolumesManager(gobject.GObject): mount_point = device.GetProperty('volume.mount_point') + storage_udi = device.GetProperty('block.storage_device') + obj = bus.get_object(HAL_SERVICE_NAME, storage_udi) + storage_device = dbus.Interface(obj, HAL_DEVICE_IFACE) + can_eject = storage_device.GetProperty('storage.hotpluggable') + volume = Volume(volume_name, self._get_icon_for_volume(device), profile.get_color(), udi, - mount_point) + mount_point, + can_eject) self._volumes[udi] = volume logging.debug('mounted volume %s' % udi) @@ -227,16 +223,20 @@ class VolumesManager(gobject.GObject): storage_drive_type = storage_device.GetProperty('storage.drive_type') if storage_drive_type == 'sd_mmc': return 'media-flash-sd-mmc' + elif device.GetProperty('volume.mount_point') == '/': + return 'computer-xo' else: return 'media-flash-usb' class Volume(object): - def __init__(self, name, icon_name, icon_color, udi, mount_point): + def __init__(self, name, icon_name, icon_color, udi, mount_point, + can_eject): self.name = name self.icon_name = icon_name self.icon_color = icon_color self.udi = udi self.mount_point = mount_point + self.can_eject = can_eject def unmount(self): logging.debug('Volumes.unmount: %r', self.udi) |