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