Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--configure.ac1
-rw-r--r--data/icons/Makefile.am1
-rw-r--r--data/icons/activity-journal.svg11
-rw-r--r--src/Makefile.am2
-rw-r--r--src/journal/Makefile.am18
-rw-r--r--src/journal/__init__.py0
-rw-r--r--src/journal/collapsedentry.py385
-rw-r--r--src/journal/detailview.py133
-rw-r--r--src/journal/expandedentry.py412
-rw-r--r--src/journal/journalactivity.py338
-rw-r--r--src/journal/journalentrybundle.py96
-rw-r--r--src/journal/journaltoolbox.py418
-rw-r--r--src/journal/keepicon.py57
-rw-r--r--src/journal/listview.py460
-rw-r--r--src/journal/misc.py109
-rw-r--r--src/journal/modalalert.py93
-rw-r--r--src/journal/objectchooser.py199
-rw-r--r--src/journal/palettes.py113
-rw-r--r--src/journal/query.py266
-rw-r--r--src/journal/volumesmanager.py315
-rw-r--r--src/journal/volumestoolbar.py137
-rw-r--r--src/model/homeactivity.py18
-rw-r--r--src/model/homemodel.py6
-rw-r--r--src/view/Shell.py10
-rw-r--r--src/view/frame/activitiestray.py17
-rw-r--r--src/view/keyhandler.py2
26 files changed, 3591 insertions, 26 deletions
diff --git a/configure.ac b/configure.ac
index 9c58b60..3af0587 100644
--- a/configure.ac
+++ b/configure.ac
@@ -51,6 +51,7 @@ src/controlpanel/Makefile
src/controlpanel/model/Makefile
src/controlpanel/view/Makefile
src/intro/Makefile
+src/journal/Makefile
src/hardware/Makefile
src/view/Makefile
src/view/devices/Makefile
diff --git a/data/icons/Makefile.am b/data/icons/Makefile.am
index 8209ca3..ac26247 100644
--- a/data/icons/Makefile.am
+++ b/data/icons/Makefile.am
@@ -1,6 +1,7 @@
sugardir = $(pkgdatadir)/data/icons
sugar_DATA = \
+ activity-journal.svg \
module-about_me.svg \
module-about_my_xo.svg \
module-date_and_time.svg \
diff --git a/data/icons/activity-journal.svg b/data/icons/activity-journal.svg
new file mode 100644
index 0000000..1ae35db
--- /dev/null
+++ b/data/icons/activity-journal.svg
@@ -0,0 +1,11 @@
+<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd' [
+ <!ENTITY stroke_color "#666666">
+ <!ENTITY fill_color "#ffffff">
+]><svg enable-background="new 0 0 55 55" height="55px" id="Layer_1" version="1.1" viewBox="0 0 55 55" width="55px" x="0px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" y="0px"><g display="block" id="activity-journal">
+ <path d="M45.866,44.669 c0,2.511-1.528,4.331-4.332,4.331H12.077V6h29.458c2.15,0,4.332,2.154,4.332,4.33L45.866,44.669L45.866,44.669z" fill="&fill_color;" stroke="&stroke_color;" stroke-linecap="round" stroke-linejoin="round" stroke-width="3.5"/>
+
+ <line fill="none" stroke="&stroke_color;" stroke-linecap="round" stroke-linejoin="round" stroke-width="3.5" x1="21.341" x2="21.341" y1="6.121" y2="48.881"/>
+ <path d="M7.384,14.464 c0,0,2.084,0.695,4.17,0.695c2.086,0,4.173-0.695,4.173-0.695" fill="none" stroke="&stroke_color;" stroke-linecap="round" stroke-linejoin="round" stroke-width="3.5"/>
+ <path d="M7.384,28.021 c0,0,1.912,0.695,4.345,0.695s3.999-0.695,3.999-0.695" fill="none" stroke="&stroke_color;" stroke-linecap="round" stroke-linejoin="round" stroke-width="3.5"/>
+ <path d="M7.384,41.232 c0,0,1.736,0.695,4.518,0.695c2.781,0,3.825-0.695,3.825-0.695" fill="none" stroke="&stroke_color;" stroke-linecap="round" stroke-linejoin="round" stroke-width="3.5"/>
+</g></svg> \ No newline at end of file
diff --git a/src/Makefile.am b/src/Makefile.am
index 48fb84f..e1da7ce 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -1,4 +1,4 @@
-SUBDIRS = controlpanel hardware model view intro
+SUBDIRS = controlpanel hardware journal model view intro
sugardir = $(pkgdatadir)/shell
sugar_PYTHON = \
diff --git a/src/journal/Makefile.am b/src/journal/Makefile.am
new file mode 100644
index 0000000..f9944e6
--- /dev/null
+++ b/src/journal/Makefile.am
@@ -0,0 +1,18 @@
+sugardir = $(pkgdatadir)/shell/journal
+sugar_PYTHON = \
+ __init__.py \
+ collapsedentry.py \
+ detailview.py \
+ expandedentry.py \
+ journalactivity.py \
+ journalentrybundle.py \
+ journaltoolbox.py \
+ keepicon.py \
+ listview.py \
+ misc.py \
+ modalalert.py \
+ objectchooser.py \
+ palettes.py \
+ query.py \
+ volumesmanager.py \
+ volumestoolbar.py
diff --git a/src/journal/__init__.py b/src/journal/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/journal/__init__.py
diff --git a/src/journal/collapsedentry.py b/src/journal/collapsedentry.py
new file mode 100644
index 0000000..c69960a
--- /dev/null
+++ b/src/journal/collapsedentry.py
@@ -0,0 +1,385 @@
+# 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 json
+
+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 journal.keepicon import KeepIcon
+from journal.palettes import ObjectPalette, BuddyPalette
+from journal import misc
+
+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, model, width):
+ hippo.CanvasBox.__init__(self,
+ orientation=hippo.ORIENTATION_HORIZONTAL,
+ box_width=width,
+ xalign=hippo.ALIGNMENT_START)
+ self.set_model(model)
+
+ def set_model(self, model):
+ for item in self.get_children():
+ self.remove(item)
+
+ for buddy in model[0:3]:
+ nick_, color = buddy
+ icon = BuddyIcon(buddy,
+ icon_name='computer-xo',
+ xo_color=XoColor(color),
+ cache=True)
+ self.append(icon)
+
+class EntryIcon(CanvasIcon):
+ def __init__(self, **kwargs):
+ CanvasIcon.__init__(self, **kwargs)
+ self._jobject = None
+
+ def set_jobject(self, jobject):
+ self._jobject = jobject
+ self.props.file_name = misc.get_icon_name(jobject)
+ self.palette = None
+
+ def create_palette(self):
+ if self.show_palette:
+ return ObjectPalette(self._jobject)
+ else:
+ return None
+
+ 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._jobject = 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.jobject.metadata.has_key('buddies') and \
+ self.jobject.metadata['buddies']:
+ # json cannot read unicode strings
+ buddies_str = self.jobject.metadata['buddies'].encode('utf8')
+ buddies = json.read(buddies_str).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._jobject.metadata.has_key('progress') and \
+ int(self._jobject.metadata['progress']) < 100
+
+ def get_keep(self):
+ keep = int(self._jobject.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()
+
+ 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')
+ datastore.delete(self._jobject.object_id)
+ return True
+
+ def set_selected(self, is_selected):
+ self._is_selected = is_selected
+ self._update_color()
+
+ def set_jobject(self, jobject):
+ self._jobject = jobject
+ self._is_selected = False
+
+ self.keep_icon.props.keep = self.get_keep()
+
+ self.date.props.text = misc.get_date(jobject)
+
+ self.icon.set_jobject(jobject)
+ if jobject.is_activity_bundle():
+ 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']:
+ self.icon.props.xo_color = XoColor( \
+ jobject.metadata['icon-color'])
+ else:
+ self.icon.props.xo_color = None
+
+ if jobject.metadata.get('title', ''):
+ title_text = jobject.metadata['title']
+ else:
+ title_text = _('Untitled')
+ self.title.props.text = title_text
+
+ self.buddies_list.set_model(self._decode_buddies())
+
+ if jobject.metadata.has_key('progress'):
+ self.progress_bar.props.widget.props.fraction = \
+ int(jobject.metadata['progress']) / 100.0
+
+ self.update_visibility()
+ self._update_color()
+
+ def get_jobject(self):
+ return self._jobject
+
+ jobject = property(get_jobject, set_jobject)
+
+ def update_date(self):
+ self.date.props.text = misc.get_date(self._jobject)
+
+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.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_jobject(self, jobject):
+ BaseCollapsedEntry.set_jobject(self, jobject)
+ self._title_entry.props.text = self.title.props.text
+
+ jobject = property(BaseCollapsedEntry.get_jobject, set_jobject)
+
+ def __detail_button_release_event_cb(self, button, event):
+ logging.debug('_detail_button_release_event_cb')
+ if not self.is_in_progress():
+ self.emit('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')
+ self.jobject.resume()
+ 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._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)
+
+ 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/journal/detailview.py b/src/journal/detailview.py
new file mode 100644
index 0000000..3e9a721
--- /dev/null
+++ b/src/journal/detailview.py
@@ -0,0 +1,133 @@
+# 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
+
+from sugar.graphics import style
+from sugar.graphics.icon import CanvasIcon
+from sugar.datastore import datastore
+
+from journal.expandedentry import ExpandedEntry
+
+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._expanded_entry = None
+
+ canvas = hippo.Canvas()
+
+ self._root = hippo.CanvasBox()
+ self._root.props.background_color = style.COLOR_PANEL_GREY.get_int()
+ canvas.set_root(self._root)
+
+ back_bar = BackBar()
+ back_bar.connect('button-release-event',
+ self.__back_bar_release_event_cb)
+ self._root.append(back_bar)
+
+ gobject.GObject.__init__(self, **kwargs)
+
+ self.pack_start(canvas)
+ canvas.show()
+
+ def _fav_icon_activated_cb(self, fav_icon):
+ keep = not self._expanded_entry.get_keep()
+ self._expanded_entry.set_keep(keep)
+ fav_icon.props.keep = keep
+
+ def __back_bar_release_event_cb(self, back_bar, event):
+ self.emit('go-back-clicked')
+ return False
+
+ def _update_view(self):
+ if self._expanded_entry:
+ self._root.remove(self._expanded_entry)
+
+ # Work around pygobject bug #479227
+ 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)
+
+ 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
+
+
+class BackBar(hippo.CanvasBox):
+ def __init__(self):
+ hippo.CanvasBox.__init__(self,
+ orientation=hippo.ORIENTATION_HORIZONTAL,
+ border=style.LINE_WIDTH,
+ background_color=style.COLOR_PANEL_GREY.get_int(),
+ border_color=style.COLOR_SELECTION_GREY.get_int(),
+ padding=style.DEFAULT_PADDING,
+ padding_left=style.DEFAULT_SPACING,
+ spacing=style.DEFAULT_SPACING)
+
+ icon = CanvasIcon(icon_name='go-previous',
+ size=style.SMALL_ICON_SIZE,
+ fill_color=style.COLOR_TOOLBAR_GREY.get_svg())
+ self.append(icon)
+
+ label = hippo.CanvasText(text=_('Back'),
+ font_desc=style.FONT_NORMAL.get_pango_desc())
+ self.append(label)
+
+ if gtk.widget_get_default_direction() == gtk.TEXT_DIR_RTL:
+ self.reverse()
+
+ self.connect('motion-notify-event', self.__motion_notify_event_cb)
+
+ def __motion_notify_event_cb(self, box, event):
+ if event.detail == hippo.MOTION_DETAIL_ENTER:
+ box.props.background_color = style.COLOR_SELECTION_GREY.get_int()
+ elif event.detail == hippo.MOTION_DETAIL_LEAVE:
+ box.props.background_color = style.COLOR_PANEL_GREY.get_int()
+ return False
diff --git a/src/journal/expandedentry.py b/src/journal/expandedentry.py
new file mode 100644
index 0000000..fd591fa
--- /dev/null
+++ b/src/journal/expandedentry.py
@@ -0,0 +1,412 @@
+# 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 StringIO
+
+import hippo
+import cairo
+import gobject
+import gtk
+import json
+
+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 journal.keepicon import KeepIcon
+from journal.palettes import ObjectPalette, BuddyPalette
+from journal import misc
+
+class Separator(hippo.CanvasBox, hippo.CanvasItem):
+ def __init__(self, orientation):
+ hippo.CanvasBox.__init__(self,
+ background_color=style.COLOR_PANEL_GREY.get_int())
+
+ if orientation == hippo.ORIENTATION_VERTICAL:
+ self.props.box_width = style.LINE_WIDTH
+ else:
+ self.props.box_height = style.LINE_WIDTH
+
+class CanvasTextView(hippo.CanvasWidget):
+ def __init__(self, text, **kwargs):
+ hippo.CanvasWidget.__init__(self, **kwargs)
+ self.text_view_widget = gtk.TextView()
+ self.text_view_widget.props.buffer.props.text = text
+ self.text_view_widget.props.left_margin = style.DEFAULT_SPACING
+ self.text_view_widget.props.right_margin = style.DEFAULT_SPACING
+ self.text_view_widget.props.wrap_mode = gtk.WRAP_WORD
+ self.text_view_widget.show()
+
+ # TODO: These fields should expand vertically instead of scrolling
+ scrolled_window = gtk.ScrolledWindow()
+ scrolled_window.set_shadow_type(gtk.SHADOW_OUT)
+ scrolled_window.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
+ scrolled_window.add(self.text_view_widget)
+
+ self.props.widget = scrolled_window
+
+class BuddyList(hippo.CanvasBox):
+ def __init__(self, model):
+ hippo.CanvasBox.__init__(self, xalign=hippo.ALIGNMENT_START,
+ orientation=hippo.ORIENTATION_HORIZONTAL)
+
+ for buddy in model:
+ nick_, color = buddy
+ hbox = hippo.CanvasBox(orientation=hippo.ORIENTATION_HORIZONTAL)
+ icon = CanvasIcon(icon_name='computer-xo',
+ xo_color=XoColor(color),
+ size=style.STANDARD_ICON_SIZE)
+ icon.set_palette(BuddyPalette(buddy))
+ hbox.append(icon)
+ self.append(hbox)
+
+class ExpandedEntry(hippo.CanvasBox):
+ def __init__(self, object_id):
+ 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._update_title_sid = None
+
+ # Create header
+ header = hippo.CanvasBox(orientation=hippo.ORIENTATION_HORIZONTAL,
+ padding=style.DEFAULT_PADDING,
+ padding_right=style.GRID_CELL_SIZE,
+ spacing=style.DEFAULT_SPACING)
+ self.append(header)
+
+ # Create two column body
+
+ body = hippo.CanvasBox(orientation=hippo.ORIENTATION_HORIZONTAL,
+ spacing=style.DEFAULT_SPACING * 3,
+ padding_left=style.GRID_CELL_SIZE,
+ padding_right=style.GRID_CELL_SIZE,
+ padding_top=style.DEFAULT_SPACING * 3)
+
+ self.append(body, hippo.PACK_EXPAND)
+
+ first_column = hippo.CanvasBox(orientation=hippo.ORIENTATION_VERTICAL,
+ spacing=style.DEFAULT_SPACING)
+ body.append(first_column)
+
+ second_column = hippo.CanvasBox(orientation=hippo.ORIENTATION_VERTICAL,
+ spacing=style.DEFAULT_SPACING)
+ body.append(second_column, hippo.PACK_EXPAND)
+
+ # Header
+
+ self._keep_icon = self._create_keep_icon()
+ header.append(self._keep_icon)
+
+ self._icon = self._create_icon()
+ header.append(self._icon)
+
+ self._title = self._create_title()
+ header.append(self._title, hippo.PACK_EXPAND)
+
+ # TODO: create a version list popup instead of a date label
+ self._date = self._create_date()
+ header.append(self._date)
+
+ if gtk.widget_get_default_direction() == gtk.TEXT_DIR_RTL:
+ header.reverse()
+
+ # First column
+
+ self._preview = self._create_preview()
+ first_column.append(self._preview)
+
+ # Second column
+
+ description_box, self._description = self._create_description()
+ second_column.append(description_box)
+
+ tags_box, self._tags = self._create_tags()
+ second_column.append(tags_box)
+
+ self._buddy_list = self._create_buddy_list()
+ second_column.append(self._buddy_list)
+
+ def _create_keep_icon(self):
+ keep = int(self._jobject.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.connect_after('button-release-event',
+ self._icon_button_release_event_cb)
+
+ if self._jobject.is_activity_bundle():
+ 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']:
+ icon.props.xo_color = XoColor( \
+ self._jobject.metadata['icon-color'])
+
+ icon.set_palette(ObjectPalette(self._jobject))
+
+ 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.widget.connect('focus-out-event',
+ self._title_focus_out_event_cb)
+ return title
+
+ 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))
+ return date
+
+ def _create_preview(self):
+ width = style.zoom(320)
+ height = style.zoom(240)
+ box = hippo.CanvasBox()
+
+ if self._jobject.metadata.has_key('preview') and \
+ len(self._jobject.metadata['preview']) > 4:
+
+ if self._jobject.metadata['preview'][1:4] == 'PNG':
+ preview_data = self._jobject.metadata['preview']
+ else:
+ # TODO: We are close to be able to drop this.
+ import base64
+ preview_data = base64.b64decode(
+ self._jobject.metadata['preview'])
+
+ png_file = StringIO.StringIO(preview_data)
+ try:
+ surface = cairo.ImageSurface.create_from_png(png_file)
+ has_preview = True
+ except Exception, e:
+ logging.error('Error while loading the preview: %r' % e)
+ has_preview = False
+ else:
+ has_preview = False
+
+ if has_preview:
+ preview_box = hippo.CanvasImage(image=surface,
+ border=style.LINE_WIDTH,
+ border_color=style.COLOR_BUTTON_GREY.get_int(),
+ xalign=hippo.ALIGNMENT_CENTER,
+ yalign=hippo.ALIGNMENT_CENTER,
+ scale_width=width,
+ scale_height=height)
+ else:
+ preview_box = hippo.CanvasText(text=_('No preview'),
+ font_desc=style.FONT_NORMAL.get_pango_desc(),
+ xalign=hippo.ALIGNMENT_CENTER,
+ yalign=hippo.ALIGNMENT_CENTER,
+ border=style.LINE_WIDTH,
+ border_color=style.COLOR_BUTTON_GREY.get_int(),
+ color=style.COLOR_BUTTON_GREY.get_int(),
+ box_width=width,
+ box_height=height)
+ preview_box.connect_after('button-release-event',
+ self._preview_box_button_release_event_cb)
+ box.append(preview_box)
+ return box
+
+ def _create_buddy_list(self):
+
+ vbox = hippo.CanvasBox()
+ vbox.props.spacing = style.DEFAULT_SPACING
+
+ text = hippo.CanvasText(text=_('Participants:'),
+ font_desc=style.FONT_NORMAL.get_pango_desc())
+ text.props.color = style.COLOR_BUTTON_GREY.get_int()
+
+ if gtk.widget_get_default_direction() == gtk.TEXT_DIR_RTL:
+ text.props.xalign = hippo.ALIGNMENT_END
+ else:
+ text.props.xalign = hippo.ALIGNMENT_START
+
+ vbox.append(text)
+
+ if self._jobject.metadata.has_key('buddies') and \
+ self._jobject.metadata['buddies']:
+ # json cannot read unicode strings
+ buddies_str = self._jobject.metadata['buddies'].encode('utf8')
+ buddies = json.read(buddies_str).values()
+ vbox.append(BuddyList(buddies))
+ return vbox
+ else:
+ return vbox
+
+ def _create_description(self):
+ vbox = hippo.CanvasBox()
+ vbox.props.spacing = style.DEFAULT_SPACING
+
+ text = hippo.CanvasText(text=_('Description:'),
+ font_desc=style.FONT_NORMAL.get_pango_desc())
+ text.props.color = style.COLOR_BUTTON_GREY.get_int()
+
+ if gtk.widget_get_default_direction() == gtk.TEXT_DIR_RTL:
+ text.props.xalign = hippo.ALIGNMENT_END
+ else:
+ text.props.xalign = hippo.ALIGNMENT_START
+
+ vbox.append(text)
+
+ description = self._jobject.metadata.get('description', '')
+ text_view = CanvasTextView(description,
+ box_height=style.GRID_CELL_SIZE * 2)
+ vbox.append(text_view, hippo.PACK_EXPAND)
+
+ text_view.text_view_widget.props.accepts_tab = False
+ text_view.text_view_widget.connect('focus-out-event',
+ self._description_focus_out_event_cb)
+
+ return vbox, text_view
+
+ def _create_tags(self):
+ vbox = hippo.CanvasBox()
+ vbox.props.spacing = style.DEFAULT_SPACING
+
+ text = hippo.CanvasText(text=_('Tags:'),
+ font_desc=style.FONT_NORMAL.get_pango_desc())
+ text.props.color = style.COLOR_BUTTON_GREY.get_int()
+
+ if gtk.widget_get_default_direction() == gtk.TEXT_DIR_RTL:
+ text.props.xalign = hippo.ALIGNMENT_END
+ else:
+ text.props.xalign = hippo.ALIGNMENT_START
+
+ vbox.append(text)
+
+ tags = self._jobject.metadata.get('tags', '')
+ text_view = CanvasTextView(tags, box_height=style.GRID_CELL_SIZE * 2)
+ vbox.append(text_view, hippo.PACK_EXPAND)
+
+ text_view.text_view_widget.props.accepts_tab = False
+ text_view.text_view_widget.connect('focus-out-event',
+ self._tags_focus_out_event_cb)
+
+ return vbox, text_view
+
+ def _create_version_list(self):
+ vbox = hippo.CanvasBox()
+ vbox.props.spacing = style.DEFAULT_SPACING
+ # TODO: Enable again when we have versions in the DS
+ """
+ jobjects, count = datastore.find({'uid': self._jobject.object_id},
+ sorting=['-mtime'])
+ for jobject in jobjects:
+ hbox = hippo.CanvasBox(orientation=hippo.ORIENTATION_HORIZONTAL)
+ hbox.props.spacing = style.DEFAULT_SPACING
+
+ icon = CanvasIcon(file_name=misc.get_icon_name(jobject),
+ size=style.SMALL_ICON_SIZE)
+ if jobject.metadata.has_key('icon-color') and \
+ jobject.metadata['icon-color']:
+ icon.props.xo_color = XoColor(jobject.metadata['icon-color'])
+ hbox.append(icon)
+
+ date = hippo.CanvasText(text=misc.get_date(jobject),
+ xalign=hippo.ALIGNMENT_START,
+ font_desc=style.FONT_NORMAL.get_pango_desc())
+ hbox.append(date)
+
+ vbox.append(hbox)
+ """
+ return vbox
+
+ def _title_notify_text_cb(self, entry, pspec):
+ if not self._update_title_sid:
+ 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()
+
+ def _description_focus_out_event_cb(self, text_view, event):
+ self._update_entry()
+
+ def _tags_focus_out_event_cb(self, text_view, event):
+ self._update_entry()
+
+ def _update_entry(self):
+ needs_update = False
+
+ old_title = self._jobject.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'
+ needs_update = True
+
+ old_tags = self._jobject.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
+ needs_update = True
+
+ old_description = self._jobject.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
+ 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)
+
+ self._update_title_sid = None
+
+ def get_keep(self):
+ return self._jobject.metadata.has_key('keep') and \
+ self._jobject.metadata['keep'] == 1
+
+ def _keep_icon_activated_cb(self, keep_icon):
+ if self.get_keep():
+ self._jobject.metadata['keep'] = 0
+ else:
+ self._jobject.metadata['keep'] = 1
+ datastore.write(self._jobject, 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')
+ self._jobject.resume()
+ return True
+
+ def _preview_box_button_release_event_cb(self, button, event):
+ logging.debug('_preview_box_button_release_event_cb')
+ self._jobject.resume()
+ return True
+
diff --git a/src/journal/journalactivity.py b/src/journal/journalactivity.py
new file mode 100644
index 0000000..c83ca46
--- /dev/null
+++ b/src/journal/journalactivity.py
@@ -0,0 +1,338 @@
+# Copyright (C) 2006, Red Hat, Inc.
+# 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 sys
+import traceback
+import uuid
+
+import gtk
+import dbus
+import statvfs
+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
+
+from journal.journaltoolbox import MainToolbox, DetailToolbox
+from journal.listview import ListView
+from journal.detailview import DetailView
+from journal.volumestoolbar import VolumesToolbar
+from journal import misc
+from journal.journalentrybundle import JournalEntryBundle
+from journal.objectchooser import ObjectChooser
+from 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'
+
+J_DBUS_SERVICE = 'org.laptop.Journal'
+J_DBUS_INTERFACE = 'org.laptop.Journal'
+J_DBUS_PATH = '/org/laptop/Journal'
+
+_SPACE_TRESHOLD = 52428800
+_BUNDLE_ID = 'org.laptop.JournalActivity'
+
+class JournalActivityDBusService(dbus.service.Object):
+ def __init__(self, parent):
+ self._parent = parent
+ session_bus = dbus.SessionBus()
+ bus_name = dbus.service.BusName(J_DBUS_SERVICE,
+ bus=session_bus, replace_existing=False, allow_replacement=False)
+ logging.debug('bus_name: %r', bus_name)
+ dbus.service.Object.__init__(self, bus_name, J_DBUS_PATH)
+
+ @dbus.service.method(J_DBUS_INTERFACE,
+ in_signature='', out_signature='')
+ def FocusSearch(self):
+ """Become visible and give focus to the search entry
+ """
+ self._parent.present()
+ self._parent.show_main_view()
+ self._parent.search_grab_focus()
+
+ @dbus.service.method(J_DBUS_INTERFACE,
+ in_signature='s', out_signature='')
+ def ShowObject(self, object_id):
+ """Pop-up journal and show object with object_id"""
+
+ logging.debug('Trying to show object %s', object_id)
+
+ if self._parent.show_object(object_id):
+ self._parent.present()
+
+ def _chooser_response_cb(self, chooser, response_id, chooser_id):
+ logging.debug('JournalActivityDBusService._chooser_response_cb')
+ if response_id == gtk.RESPONSE_ACCEPT:
+ object_id = chooser.get_selected_object_id()
+ self.ObjectChooserResponse(chooser_id, object_id)
+ else:
+ self.ObjectChooserCancelled(chooser_id)
+ chooser.destroy()
+ del chooser
+
+ @dbus.service.method(J_DBUS_INTERFACE, in_signature='i', out_signature='s')
+ def ChooseObject(self, parent_xid):
+ chooser_id = uuid.uuid4().hex
+ if parent_xid > 0:
+ parent = gtk.gdk.window_foreign_new(parent_xid)
+ else:
+ parent = None
+ chooser = ObjectChooser(parent)
+ chooser.connect('response', self._chooser_response_cb, chooser_id)
+ chooser.show()
+
+ return chooser_id
+
+ @dbus.service.signal(J_DBUS_INTERFACE, signature="ss")
+ def ObjectChooserResponse(self, chooser_id, object_id):
+ pass
+
+ @dbus.service.signal(J_DBUS_INTERFACE, signature="s")
+ def ObjectChooserCancelled(self, chooser_id):
+ pass
+
+class JournalActivity(Window):
+ def __init__(self):
+ Window.__init__(self)
+
+ self.set_title(_('Journal'))
+
+ self._main_view = None
+ self._secondary_view = None
+ self._list_view = None
+ self._detail_view = None
+ self._main_toolbox = None
+ self._detail_toolbox = None
+
+ self._setup_main_view()
+ self._setup_secondary_view()
+
+ self.add_events(gtk.gdk.ALL_EVENTS_MASK |
+ gtk.gdk.VISIBILITY_NOTIFY_MASK)
+ self._realized_sid = self.connect('realize', self.__realize_cb)
+ self.connect('visibility-notify-event',
+ self.__visibility_notify_event_cb)
+ self.connect('window-state-event', self.__window_state_event_cb)
+ 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)
+
+ self._dbus_service = JournalActivityDBusService(self)
+
+ self.iconify()
+
+ self._critical_space_alert = None
+ self._check_available_space()
+
+ def __realize_cb(self, window):
+ wm.set_bundle_id(window.window, _BUNDLE_ID)
+ activity_id = activityfactory.create_activity_id()
+ wm.set_activity_id(window.window, str(activity_id))
+ self.disconnect(self._realized_sid)
+ self._realized_sid = None
+
+ def can_close(self):
+ return False
+
+ def _setup_main_view(self):
+ self._main_toolbox = MainToolbox()
+ self._main_view = gtk.VBox()
+
+ self._list_view = ListView()
+ self._list_view.connect('detail-clicked', self.__detail_clicked_cb)
+ 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)
+
+ 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'])
+
+ def _setup_secondary_view(self):
+ self._secondary_view = gtk.VBox()
+
+ self._detail_toolbox = DetailToolbox()
+ entry_toolbar = self._detail_toolbox.entry_toolbar
+
+ self._detail_view = DetailView()
+ self._detail_view.connect('go-back-clicked', self.__go_back_clicked_cb)
+ self._secondary_view.pack_end(self._detail_view)
+ self._detail_view.show()
+
+ def _key_press_event_cb(self, widget, event):
+ keyname = gtk.gdk.keyval_name(event.keyval)
+ logging.info(keyname)
+ logging.info(event.state)
+ if keyname == 'Escape':
+ self.show_main_view()
+
+ def __detail_clicked_cb(self, list_view, entry):
+ self._show_secondary_view(entry.jobject)
+
+ def __go_back_clicked_cb(self, detail_view):
+ self.show_main_view()
+
+ def _query_changed_cb(self, toolbar, query):
+ self._list_view.update_with_query(query)
+ self.show_main_view()
+
+ def show_main_view(self):
+ if self.toolbox != self._main_toolbox:
+ self.set_toolbox(self._main_toolbox)
+ self._main_toolbox.show()
+
+ if self.canvas != self._main_view:
+ self.set_canvas(self._main_view)
+ self._main_view.show()
+
+ def _show_secondary_view(self, jobject):
+ try:
+ self._detail_toolbox.entry_toolbar.set_jobject(jobject)
+ except Exception:
+ logging.error('Exception while displaying entry:\n' + \
+ ''.join(traceback.format_exception(*sys.exc_info())))
+
+ self.set_toolbox(self._detail_toolbox)
+ self._detail_toolbox.show()
+
+ try:
+ self._detail_view.props.jobject = jobject
+ except Exception:
+ logging.error('Exception while displaying entry:\n' + \
+ ''.join(traceback.format_exception(*sys.exc_info())))
+
+ self.set_canvas(self._secondary_view)
+ self._secondary_view.show()
+
+ def show_object(self, object_id):
+ jobject = datastore.get(object_id)
+ if jobject is None:
+ return False
+ else:
+ self._show_secondary_view(jobject)
+ 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)
+ 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()
+ 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()
+ self._check_available_space()
+
+ def __data_store_deleted_cb(self, uid):
+ if self.canvas == self._secondary_view and \
+ uid == self._detail_view.props.jobject.object_id:
+ 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):
+ bundle = misc.get_bundle(jobject)
+ if bundle is None:
+ return
+
+ if bundle.is_installed():
+ return
+ try:
+ bundle.install()
+ except (ZipExtractException, RegistrationException), e:
+ logging.warning('Could not install bundle %s: %r' %
+ (jobject.file_path, e))
+ return
+
+ if jobject.metadata['mime_type'] == JournalEntryBundle.MIME_TYPE:
+ datastore.delete(jobject.object_id)
+
+ def search_grab_focus(self):
+ search_toolbar = self._main_toolbox.search_toolbar
+ search_toolbar.give_entry_focus()
+
+ def __window_state_event_cb(self, window, event):
+ logging.debug('window_state_event_cb %r' % self)
+ if event.changed_mask & gtk.gdk.WINDOW_STATE_ICONIFIED:
+ state = event.new_window_state
+ visible = not state & gtk.gdk.WINDOW_STATE_ICONIFIED
+ self._list_view.set_is_visible(visible)
+
+ def __visibility_notify_event_cb(self, window, event):
+ logging.debug('visibility_notify_event_cb %r' % self)
+ visible = event.state != gtk.gdk.VISIBILITY_FULLY_OBSCURED
+ self._list_view.set_is_visible(visible)
+
+ def _check_available_space(self):
+ ''' Check available space on device
+
+ If the available space is below 50MB an alert will be
+ shown which encourages to delete old journal entries.
+ '''
+
+ if self._critical_space_alert:
+ return
+ stat = os.statvfs(env.get_profile_path())
+ free_space = stat[statvfs.F_BSIZE] * stat[statvfs.F_BAVAIL]
+ if free_space < _SPACE_TRESHOLD:
+ self._critical_space_alert = ModalAlert()
+ self._critical_space_alert.connect('destroy',
+ self.__alert_closed_cb)
+ self._critical_space_alert.show()
+
+ def __alert_closed_cb(self, data):
+ self.show_main_view()
+ self.present()
+ self._critical_space_alert = None
+
+def start():
+ journal = JournalActivity()
+ journal.show()
+
diff --git a/src/journal/journalentrybundle.py b/src/journal/journalentrybundle.py
new file mode 100644
index 0000000..8862ca3
--- /dev/null
+++ b/src/journal/journalentrybundle.py
@@ -0,0 +1,96 @@
+# 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 os
+import tempfile
+import shutil
+
+import json
+
+import dbus
+from sugar.datastore import datastore
+from sugar.bundle.bundle import Bundle, MalformedBundleException
+
+class JournalEntryBundle(Bundle):
+ """A Journal entry bundle
+
+ See http://wiki.laptop.org/go/Journal_entry_bundles for details
+ """
+
+ MIME_TYPE = 'application/vnd.olpc-journal-entry'
+
+ _zipped_extension = '.xoj'
+ _unzipped_extension = None
+ _infodir = None
+
+ def __init__(self, path):
+ Bundle.__init__(self, path)
+
+ def install(self):
+ if os.environ.has_key('SUGAR_ACTIVITY_ROOT'):
+ install_dir = os.path.join(os.environ['SUGAR_ACTIVITY_ROOT'],
+ 'data')
+ else:
+ install_dir = tempfile.gettempdir()
+ bundle_dir = os.path.join(install_dir, self._zip_root_dir)
+ uid = self._zip_root_dir
+ 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()
+ finally:
+ shutil.rmtree(bundle_dir, ignore_errors=True)
+
+ def _read_metadata(self, bundle_dir):
+ metadata_path = os.path.join(bundle_dir, '_metadata.json')
+ if not os.path.exists(metadata_path):
+ raise MalformedBundleException(
+ 'Bundle must contain the file "_metadata.json"')
+ f = open(metadata_path, 'r')
+ try:
+ json_data = f.read()
+ finally:
+ f.close()
+ return json.read(json_data)
+
+ def _read_preview(self, uid, bundle_dir):
+ preview_path = os.path.join(bundle_dir, 'preview', uid)
+ if not os.path.exists(preview_path):
+ return ''
+ f = open(preview_path, 'r')
+ try:
+ preview_data = f.read()
+ finally:
+ f.close()
+ return preview_data
+
+ def is_installed(self):
+ # These bundles can be reinstalled as many times as desired.
+ return False
+
diff --git a/src/journal/journaltoolbox.py b/src/journal/journaltoolbox.py
new file mode 100644
index 0000000..9b2f487
--- /dev/null
+++ b/src/journal/journaltoolbox.py
@@ -0,0 +1,418 @@
+# 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
+
+from gettext import gettext as _
+import logging
+from datetime import datetime, timedelta
+import os
+
+import gobject
+import gtk
+
+from sugar.graphics.toolbox import Toolbox
+from sugar.graphics.toolcombobox import ToolComboBox
+from sugar.graphics.toolbutton import ToolButton
+from sugar.graphics.combobox import ComboBox
+from sugar.graphics.menuitem import MenuItem
+from sugar.graphics.icon import Icon
+from sugar.graphics import iconentry
+from sugar.graphics import style
+from sugar import activity
+from sugar import profile
+from sugar import mime
+from sugar.datastore import datastore
+
+from journal import volumesmanager
+from journal import misc
+
+_AUTOSEARCH_TIMEOUT = 1000
+
+_ACTION_ANYTIME = 0
+_ACTION_TODAY = 1
+_ACTION_SINCE_YESTERDAY = 2
+_ACTION_PAST_WEEK = 3
+_ACTION_PAST_MONTH = 4
+_ACTION_PAST_YEAR = 5
+
+_ACTION_ANYTHING = 0
+
+_ACTION_EVERYBODY = 0
+_ACTION_MY_FRIENDS = 1
+_ACTION_MY_CLASS = 2
+
+class MainToolbox(Toolbox):
+ def __init__(self):
+ Toolbox.__init__(self)
+
+ self.search_toolbar = SearchToolbar()
+ self.search_toolbar.set_size_request(-1, style.GRID_CELL_SIZE)
+ self.add_toolbar(_('Search'), self.search_toolbar)
+ self.search_toolbar.show()
+
+ #self.manage_toolbar = ManageToolbar()
+ #self.add_toolbar(_('Manage'), self.manage_toolbar)
+ #self.manage_toolbar.show()
+
+class SearchToolbar(gtk.Toolbar):
+ __gtype_name__ = 'SearchToolbar'
+
+ __gsignals__ = {
+ 'query-changed': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([object]))
+ }
+
+ def __init__(self):
+ gtk.Toolbar.__init__(self)
+
+ self._volume_id = None
+
+ self._search_entry = iconentry.IconEntry()
+ self._search_entry.set_icon_from_name(iconentry.ICON_ENTRY_PRIMARY,
+ 'system-search')
+ self._search_entry.connect('activate', self._search_entry_activated_cb)
+ self._search_entry.connect('changed', self._search_entry_changed_cb)
+ self._search_entry.add_clear_button()
+ self._autosearch_timer = None
+ self._add_widget(self._search_entry, expand=True)
+
+ self._what_search_combo = ComboBox()
+ self._what_combo_changed_sid = self._what_search_combo.connect(
+ 'changed', self._combo_changed_cb)
+ tool_item = ToolComboBox(self._what_search_combo)
+ self.insert(tool_item, -1)
+ tool_item.show()
+
+ self._when_search_combo = self._get_when_search_combo()
+ tool_item = ToolComboBox(self._when_search_combo)
+ self.insert(tool_item, -1)
+ tool_item.show()
+
+ # TODO: enable it when the DS supports saving the buddies.
+ #self._with_search_combo = self._get_with_search_combo()
+ #tool_item = ToolComboBox(self._with_search_combo)
+ #self.insert(tool_item, -1)
+ #tool_item.show()
+
+ self._query = self._build_query()
+
+ self.refresh_filters()
+
+ def give_entry_focus(self):
+ self._search_entry.grab_focus()
+
+ def _get_when_search_combo(self):
+ when_search = ComboBox()
+ when_search.append_item(_ACTION_ANYTIME, _('Anytime'))
+ when_search.append_separator()
+ when_search.append_item(_ACTION_TODAY, _('Today'))
+ when_search.append_item(_ACTION_SINCE_YESTERDAY,
+ _('Since yesterday'))
+ # TRANS: Filter entries modified during the last 7 days.
+ when_search.append_item(_ACTION_PAST_WEEK, _('Past week'))
+ # TRANS: Filter entries modified during the last 30 days.
+ when_search.append_item(_ACTION_PAST_MONTH, _('Past month'))
+ # TRANS: Filter entries modified during the last 356 days.
+ when_search.append_item(_ACTION_PAST_YEAR, _('Past year'))
+ when_search.set_active(0)
+ when_search.connect('changed', self._combo_changed_cb)
+ return when_search
+
+ def _get_with_search_combo(self):
+ with_search = ComboBox()
+ with_search.append_item(_ACTION_EVERYBODY, _('Anyone'))
+ with_search.append_separator()
+ with_search.append_item(_ACTION_MY_FRIENDS, _('My friends'))
+ with_search.append_item(_ACTION_MY_CLASS, _('My class'))
+ with_search.append_separator()
+
+ # TODO: Ask the model for buddies.
+ with_search.append_item(3, 'Dan', 'theme:xo')
+
+ with_search.set_active(0)
+ with_search.connect('changed', self._combo_changed_cb)
+ return with_search
+
+ def _add_widget(self, widget, expand=False):
+ tool_item = gtk.ToolItem()
+ tool_item.set_expand(expand)
+
+ tool_item.add(widget)
+ widget.show()
+
+ self.insert(tool_item, -1)
+ tool_item.show()
+
+ def _build_query(self):
+ query = {}
+ if self._volume_id:
+ query['mountpoints'] = [self._volume_id]
+ if self._what_search_combo.props.value:
+ value = self._what_search_combo.props.value
+ generic_type = mime.get_generic_type(value)
+ if generic_type:
+ mime_types = generic_type.mime_types
+ query['mime_type'] = mime_types
+ else:
+ query['activity'] = self._what_search_combo.props.value
+ if self._when_search_combo.props.value:
+ date_from, date_to = self._get_date_range()
+ query['mtime'] = {'start': date_from, 'end': date_to}
+ if self._search_entry.props.text:
+ text = self._search_entry.props.text.strip()
+
+ if not text.startswith('"'):
+ query_text = ''
+ words = text.split(' ')
+ for word in words:
+ if word:
+ if query_text:
+ query_text += ' '
+ query_text += word + '*'
+ else:
+ query_text = text
+
+ if query_text:
+ query['query'] = query_text
+
+ return query
+
+ def _get_date_range(self):
+ today_start = datetime.today().replace(hour=0, minute=0, second=0)
+ right_now = datetime.today()
+ if self._when_search_combo.props.value == _ACTION_TODAY:
+ date_range = (today_start, right_now)
+ elif self._when_search_combo.props.value == _ACTION_SINCE_YESTERDAY:
+ date_range = (today_start - timedelta(1), right_now)
+ elif self._when_search_combo.props.value == _ACTION_PAST_WEEK:
+ date_range = (today_start - timedelta(7), right_now)
+ elif self._when_search_combo.props.value == _ACTION_PAST_MONTH:
+ date_range = (today_start - timedelta(30), right_now)
+ elif self._when_search_combo.props.value == _ACTION_PAST_YEAR:
+ date_range = (today_start - timedelta(356), right_now)
+
+ return (date_range[0].isoformat(),
+ date_range[1].isoformat())
+
+ def _combo_changed_cb(self, combo):
+ new_query = self._build_query()
+ if self._query != new_query:
+ self._query = new_query
+ self.emit('query-changed', self._query)
+
+ def _search_entry_activated_cb(self, search_entry):
+ if self._autosearch_timer:
+ gobject.source_remove(self._autosearch_timer)
+ new_query = self._build_query()
+ if self._query != new_query:
+ self._query = new_query
+ self.emit('query-changed', self._query)
+
+ def _search_entry_changed_cb(self, search_entry):
+ if not search_entry.props.text:
+ search_entry.activate()
+ return
+
+ if self._autosearch_timer:
+ gobject.source_remove(self._autosearch_timer)
+ self._autosearch_timer = gobject.timeout_add(_AUTOSEARCH_TIMEOUT,
+ self._autosearch_timer_cb)
+
+ def _autosearch_timer_cb(self):
+ logging.debug('_autosearch_timer_cb')
+ self._autosearch_timer = None
+ self._search_entry.activate()
+ return False
+
+ def set_volume_id(self, volume_id):
+ self._volume_id = volume_id
+ new_query = self._build_query()
+ if self._query != new_query:
+ self._query = new_query
+ self.emit('query-changed', self._query)
+
+ def refresh_filters(self):
+ current_value = self._what_search_combo.props.value
+ current_value_index = 0
+
+ self._what_search_combo.handler_block(self._what_combo_changed_sid)
+ try:
+ self._what_search_combo.remove_all()
+ # TRANS: Item in a combo box that filters by entry type.
+ self._what_search_combo.append_item(_ACTION_ANYTHING, _('Anything'))
+
+ registry = activity.get_registry()
+ appended_separator = False
+ for service_name in datastore.get_unique_values('activity'):
+ activity_info = registry.get_activity(service_name)
+ if not activity_info is None:
+ if not appended_separator:
+ self._what_search_combo.append_separator()
+ appended_separator = True
+
+ if os.path.exists(activity_info.icon):
+ self._what_search_combo.append_item(service_name,
+ activity_info.name,
+ file_name=activity_info.icon)
+ else:
+ self._what_search_combo.append_item(service_name,
+ activity_info.name,
+ icon_name='application-octet-stream')
+
+ if service_name == current_value:
+ current_value_index = \
+ len(self._what_search_combo.get_model()) - 1
+
+ self._what_search_combo.append_separator()
+
+ types = mime.get_all_generic_types()
+ for generic_type in types :
+ self._what_search_combo.append_item(
+ generic_type.type_id, generic_type.name, generic_type.icon)
+ if generic_type.type_id == current_value:
+ current_value_index = \
+ len(self._what_search_combo.get_model()) - 1
+
+ self._what_search_combo.set_active(current_value_index)
+ finally:
+ self._what_search_combo.handler_unblock(
+ self._what_combo_changed_sid)
+
+class ManageToolbar(gtk.Toolbar):
+ __gtype_name__ = 'ManageToolbar'
+
+ def __init__(self):
+ gtk.Toolbar.__init__(self)
+
+class DetailToolbox(Toolbox):
+ def __init__(self):
+ Toolbox.__init__(self)
+
+ self.entry_toolbar = EntryToolbar()
+ self.add_toolbar('', self.entry_toolbar)
+ self.entry_toolbar.show()
+
+class EntryToolbar(gtk.Toolbar):
+ def __init__(self):
+ gtk.Toolbar.__init__(self)
+
+ self._jobject = None
+
+ self._resume = ToolButton('activity-start')
+ self._resume.connect('clicked', self._resume_clicked_cb)
+ self.add(self._resume)
+ self._resume.show()
+
+ self._copy = ToolButton()
+
+ icon = Icon(icon_name='edit-copy', xo_color=profile.get_color())
+ self._copy.set_icon_widget(icon)
+ icon.show()
+
+ self._copy.set_tooltip(_('Copy'))
+ self._copy.connect('clicked', self._copy_clicked_cb)
+ self.add(self._copy)
+ self._copy.show()
+
+ separator = gtk.SeparatorToolItem()
+ self.add(separator)
+ separator.show()
+
+ erase_button = ToolButton('list-remove')
+ erase_button.set_tooltip(_('Erase'))
+ erase_button.connect('clicked', self._erase_button_clicked_cb)
+ self.add(erase_button)
+ erase_button.show()
+
+ def set_jobject(self, jobject):
+ self._jobject = jobject
+ self._refresh_copy_palette()
+ self._refresh_resume_palette()
+
+ def _resume_clicked_cb(self, button):
+ if self._jobject:
+ self._jobject.resume()
+
+ 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)
+
+ def _clipboard_get_func_cb(self, clipboard, selection_data, info, data):
+ selection_data.set('text/uri-list', 8, self._jobject.file_path)
+
+ def _clipboard_clear_func_cb(self, clipboard, data):
+ pass
+
+ def _erase_button_clicked_cb(self, button):
+ if self._jobject:
+ bundle = misc.get_bundle(self._jobject)
+ if bundle is not None and bundle.is_installed():
+ bundle.uninstall()
+ datastore.delete(self._jobject.object_id)
+
+ def _resume_menu_item_activate_cb(self, menu_item, service_name):
+ if self._jobject:
+ self._jobject.resume(service_name)
+
+ def _copy_menu_item_activate_cb(self, menu_item, volume):
+ if self._jobject:
+ datastore.copy(self._jobject, volume.id)
+
+ def _refresh_copy_palette(self):
+ palette = self._copy.get_palette()
+
+ for menu_item in palette.menu.get_children():
+ palette.menu.remove(menu_item)
+ menu_item.destroy()
+
+ volumes_manager = volumesmanager.get_volumes_manager()
+ for volume in volumes_manager.get_volumes():
+ if self._jobject.metadata['mountpoint'] == volume.id:
+ continue
+ menu_item = MenuItem(volume.name)
+ menu_item.set_image(Icon(icon_name=volume.icon_name,
+ icon_size=gtk.ICON_SIZE_MENU))
+ menu_item.connect('activate',
+ self._copy_menu_item_activate_cb,
+ volume)
+ palette.menu.append(menu_item)
+ menu_item.show()
+
+ def _refresh_resume_palette(self):
+ if self._jobject.metadata.get('activity_id', ''):
+ # TRANS: Action label for resuming an activity.
+ self._resume.set_tooltip(_('Resume'))
+ else:
+ # TRANS: Action label for starting an entry.
+ self._resume.set_tooltip(_('Start'))
+
+ palette = self._resume.get_palette()
+
+ for menu_item in palette.menu.get_children():
+ palette.menu.remove(menu_item)
+ menu_item.destroy()
+
+ for activity_info in self._jobject.get_activities():
+ menu_item = MenuItem(activity_info.name)
+ menu_item.set_image(Icon(file=activity_info.icon,
+ icon_size=gtk.ICON_SIZE_MENU))
+ menu_item.connect('activate', self._resume_menu_item_activate_cb,
+ activity_info.bundle_id)
+ palette.menu.append(menu_item)
+ menu_item.show()
+
diff --git a/src/journal/keepicon.py b/src/journal/keepicon.py
new file mode 100644
index 0000000..8a86c83
--- /dev/null
+++ b/src/journal/keepicon.py
@@ -0,0 +1,57 @@
+# Copyright (C) 2006, Red Hat, Inc.
+#
+# 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 gobject
+import hippo
+
+from sugar.graphics.icon import CanvasIcon
+from sugar.graphics import style
+from sugar import profile
+
+class KeepIcon(CanvasIcon):
+ def __init__(self, keep):
+ CanvasIcon.__init__(self, icon_name='emblem-favorite',
+ box_width=style.GRID_CELL_SIZE * 3 / 5,
+ size=style.SMALL_ICON_SIZE)
+ self.connect('motion-notify-event', self.__motion_notify_event_cb)
+
+ self._keep = None
+ self.set_keep(keep)
+
+ def set_keep(self, keep):
+ if keep == self._keep:
+ return
+
+ self._keep = keep
+ if keep:
+ self.props.xo_color = profile.get_color()
+ else:
+ self.props.stroke_color = style.COLOR_BUTTON_GREY.get_svg()
+ self.props.fill_color = style.COLOR_TRANSPARENT.get_svg()
+
+ def get_keep(self):
+ return self._keep
+
+ keep = gobject.property(type=int, default=0, getter=get_keep,
+ setter=set_keep)
+
+ def __motion_notify_event_cb(self, icon, event):
+ if not self._keep:
+ if event.detail == hippo.MOTION_DETAIL_ENTER:
+ icon.props.fill_color = style.COLOR_BUTTON_GREY.get_svg()
+ elif event.detail == hippo.MOTION_DETAIL_LEAVE:
+ icon.props.fill_color = style.COLOR_TRANSPARENT.get_svg()
+
diff --git a/src/journal/listview.py b/src/journal/listview.py
new file mode 100644
index 0000000..a0f71e5
--- /dev/null
+++ b/src/journal/listview.py
@@ -0,0 +1,460 @@
+# 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
+import traceback
+import sys
+from gettext import gettext as _
+
+import hippo
+import gobject
+import gtk
+import dbus
+
+from sugar.graphics import style
+from sugar.graphics.icon import CanvasIcon
+
+from journal.collapsedentry import CollapsedEntry
+from journal import query
+
+DS_DBUS_SERVICE = 'org.laptop.sugar.DataStore'
+DS_DBUS_INTERFACE = 'org.laptop.sugar.DataStore'
+DS_DBUS_PATH = '/org/laptop/sugar/DataStore'
+
+UPDATE_INTERVAL = 300000
+
+EMPTY_JOURNAL = _("Your Journal is empty")
+NO_MATCH = _("No matching entries ")
+
+class BaseListView(gtk.HBox):
+ __gtype_name__ = 'BaseListView'
+
+ def __init__(self):
+ self._query = {}
+ self._result_set = None
+ self._entries = []
+ self._page_size = 0
+ self._last_value = -1
+ self._reflow_sid = 0
+
+ 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())
+
+ self._canvas = hippo.Canvas()
+ self._canvas.set_root(self._box)
+
+ self.pack_start(self._canvas)
+ self._canvas.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._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)
+
+ # DND stuff
+ 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)
+
+ # Auto-update stuff
+ self._fully_obscured = True
+ self._dirty = False
+ 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)
+
+ def __destroy_cb(self, widget):
+ self._datastore_created_handler.remove()
+ 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()
+ else:
+ self._vscrollbar.hide()
+
+ def _vadjustment_value_changed_cb(self, vadjustment):
+ gobject.idle_add(self._do_scroll)
+
+ def _do_scroll(self, force=False):
+ import time
+ t = time.time()
+
+ value = int(self._vadjustment.props.value)
+
+ if value == self._last_value and not force:
+ return
+ self._last_value = value
+
+ self._result_set.seek(value)
+ jobjects = 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._dirty = False
+
+ logging.debug('_do_scroll %r %r\n' % (value, (time.time() - t)))
+
+ return False
+
+ def _refresh_view(self, jobjects):
+ logging.debug('ListView %r' % self)
+ # Indicate when the Journal is empty
+ if len(jobjects) == 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(self._entries):
+ entry = self.create_entry()
+ self._box.append(entry)
+ self._entries.append(entry)
+ entry.jobject = jobjects[i]
+ else:
+ entry = self._entries[i]
+ entry.jobject = jobjects[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 update_with_query(self, query_dict):
+ logging.debug('ListView.update_with_query')
+ self._query = query_dict
+ if self._page_size > 0:
+ self.refresh()
+
+ def refresh(self):
+ if self._result_set:
+ self._result_set.destroy()
+ self._result_set = query.find(self._query)
+ self._vadjustment.props.upper = self._result_set.length
+ self._vadjustment.changed()
+
+ self._vadjustment.props.value = min(self._vadjustment.props.value,
+ self._result_set.length - self._page_size)
+ if self._result_set.length == 0:
+ if self._query.get('query', '') or \
+ self._query.get('mime_type', '') or \
+ self._query.get('mtime', ''):
+ self._show_message(NO_MATCH)
+ else:
+ self._show_message(EMPTY_JOURNAL)
+ else:
+ self._clear_message()
+ self._do_scroll(force=True)
+
+ 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 None:
+ self._result_set = query.find(self._query)
+
+ 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(force=True)
+
+ self._reflow_sid = 0
+
+ def _show_message(self, message):
+ box = hippo.CanvasBox(orientation=hippo.ORIENTATION_VERTICAL,
+ background_color=style.COLOR_WHITE.get_int(),
+ yalign=hippo.ALIGNMENT_CENTER)
+ icon = CanvasIcon(size=style.LARGE_ICON_SIZE,
+ icon_name='activity-journal',
+ stroke_color = style.COLOR_BUTTON_GREY.get_svg(),
+ fill_color = style.COLOR_TRANSPARENT.get_svg())
+ text = hippo.CanvasText(text=message,
+ xalign=hippo.ALIGNMENT_CENTER,
+ font_desc=style.FONT_NORMAL.get_pango_desc(),
+ color = style.COLOR_BUTTON_GREY.get_int())
+
+ box.append(icon)
+ box.append(text)
+ self._canvas.set_root(box)
+
+ 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
+
+ def _drag_data_get_cb(self, widget, context, selection, target_type,
+ event_time):
+ logging.debug("drag_data_get_cb: requested target " + selection.target)
+
+ jobject = self._last_clicked_entry.jobject
+ if selection.target == 'text/uri-list':
+ selection.set(selection.target, 8, jobject.file_path)
+ elif selection.target == 'journal-object-id':
+ selection.set(selection.target, 8, jobject.object_id)
+
+ 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
+
+ def update_dates(self):
+ logging.debug('ListView.update_dates')
+ for entry in self._entries:
+ 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()
+
+ 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
+
+ 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()
+ if self._update_dates_timer is None:
+ logging.debug('Adding date updating timer')
+ self._update_dates_timer = \
+ gobject.timeout_add(UPDATE_INTERVAL,
+ self.__update_dates_timer_cb)
+ else:
+ self._fully_obscured = True
+ if self._update_dates_timer is not None:
+ logging.debug('Remove date updating timer')
+ gobject.source_remove(self._update_dates_timer)
+ self._update_dates_timer = None
+
+ def __update_dates_timer_cb(self):
+ self.update_dates()
+ return True
+
+class ListView(BaseListView):
+ __gtype_name__ = 'ListView'
+
+ __gsignals__ = {
+ 'detail-clicked': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([object]))
+ }
+
+ def __init__(self):
+ BaseListView.__init__(self)
+
+ def create_entry(self):
+ entry = CollapsedEntry()
+ entry.connect('detail-clicked', self.__entry_activated_cb)
+ return entry
+
+ def __entry_activated_cb(self, entry):
+ self.emit('detail-clicked', entry)
+
diff --git a/src/journal/misc.py b/src/journal/misc.py
new file mode 100644
index 0000000..8ce86d3
--- /dev/null
+++ b/src/journal/misc.py
@@ -0,0 +1,109 @@
+# 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
+import time
+import traceback
+import sys
+import os
+from gettext import gettext as _
+
+import gtk
+
+from sugar import activity
+from sugar import mime
+from sugar.bundle.activitybundle import ActivityBundle
+from sugar.bundle.contentbundle import ContentBundle
+from sugar.bundle.bundle import MalformedBundleException
+from sugar import util
+
+from journal.journalentrybundle import JournalEntryBundle
+
+def _get_icon_file_name(icon_name):
+ icon_theme = gtk.icon_theme_get_default()
+ info = icon_theme.lookup_icon(icon_name, gtk.ICON_SIZE_LARGE_TOOLBAR, 0)
+ if not info:
+ # display standard icon when icon for mime type is not found
+ info = icon_theme.lookup_icon('application-octet-stream',
+ gtk.ICON_SIZE_LARGE_TOOLBAR, 0)
+ fname = info.get_filename()
+ del info
+ return fname
+
+_icon_cache = util.LRU(50)
+
+def get_icon_name(jobject):
+
+ cache_key = (jobject.object_id, jobject.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:
+ try:
+ bundle = ActivityBundle(jobject.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']
+ activity_info = activity.get_registry().get_activity(service_name)
+ if activity_info:
+ file_name = activity_info.icon
+
+ mime_type = jobject.metadata['mime_type']
+ if not file_name and mime_type:
+ icon_name = mime.get_mime_icon(mime_type)
+ if icon_name:
+ file_name = _get_icon_file_name(icon_name)
+
+ if not file_name or not os.path.exists(file_name):
+ file_name = _get_icon_file_name('application-octet-stream')
+
+ _icon_cache[cache_key] = file_name
+
+ return file_name
+
+def get_date(jobject):
+ """ Convert from a string in iso format to a more human-like format. """
+ if jobject.metadata.has_key('timestamp'):
+ timestamp = float(jobject.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")
+ return util.timestamp_to_elapsed_string(time.mktime(ti))
+ else:
+ return _('No date')
+
+def get_bundle(jobject):
+ 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)
+ else:
+ return None
+ except MalformedBundleException, e:
+ logging.warning('Incorrect bundle: %r' % e)
+ return None
+
diff --git a/src/journal/modalalert.py b/src/journal/modalalert.py
new file mode 100644
index 0000000..6c7bce9
--- /dev/null
+++ b/src/journal/modalalert.py
@@ -0,0 +1,93 @@
+# Copyright (C) 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 gtk
+from gettext import gettext as _
+
+from sugar.graphics.icon import Icon
+from sugar.graphics import style
+from sugar import profile
+
+class ModalAlert(gtk.Window):
+
+ __gtype_name__ = 'SugarModalAlert'
+
+ def __init__(self):
+ gtk.Window.__init__(self)
+
+ self.set_border_width(style.LINE_WIDTH)
+ offset = style.GRID_CELL_SIZE
+ width = gtk.gdk.screen_width() - offset * 2
+ height = gtk.gdk.screen_height() - offset * 2
+ self.set_size_request(width, height)
+ self.set_position(gtk.WIN_POS_CENTER_ALWAYS)
+ self.set_decorated(False)
+ self.set_resizable(False)
+ self.set_modal(True)
+
+ self._main_view = gtk.EventBox()
+ self._vbox = gtk.VBox()
+ self._vbox.set_spacing(style.DEFAULT_SPACING)
+ self._vbox.set_border_width(style.GRID_CELL_SIZE * 2)
+ self._main_view.modify_bg(gtk.STATE_NORMAL,
+ style.COLOR_BLACK.get_gdk_color())
+ self._main_view.add(self._vbox)
+ self._vbox.show()
+
+ icon = Icon(icon_name='activity-journal',
+ pixel_size=style.XLARGE_ICON_SIZE,
+ xo_color=profile.get_color())
+ self._vbox.pack_start(icon, False)
+ icon.show()
+
+ self._title = gtk.Label()
+ self._title.modify_fg(gtk.STATE_NORMAL,
+ style.COLOR_WHITE.get_gdk_color())
+ self._title.set_markup('<b>%s</b>' % _('Your Journal is full'))
+ self._vbox.pack_start(self._title, False)
+ self._title.show()
+
+ self._message = gtk.Label(_('Please delete some old Journal'
+ ' entries to make space for new ones.'))
+ self._message.modify_fg(gtk.STATE_NORMAL,
+ style.COLOR_WHITE.get_gdk_color())
+ self._vbox.pack_start(self._message, False)
+ self._message.show()
+
+ alignment = gtk.Alignment(xalign=0.5, yalign=0.5)
+ self._vbox.pack_start(alignment, expand=False)
+ alignment.show()
+
+ self._show_journal = gtk.Button()
+ self._show_journal.set_label(_('Show Journal'))
+ alignment.add(self._show_journal)
+ self._show_journal.show()
+ self._show_journal.connect('clicked', self.__show_journal_cb)
+
+ self.add(self._main_view)
+ self._main_view.show()
+
+ self.connect("realize", self.__realize_cb)
+
+ def __realize_cb(self, widget):
+ self.window.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG)
+ self.window.set_accept_focus(True)
+
+ def __show_journal_cb(self, button):
+ '''The opener will listen on the destroy signal
+ '''
+ self.destroy()
+
diff --git a/src/journal/objectchooser.py b/src/journal/objectchooser.py
new file mode 100644
index 0000000..00e4a26
--- /dev/null
+++ b/src/journal/objectchooser.py
@@ -0,0 +1,199 @@
+# 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
+
+from gettext import gettext as _
+import logging
+
+import gobject
+import gtk
+import hippo
+
+from sugar.graphics import style
+from sugar.graphics.toolbutton import ToolButton
+from sugar.datastore import datastore
+
+from journal.listview import ListView
+from journal.collapsedentry import BaseCollapsedEntry
+from journal.journaltoolbox import SearchToolbar
+from journal.volumestoolbar import VolumesToolbar
+
+class ObjectChooser(gtk.Window):
+
+ __gtype_name__ = 'ObjectChooser'
+
+ __gsignals__ = {
+ 'response': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([int]))
+ }
+
+ def __init__(self, parent=None):
+ gtk.Window.__init__(self)
+ self.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG)
+ self.set_decorated(False)
+ self.set_position(gtk.WIN_POS_CENTER_ALWAYS)
+ self.set_border_width(style.LINE_WIDTH)
+
+ self._selected_object_id = None
+
+ self.add_events(gtk.gdk.VISIBILITY_NOTIFY_MASK)
+ self.connect('visibility-notify-event',
+ self.__visibility_notify_event_cb)
+ self.connect('delete-event', self.__delete_event_cb)
+ self.connect('key-press-event', self.__key_press_event_cb)
+ if parent is not None:
+ self.connect('realize', self.__realize_cb, parent)
+
+ vbox = gtk.VBox()
+ self.add(vbox)
+ vbox.show()
+
+ title_box = TitleBox()
+ title_box.connect('volume-changed', self.__volume_changed_cb)
+ title_box.close_button.connect('clicked',
+ self.__close_button_clicked_cb)
+ title_box.set_size_request(-1, style.GRID_CELL_SIZE)
+ vbox.pack_start(title_box, expand=False)
+ title_box.show()
+
+ separator = gtk.HSeparator()
+ vbox.pack_start(separator, expand=False)
+ separator.show()
+
+ self._toolbar = SearchToolbar()
+ self._toolbar.connect('query-changed', self.__query_changed_cb)
+ self._toolbar.set_size_request(-1, style.GRID_CELL_SIZE)
+ vbox.pack_start(self._toolbar, expand=False)
+ self._toolbar.show()
+
+ self._list_view = ChooserListView()
+ self._list_view.connect('entry-activated', self.__entry_activated_cb)
+ vbox.pack_start(self._list_view)
+ self._list_view.show()
+
+ self._toolbar.set_volume_id(datastore.mounts()[0]['id'])
+
+ width = gtk.gdk.screen_width() - style.GRID_CELL_SIZE * 2
+ height = gtk.gdk.screen_height() - style.GRID_CELL_SIZE * 2
+ self.set_size_request(width, height)
+
+ def __realize_cb(self, chooser, parent):
+ self.window.set_transient_for(parent)
+ # TODO: Should we disconnect the signal here?
+
+ def __entry_activated_cb(self, list_view, entry):
+ self._selected_object_id = entry.jobject.object_id
+ self.emit('response', gtk.RESPONSE_ACCEPT)
+
+ def __delete_event_cb(self, chooser, event):
+ self.emit('response', gtk.RESPONSE_DELETE_EVENT)
+
+ def __key_press_event_cb(self, widget, event):
+ keyname = gtk.gdk.keyval_name(event.keyval)
+ if keyname == 'Escape':
+ self.emit('response', gtk.RESPONSE_DELETE_EVENT)
+
+ def __close_button_clicked_cb(self, button):
+ self.emit('response', gtk.RESPONSE_DELETE_EVENT)
+
+ def get_selected_object_id(self):
+ return self._selected_object_id
+
+ def __query_changed_cb(self, toolbar, query):
+ self._list_view.update_with_query(query)
+
+ def __volume_changed_cb(self, volume_toolbar, volume_id):
+ logging.debug('Selected volume: %r.' % volume_id)
+ self._toolbar.set_volume_id(volume_id)
+
+ def __visibility_notify_event_cb(self, window, event):
+ logging.debug('visibility_notify_event_cb %r' % self)
+ visible = event.state == gtk.gdk.VISIBILITY_FULLY_OBSCURED
+ self._list_view.set_is_visible(visible)
+
+class TitleBox(VolumesToolbar):
+ __gtype_name__ = 'TitleBox'
+
+ def __init__(self):
+ VolumesToolbar.__init__(self)
+
+ label = gtk.Label()
+ label.set_markup('<b>%s</b>' % _('Choose an object'))
+ label.set_alignment(0, 0.5)
+ self._add_widget(label, expand=True)
+
+ self.close_button = ToolButton(icon_name='dialog-cancel')
+ self.close_button.set_tooltip(_('Close'))
+ self.insert(self.close_button, -1)
+ self.close_button.show()
+
+ def _add_widget(self, widget, expand=False):
+ tool_item = gtk.ToolItem()
+ tool_item.set_expand(expand)
+
+ tool_item.add(widget)
+ widget.show()
+
+ self.insert(tool_item, -1)
+ tool_item.show()
+
+class ChooserCollapsedEntry(BaseCollapsedEntry):
+ __gtype_name__ = 'ChooserCollapsedEntry'
+
+ __gsignals__ = {
+ 'entry-activated': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([]))
+ }
+
+ def __init__(self):
+ BaseCollapsedEntry.__init__(self)
+
+ self.connect_after('button-release-event',
+ self.__button_release_event_cb)
+ self.connect('motion-notify-event', self.__motion_notify_event_cb)
+
+ def __button_release_event_cb(self, entry, event):
+ self.emit('entry-activated')
+ return True
+
+ def __motion_notify_event_cb(self, entry, event):
+ if event.detail == hippo.MOTION_DETAIL_ENTER:
+ self.props.background_color = style.COLOR_PANEL_GREY.get_int()
+ elif event.detail == hippo.MOTION_DETAIL_LEAVE:
+ self.props.background_color = style.COLOR_WHITE.get_int()
+ return False
+
+class ChooserListView(ListView):
+ __gtype_name__ = 'ChooserListView'
+
+ __gsignals__ = {
+ 'entry-activated': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([object]))
+ }
+
+ def __init__(self):
+ ListView.__init__(self)
+
+ def create_entry(self):
+ entry = ChooserCollapsedEntry()
+ entry.connect('entry-activated', self.__entry_activated_cb)
+ return entry
+
+ def __entry_activated_cb(self, entry):
+ self.emit('entry-activated', entry)
+
diff --git a/src/journal/palettes.py b/src/journal/palettes.py
new file mode 100644
index 0000000..b1072b2
--- /dev/null
+++ b/src/journal/palettes.py
@@ -0,0 +1,113 @@
+# Copyright (C) 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
+
+from gettext import gettext as _
+
+import gtk
+
+from sugar import profile
+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 journal import misc
+
+class ObjectPalette(Palette):
+ def __init__(self, jobject):
+
+ self._jobject = jobject
+
+ 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.xo_color = \
+ XoColor(jobject.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']
+ else:
+ title = _('Untitled')
+
+ Palette.__init__(self, primary_text=title,
+ icon=activity_icon)
+
+ if jobject.metadata.get('activity_id', ''):
+ resume_label = _('Resume')
+ else:
+ resume_label = _('Start')
+ menu_item = MenuItem(resume_label, 'activity-start')
+ menu_item.connect('activate', self.__start_activate_cb)
+ self.menu.append(menu_item)
+ menu_item.show()
+
+ # TODO: Add "Start with" menu item
+
+ menu_item = MenuItem(_('Copy'))
+ icon = Icon(icon_name='edit-copy', xo_color=profile.get_color(),
+ icon_size=gtk.ICON_SIZE_MENU)
+ menu_item.set_image(icon)
+ menu_item.connect('activate', self.__copy_activate_cb)
+ self.menu.append(menu_item)
+ menu_item.show()
+
+ menu_item = MenuItem(_('Erase'), 'list-remove')
+ menu_item.connect('activate', self.__erase_activate_cb)
+ self.menu.append(menu_item)
+ menu_item.show()
+
+ def __start_activate_cb(self, menu_item):
+ self._jobject.resume()
+
+ def __copy_activate_cb(self, menu_item):
+ clipboard = gtk.Clipboard()
+ clipboard.set_with_data([('text/uri-list', 0, 0)],
+ self.__clipboard_get_func_cb,
+ self.__clipboard_clear_func_cb)
+
+ def __clipboard_get_func_cb(self, clipboard, selection_data, info, data):
+ selection_data.set('text/uri-list', 8, self._jobject.file_path)
+
+ def __clipboard_clear_func_cb(self, clipboard, data):
+ pass
+
+ def __erase_activate_cb(self, menu_item):
+ bundle = misc.get_bundle(self._jobject)
+ if bundle is not None and bundle.is_installed():
+ bundle.uninstall()
+ datastore.delete(self._jobject.object_id)
+
+
+class BuddyPalette(Palette):
+ def __init__(self, buddy):
+ self._buddy = buddy
+
+ nick, colors = buddy
+ buddy_icon = Icon(icon_name='computer-xo',
+ icon_size=style.STANDARD_ICON_SIZE,
+ xo_color=XoColor(colors))
+
+ Palette.__init__(self, primary_text=nick,
+ icon=buddy_icon)
+
+ # TODO: Support actions on buddies, like make friend, invite, etc.
diff --git a/src/journal/query.py b/src/journal/query.py
new file mode 100644
index 0000000..04d9b16
--- /dev/null
+++ b/src/journal/query.py
@@ -0,0 +1,266 @@
+# 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/journal/volumesmanager.py b/src/journal/volumesmanager.py
new file mode 100644
index 0000000..b2ef08d
--- /dev/null
+++ b/src/journal/volumesmanager.py
@@ -0,0 +1,315 @@
+# 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 dbus
+
+from sugar import profile
+from sugar.datastore import datastore
+
+HAL_SERVICE_NAME = 'org.freedesktop.Hal'
+HAL_MANAGER_PATH = '/org/freedesktop/Hal/Manager'
+HAL_MANAGER_IFACE = 'org.freedesktop.Hal.Manager'
+HAL_DEVICE_IFACE = 'org.freedesktop.Hal.Device'
+HAL_VOLUME_IFACE = 'org.freedesktop.Hal.Device.Volume'
+
+MOUNT_OPTION_UID = 500
+MOUNT_OPTION_UMASK = 000
+
+_volumes_manager = None
+
+class VolumesManager(gobject.GObject):
+
+ __gtype_name__ = 'VolumesManager'
+
+ __gsignals__ = {
+ 'volume-added': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([object])),
+ 'volume-removed': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([object]))
+ }
+
+ def __init__(self):
+ gobject.GObject.__init__(self)
+
+ self._volumes = []
+
+ # Internal flash is not in HAL
+ internal_fash_id = datastore.mounts()[0]['id']
+ self._volumes.append(Volume(internal_fash_id, _('Journal'),
+ 'activity-journal', profile.get_color(),
+ None, False))
+
+ bus = dbus.SystemBus()
+ proxy = bus.get_object(HAL_SERVICE_NAME, HAL_MANAGER_PATH)
+ self._hal_manager = dbus.Interface(proxy, HAL_MANAGER_IFACE)
+ self._hal_manager.connect_to_signal('DeviceAdded',
+ self._hal_device_added_cb)
+
+ for udi in self._hal_manager.FindDeviceByCapability('volume'):
+ if self._is_device_relevant(udi):
+ try:
+ self._add_hal_device(udi)
+ except Exception, e:
+ logging.error('Exception when mounting device %r: %r' % \
+ (udi, e))
+
+ def get_volumes(self):
+ return self._volumes
+
+ def _get_volume_by_udi(self, udi):
+ for volume in self._volumes:
+ if volume.udi == udi:
+ return volume
+ return None
+
+ def _hal_device_added_cb(self, udi):
+ bus = dbus.SystemBus()
+ device_object = bus.get_object(HAL_SERVICE_NAME, udi)
+ device = dbus.Interface(device_object, HAL_DEVICE_IFACE)
+ if device.QueryCapability('volume'):
+ logging.debug('VolumesManager._hal_device_added_cb: %r', udi)
+ if self._is_device_relevant(udi):
+ self._add_hal_device(udi)
+
+ def _is_device_relevant(self, udi):
+ bus = dbus.SystemBus()
+ device_object = bus.get_object(HAL_SERVICE_NAME, udi)
+ device = dbus.Interface(device_object, HAL_DEVICE_IFACE)
+
+ # 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
+
+ def _add_hal_device(self, udi):
+ logging.debug('VolumeToolbar._add_hal_device: %r' % udi)
+
+ bus = dbus.SystemBus()
+ device_object = bus.get_object(HAL_SERVICE_NAME, udi)
+ device = dbus.Interface(device_object, HAL_DEVICE_IFACE)
+
+ # listen to mount/unmount
+ device.connect_to_signal('PropertyModified',
+ lambda *args: self._hal_device_property_modified_cb(udi, *args))
+
+ bus.add_signal_receiver(self._hal_device_removed_cb,
+ 'DeviceRemoved',
+ HAL_MANAGER_IFACE, HAL_SERVICE_NAME,
+ HAL_MANAGER_PATH, arg0=udi)
+
+ if device.GetProperty('volume.is_mounted'):
+ volume_id = self._mount_in_datastore(udi)
+ return
+
+ label = device.GetProperty('volume.label')
+ fs_type = device.GetProperty('volume.fstype')
+ valid_options = device.GetProperty('volume.mount.valid_options')
+ options = []
+
+ if 'uid=' in valid_options:
+ options.append('uid=%i' % MOUNT_OPTION_UID)
+
+ if 'umask=' in valid_options:
+ options.append('umask=%i' % MOUNT_OPTION_UMASK)
+
+ if 'noatime' in valid_options:
+ options.append('noatime')
+
+ if 'utf8' in valid_options:
+ options.append('utf8')
+
+ if 'iocharset=' in valid_options:
+ options.append('iocharset=utf8')
+
+ mount_point = label
+ if not mount_point:
+ mount_point = device.GetProperty('volume.uuid')
+
+ volume = dbus.Interface(device_object, HAL_VOLUME_IFACE)
+
+ # Try 100 times to get a mount point
+ mounted = False
+ i = 0
+ while not mounted:
+ try:
+ if i > 0:
+ volume.Mount('%s_%d' % (mount_point, i), fs_type, options)
+ else:
+ volume.Mount(mount_point, fs_type, options)
+ mounted = True
+ except dbus.DBusException, e:
+ s = 'org.freedesktop.Hal.Device.Volume.MountPointNotAvailable'
+ if i < 100 and e.get_dbus_name() == s:
+ i += 1
+ else:
+ raise
+
+ def _hal_device_property_modified_cb(self, udi, count, changes):
+ if 'volume.is_mounted' in [change[0] for change in changes]:
+ logging.debug('VolumesManager._hal_device_property_modified: %r' % \
+ (udi))
+ bus = dbus.SystemBus()
+ #proxy = bus.get_object(HAL_SERVICE_NAME, HAL_MANAGER_PATH)
+ #hal_manager = dbus.Interface(proxy, HAL_MANAGER_IFACE)
+ # TODO: Why this doesn't work?
+ #if not hal_manager.DeviceExists(udi):
+ # return
+
+ proxy = bus.get_object(HAL_SERVICE_NAME, udi)
+ device = dbus.Interface(proxy, HAL_DEVICE_IFACE)
+ try:
+ is_mounted = device.GetProperty('volume.is_mounted')
+ except dbus.DBusException, e:
+ logging.debug('e: %s' % e)
+ return
+
+ if is_mounted:
+ if self._get_volume_by_udi(udi) is not None:
+ # device already mounted in the datastore
+ return
+ volume_id = self._mount_in_datastore(udi)
+ else:
+ self.unmount_from_datastore(udi)
+ return
+
+ def _mount_in_datastore(self, udi):
+ logging.debug('VolumeToolbar._mount_in_datastore: %r' % udi)
+
+ bus = dbus.SystemBus()
+ device_object = bus.get_object(HAL_SERVICE_NAME, udi)
+ device = dbus.Interface(device_object, HAL_DEVICE_IFACE)
+
+ mount_point = device.GetProperty('volume.mount_point')
+ ds_mounts = datastore.mounts()
+ for ds_mount in ds_mounts:
+ if mount_point == ds_mount['uri']:
+ return ds_mount['id']
+
+ mount_id = datastore.mount('inplace:' + mount_point,
+ dict(title=mount_point))
+ if not mount_id:
+ self._unmount_hal_device(udi)
+ raise RuntimeError('datastore.mount(%r, %r) failed.' % (
+ 'inplace:' + mount_point,
+ dict(title=mount_point)))
+
+ volume_name = device.GetProperty('volume.label')
+ if not volume_name:
+ volume_name = device.GetProperty('volume.uuid')
+ volume = Volume(mount_id,
+ volume_name,
+ self._get_icon_for_volume(udi),
+ profile.get_color(),
+ udi,
+ True)
+ self._volumes.append(volume)
+ self.emit('volume-added', volume)
+
+ logging.debug('mounted volume %s' % mount_point)
+
+ def _hal_device_removed_cb(self, udi):
+ logging.debug('VolumesManager._hal_device_removed_cb: %r', udi)
+ bus = dbus.SystemBus()
+ #proxy = bus.get_object(HAL_SERVICE_NAME, HAL_MANAGER_PATH)
+ #hal_manager = dbus.Interface(proxy, HAL_MANAGER_IFACE)
+ # TODO: Why this doesn't work?
+ #if not hal_manager.DeviceExists(udi):
+ # self._unmount_from_datastore(udi)
+ # self._remove_button(udi)
+ # return
+
+ proxy = bus.get_object(HAL_SERVICE_NAME, udi)
+ device = dbus.Interface(proxy, HAL_DEVICE_IFACE)
+ try:
+ is_mounted = device.GetProperty('volume.is_mounted')
+ except dbus.DBusException, e:
+ logging.debug('e: %s' % e)
+ self.unmount_from_datastore(udi)
+ return
+
+ if is_mounted:
+ self._unmount_from_datastore(udi)
+ self._unmount_hal_device(udi)
+
+ def unmount_from_datastore(self, udi):
+ logging.debug('VolumesManager._unmount_from_datastore: %r', udi)
+ volume = self._get_volume_by_udi(udi)
+ if volume is not None:
+ datastore.unmount(volume.id)
+
+ self._volumes.remove(volume)
+ self.emit('volume-removed', volume)
+
+ def unmount_hal_device(self, udi):
+ logging.debug('VolumesManager._unmount_hal_device: %r', udi)
+ bus = dbus.SystemBus()
+ device_object = bus.get_object(HAL_SERVICE_NAME, udi)
+ volume = dbus.Interface(device_object, HAL_VOLUME_IFACE)
+ volume.Unmount([])
+
+ def _get_icon_for_volume(self, udi):
+ bus = dbus.SystemBus()
+ device_object = bus.get_object(HAL_SERVICE_NAME, udi)
+ device = dbus.Interface(device_object, HAL_DEVICE_IFACE)
+
+ storage_udi = device.GetProperty('block.storage_device')
+ obj = bus.get_object(HAL_SERVICE_NAME, storage_udi)
+ storage_device = dbus.Interface(obj, HAL_DEVICE_IFACE)
+
+ storage_drive_type = storage_device.GetProperty('storage.drive_type')
+ if storage_drive_type == 'sd_mmc':
+ return 'media-flash-sd-mmc'
+ else:
+ return 'media-flash-usb'
+
+class Volume(object):
+ def __init__(self, volume_id, name, icon_name, icon_color, udi,
+ can_unmount):
+ self.id = volume_id
+ self.name = name
+ self.icon_name = icon_name
+ self.icon_color = icon_color
+ self.udi = udi
+ self.can_unmount = can_unmount
+
+ def unmount(self):
+ get_volumes_manager().unmount_from_datastore(self.udi)
+ get_volumes_manager().unmount_hal_device(self.udi)
+
+def get_volumes_manager():
+ global _volumes_manager
+ if _volumes_manager is None:
+ _volumes_manager = VolumesManager()
+ return _volumes_manager
+
diff --git a/src/journal/volumestoolbar.py b/src/journal/volumestoolbar.py
new file mode 100644
index 0000000..372db8b
--- /dev/null
+++ b/src/journal/volumestoolbar.py
@@ -0,0 +1,137 @@
+# 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
+
+from sugar.datastore import datastore
+from sugar.graphics.radiotoolbutton import RadioToolButton
+from sugar.graphics.palette import Palette
+
+from journal import volumesmanager
+
+class VolumesToolbar(gtk.Toolbar):
+ __gtype_name__ = 'VolumesToolbar'
+
+ __gsignals__ = {
+ 'volume-changed': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([str]))
+ }
+
+ def __init__(self):
+ gtk.Toolbar.__init__(self)
+ self._volume_buttons = []
+ self._volume_added_hid = None
+ self._volume_removed_hid = None
+
+ self.connect('destroy', self.__destroy_cb)
+
+ gobject.idle_add(self._set_up_volumes)
+
+ def __destroy_cb(self, widget):
+ volumes_manager = volumesmanager.get_volumes_manager()
+ volumes_manager.disconnect(self._volume_added_hid)
+ volumes_manager.disconnect(self._volume_removed_hid)
+
+ def _set_up_volumes(self):
+ volumes_manager = volumesmanager.get_volumes_manager()
+ self._volume_added_hid = \
+ volumes_manager.connect('volume-added', self._volume_added_cb)
+ self._volume_removed_hid = \
+ volumes_manager.connect('volume-removed',
+ self._volume_removed_cb)
+
+ for volume in volumes_manager.get_volumes():
+ self._add_button(volume)
+
+ def _volume_added_cb(self, volumes_manager, volume):
+ self._add_button(volume)
+
+ def _volume_removed_cb(self, volumes_manager, volume):
+ self._remove_button(volume)
+
+ def _add_button(self, volume):
+ logging.debug('VolumeToolbar._add_button: %r' % volume.name)
+
+ if self._volume_buttons:
+ group = self._volume_buttons[0]
+ else:
+ group = None
+
+ palette = Palette(volume.name)
+
+ button = VolumeButton(volume, group)
+ button.set_palette(palette)
+ button.connect('toggled', self._button_toggled_cb, volume)
+ if self._volume_buttons:
+ position = self.get_item_index(self._volume_buttons[-1]) + 1
+ else:
+ position = 0
+ self.insert(button, position)
+ button.show()
+
+ self._volume_buttons.append(button)
+
+ if volume.can_unmount:
+ menu_item = gtk.MenuItem(_('Unmount'))
+ menu_item.connect('activate', self._unmount_activated_cb, volume)
+ palette.menu.append(menu_item)
+ menu_item.show()
+
+ if len(self.get_children()) > 1:
+ self.show()
+
+ def _button_toggled_cb(self, button, volume):
+ if button.props.active:
+ self.emit('volume-changed', volume.id)
+
+ def _unmount_activated_cb(self, menu_item, volume):
+ logging.debug('VolumesToolbar._unmount_activated_cb: %r', volume.udi)
+ volume.unmount()
+
+ def _remove_button(self, volume):
+ for button in self.get_children():
+ if button.volume.id == volume.id:
+ self._volume_buttons.remove(button)
+ self.remove(button)
+ self.get_children()[0].props.active = True
+
+ if len(self.get_children()) < 2:
+ self.hide()
+ return
+
+class VolumeButton(RadioToolButton):
+ def __init__(self, volume, group):
+ RadioToolButton.__init__(self)
+ self.props.named_icon = volume.icon_name
+ self.props.xo_color = volume.icon_color
+ self.props.group = group
+
+ self.volume = volume
+ self.drag_dest_set(gtk.DEST_DEFAULT_ALL,
+ [('journal-object-id', 0, 0)],
+ gtk.gdk.ACTION_COPY)
+ self.connect('drag-data-received', self._drag_data_received_cb)
+
+ 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)
+
diff --git a/src/model/homeactivity.py b/src/model/homeactivity.py
index 9dc4f9c..bde7fa3 100644
--- a/src/model/homeactivity.py
+++ b/src/model/homeactivity.py
@@ -16,6 +16,7 @@
import time
import logging
+import os
import gobject
import dbus
@@ -23,6 +24,9 @@ import dbus
from sugar.graphics.xocolor import XoColor
from sugar.presence import presenceservice
from sugar import profile
+from sugar import wm
+
+import config
_SERVICE_NAME = "org.laptop.Activity"
_SERVICE_PATH = "/org/laptop/Activity"
@@ -44,7 +48,7 @@ class HomeActivity(gobject.GObject):
gobject.PARAM_READWRITE),
}
- def __init__(self, activity_info, activity_id):
+ def __init__(self, activity_info, activity_id, window=None):
"""Initialise the HomeActivity
activity_info -- sugar.activity.registry.ActivityInfo instance,
@@ -53,10 +57,11 @@ class HomeActivity(gobject.GObject):
the "type" of activity being created.
activity_id -- unique identifier for this instance
of the activity type
+ window -- Main WnckWindow of the activity
"""
gobject.GObject.__init__(self)
- self._window = None
+ self._window = window
self._xid = None
self._pid = None
self._service = None
@@ -105,7 +110,9 @@ class HomeActivity(gobject.GObject):
def get_icon_path(self):
"""Retrieve the activity's icon (file) name"""
- if self._activity_info:
+ if self.is_journal():
+ return os.path.join(config.data_path, 'icons/activity-journal.svg')
+ elif self._activity_info:
return self._activity_info.icon
else:
return None
@@ -162,10 +169,7 @@ class HomeActivity(gobject.GObject):
def get_type(self):
"""Retrieve the activity bundle id for future reference"""
- if self._activity_info:
- return self._activity_info.bundle_id
- else:
- return None
+ return wm.get_bundle_id(self._window)
def is_journal(self):
"""Returns boolean if the activity is of type JournalActivity"""
diff --git a/src/model/homemodel.py b/src/model/homemodel.py
index 9f3db8e..668b60e 100644
--- a/src/model/homemodel.py
+++ b/src/model/homemodel.py
@@ -169,10 +169,10 @@ class HomeModel(gobject.GObject):
home_activity = self._get_activity_by_id(activity_id)
if not home_activity:
- home_activity = HomeActivity(activity_info, activity_id)
+ home_activity = HomeActivity(activity_info, activity_id, window)
self._add_activity(home_activity)
-
- home_activity.set_window(window)
+ else:
+ home_activity.set_window(window)
if get_sugar_window_type(window) != 'launcher':
home_activity.props.launching = False
diff --git a/src/view/Shell.py b/src/view/Shell.py
index 89c0bb5..dbc1546 100644
--- a/src/view/Shell.py
+++ b/src/view/Shell.py
@@ -39,6 +39,7 @@ from view.keyhandler import KeyHandler
from view.home.HomeWindow import HomeWindow
from view.launchwindow import LaunchWindow
from model import shellmodel
+from journal import journalactivity
# #3903 - this constant can be removed and assumed to be 1 when dbus-python
# 0.82.3 is the only version used
@@ -78,18 +79,15 @@ class Shell(gobject.GObject):
try:
datastore.mount(ds_path, [], timeout=120 * \
DBUS_PYTHON_TIMEOUT_UNITS_PER_SECOND)
- except Exception:
+ except Exception, e:
# Don't explode if there's corruption; move the data out of the way
# and attempt to create a store from scratch.
+ logging.error(e)
shutil.move(ds_path, os.path.abspath(ds_path) + str(time.time()))
datastore.mount(ds_path, [], timeout=120 * \
DBUS_PYTHON_TIMEOUT_UNITS_PER_SECOND)
- # Checking for the bundle existence will also ensure
- # that the shell service is started up.
- registry = activity.get_registry()
- if registry.get_activity('org.laptop.JournalActivity'):
- self.start_activity('org.laptop.JournalActivity')
+ journalactivity.start()
def __launch_started_cb(self, home_model, home_activity):
if home_activity.is_journal():
diff --git a/src/view/frame/activitiestray.py b/src/view/frame/activitiestray.py
index 1311f07..0291732 100644
--- a/src/view/frame/activitiestray.py
+++ b/src/view/frame/activitiestray.py
@@ -55,14 +55,6 @@ class ActivityButton(RadioToolButton):
self.set_icon_widget(self._icon)
self._icon.show()
- if self._home_activity.is_journal():
- palette = JournalPalette(self._home_activity)
- else:
- palette = CurrentActivityPalette(self._home_activity)
- palette.props.invoker = FrameWidgetInvoker(self)
- palette.set_group_id('frame')
- self.set_palette(palette)
-
if home_activity.props.launching:
self._icon.props.pulsing = True
self._notify_launching_hid = home_activity.connect( \
@@ -71,6 +63,15 @@ class ActivityButton(RadioToolButton):
self._notify_launching_hid = None
self._notif_icon = None
+ def create_palette(self):
+ if self._home_activity.is_journal():
+ palette = JournalPalette(self._home_activity)
+ else:
+ palette = CurrentActivityPalette(self._home_activity)
+ palette.props.invoker = FrameWidgetInvoker(self)
+ palette.set_group_id('frame')
+ self.set_palette(palette)
+
def __notify_launching_cb(self, home_activity, pspec):
if not home_activity.props.launching:
self._icon.props.pulsing = False
diff --git a/src/view/keyhandler.py b/src/view/keyhandler.py
index b2085a4..142bab4 100644
--- a/src/view/keyhandler.py
+++ b/src/view/keyhandler.py
@@ -243,7 +243,7 @@ class KeyHandler(object):
bus = dbus.SessionBus()
obj = bus.get_object(J_DBUS_SERVICE, J_DBUS_PATH)
journal = dbus.Interface(obj, J_DBUS_INTERFACE)
- journal.FocusSearch({})
+ journal.FocusSearch()
def handle_open_search(self):
self.focus_journal_search()