Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/src/journal/listview.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/journal/listview.py')
-rw-r--r--src/journal/listview.py460
1 files changed, 460 insertions, 0 deletions
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)
+