From f74e5d1fdcd96dba7f4e37dfc5e9b8ef8e6e02d3 Mon Sep 17 00:00:00 2001 From: Aleksey Lim Date: Thu, 20 Aug 2009 00:05:09 +0000 Subject: Extract objectsview from listview; initial thumbs view commit --- (limited to 'src') diff --git a/src/jarabe/journal/browse/__init__.py b/src/jarabe/journal/browse/__init__.py new file mode 100644 index 0000000..307e9c3 --- /dev/null +++ b/src/jarabe/journal/browse/__init__.py @@ -0,0 +1,15 @@ +# Copyright (C) 2009, Aleksey Lim +# +# 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 diff --git a/src/jarabe/journal/browse/lazymodel.py b/src/jarabe/journal/browse/lazymodel.py new file mode 100644 index 0000000..b11f02f --- /dev/null +++ b/src/jarabe/journal/browse/lazymodel.py @@ -0,0 +1,401 @@ +# Copyright (C) 2009, Aleksey Lim +# +# 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 +import logging +from gobject import GObject, SIGNAL_RUN_FIRST, TYPE_PYOBJECT + +class Source(GObject): + __gsignals__ = { + 'objects-updated': (SIGNAL_RUN_FIRST, None, []), + 'row-delayed-fetch': (SIGNAL_RUN_FIRST, None, 2*[TYPE_PYOBJECT]) } + + def get_count(self): + """ Returns number of objects """ + pass + + def get_row(self, offset): + """ Get object + + Returns: + objects in dict {field_name: value, ...} + False can't fint object + None wait for reply signal + + """ + pass + + def get_order(self): + """ Get current order, returns (field_name, gtk.SortType) """ + pass + + def set_order(self, field_name, sort_type): + """ Set current order """ + pass + +class LazyModel(gtk.GenericTreeModel): + def __init__(self, columns, calc_columns={}): + """ columns/calc_columns = {field_name: (column_num, column_type)} """ + gtk.GenericTreeModel.__init__(self) + + self._columns_by_name = {} + self._columns_by_num = {} + self._columns_types = {} + + for name, i in columns.items(): + self._columns_by_name[name] = i[0] + self._columns_by_num[i[0]] = name + self._columns_types[i[0]] = i[1] + + for name, i in calc_columns.items(): + self._columns_types[i[0]] = i[1] + + self._n_columns = max(self._columns_types.keys()) + 1 + + self._source = None + self._closing = False + self._view = None + + self.set_source(None, force=True) + self.set_view(None, force=True) + + def on_calc_value(self, row, column): + # stub + pass + + def get_source(self): + return self._source + + def set_source(self, source, force=False): + if self._source == source and not force: + return + + if self._source is not None: + self._source.disconnect_by_func(self.refresh) + self._source.disconnect_by_func(self._delayed_fetch_cb) + + self._source = source + if self._source is not None: + self._source.connect('objects-updated', self.refresh) + self._source.connect('row-delayed-fetch', self._delayed_fetch_cb) + + self.refresh() + + source = property(get_source, set_source) + + def get_view(self): + return self._view + + def set_view(self, view, force=False): + if self._view == view and not force: + return + + cursor = None + + if self._view is not None: + cursor = self._view.get_cursor() + try: + self._closing = True + self._view.set_model(None) + finally: + self._closing = False + + self._view = view + self._cache = {} + self._frame = (0, -1) + self._in_process = {} + self._postponed = [] + self._last_count = self._source and self._source.get_count() or 0 + + if self._source is not None and view is not None: + self._update_columns() + view.set_model(self) + if cursor is not None: + view.set_cursor(*cursor) + + view = property(get_view, set_view) + + def get_order(self): + if not self._source: + return None + order = self._source.get_order() + if order is None: + return None + return (self._columns_by_name[order[0]], order[1]) + + def set_order(self, column, order): + if not self._source: + return + self._source.set_order(self._columns_by_num[column], order) + self._update_columns() + + def refresh(self, sender=None): + if self._source is None or self._view is None: + return + + if self._last_count == 0: + self.set_view(self._view, force=True) + + self._update_columns() + + count = self._source.get_count() + + for i in range(self._last_count, count): + self.emit('row-inserted', (i,), self.get_iter((i,))) + + for i in reversed(range(count, self._last_count)): + self.emit('row-deleted', (i,)) + + if self._frame[0] >= count: + self._frame = (0, -1) + elif self._frame[1] >= count: + for i in range(count, self._frame[1]): + if self._cache.has_key(i): + del self._cache[i] + self._frame = (self._frame[0], count-1) + self._cache = {} + for i in range(self._frame[0], self._frame[1]+1): + self.emit('row-changed', (i,), self.get_iter((i,))) + + self._last_count = count + + def recalc(self, fields): + for i, row in self._cache.items(): + for field in fields: + if row.has_key(field): + del row[i] + self.emit('row-changed', (i,), self.get_iter((i,))) + + def get_row(self, pos, frame=None): + if not self._source: + return False + if not isinstance(pos, tuple): + pos = self.get_path(pos) + return self._get_row(pos[0], frame or (pos, pos)) + + def _delayed_fetch_cb(self, source, offset, object): + if not self._in_process.has_key(offset): + logging.debug('_delayed_fetch_cb: no offset=%s' % offset) + return + + logging.debug('_delayed_fetch_cb: get %s' % offset) + + path = (offset,) + iter = self.get_iter(path) + row = Row(self, path, iter, object) + + if self.in_frame(offset): + self._cache[offset] = row + + del self._in_process[offset] + self.emit('row-changed', path, iter) + if self._in_process: + return + + while self._postponed: + offset, force = self._postponed.pop() + if not force and not self.in_frame(offset): + continue + row = self.get_row((offset,)) + if row: + self.emit('row-changed', row.path, row.iter) + else: + break + + def _get_row(self, offset, frame): + def fetch(): + row = self._source.get_row(offset) + + if not row: + if row is not None: + logging.debug('_get_row: can not find row for %s' % offset) + return False + logging.debug('_get_row: wait for reply for %s' % offset) + self._in_process[offset] = True + return None + + row = Row(self, (offset,), self.get_iter(offset), row) + self._cache[offset] = row + return row + + out = self._cache.get(offset) + if out: + return out + + if frame[0] >= frame[1]: + # just return requested single row and do not change cache + # if requested frame has <= 1 rows + if self._in_process: + self._postponed.append((offset, True)) + return None + else: + return fetch() + + if frame != self._frame: + # switch to new frame + intersect_min = max(frame[0], self._frame[0]) + intersect_max = min(frame[1], self._frame[1]) + if intersect_min > intersect_max: + self._cache = {} + else: + for i in range(self._frame[0], intersect_min): + if self._cache.has_key(i): + del self._cache[i] + for i in range(intersect_max+1, self._frame[1]+1): + if self._cache.has_key(i): + del self._cache[i] + self._frame = frame + + if self._in_process: + self._postponed.append((offset, False)) + return None + + return fetch() + + def _update_columns(self): + order = self.get_order() + if not order or not hasattr(self._view, 'get_columns'): + return + + for column in self._view.get_columns(): + if column.get_sort_column_id() == order[0]: + column.props.sort_indicator = True + column.props.sort_order = order[1] + else: + column.props.sort_indicator = False + + def in_frame(self, offset): + return offset >= self._frame[0] and offset <= self._frame[1] + + # interface implementation ------------------------------------------------- + + def on_get_n_columns(self): + return self._n_columns + + def on_get_column_type(self, index): + return self._columns_types.get(index, bool) + + def on_iter_n_children(self, iter): + if iter is None and not self._closing: + return self._source.get_count() + else: + return 0 + + def on_get_value(self, offset, column): + if not self._view or offset >= self._source.get_count(): + return None + + # return value only if iter came from visible range + # (on setting model, gtk.TreeView scans all items) + range = self._view.get_visible_range() + if range and offset >= range[0][0] and offset <= range[1][0]: + row = self._get_row(offset, (range[0][0], range[1][0])) + return row and row[column] + + return None + + def on_iter_nth_child(self, iter, n): + return n + + def on_get_path(self, iter): + return (iter) + + def on_get_iter(self, path): + if self._source.get_count() and not self._closing: + return path[0] + else: + return False + + def on_iter_next(self, iter): + if iter != None: + if iter >= self._source.get_count() - 1 or self._closing: + 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 + +class Row: + def __init__(self, model, path, iter, object): + self.model = model + self.iter = iter + self.path = path + self.object = object + self.row = [None] * len(model._columns_by_name) + self._calced_row = {} + + for name, value in object.items(): + column = model._columns_by_name.get(str(name), -1) + if column != -1: + self.row[column] = value + + def __getitem__(self, key): + if isinstance(key, int): + if key < len(self.row): + return self.row[key] + else: + if self._calced_row.has_key(key): + return self._calced_row[key] + else: + value = self.model.on_calc_value(self, key) + if value is not None: + self._calced_row[key] = value + return value + else: + return self.object[key] + + def __setitem__(self, key, value): + if isinstance(key, int): + if key < len(self.row): + self.row[key] = value + else: + self._calced_row[key] = value + else: + self.object[key] = value + + def __delitem__(self, key): + if isinstance(key, int): + if key < len(self.row): + del self.row[key] + else: + del self._calced_row[key] + else: + del self.object[key] + + def __contains__(self, key): + if isinstance(key, int): + return key < len(self.row) + else: + return self.object.__contains__(key) + + def has_key(self, key): + return self.__contains__(key) + + def get(self, key, default=None): + if self.has_key(key): + return self.__getitem__(key) + else: + return default diff --git a/src/jarabe/journal/browse/localsource.py b/src/jarabe/journal/browse/localsource.py new file mode 100644 index 0000000..e19941e --- /dev/null +++ b/src/jarabe/journal/browse/localsource.py @@ -0,0 +1,41 @@ +# Copyright (C) 2009, Aleksey Lim +# +# 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 jarabe.journal.browse.source import Source + +class LocalSource(Source): + def __init__(self, resultset): + Source.__init__(self) + self._resultset = resultset + + def get_count(self): + return self._resultset.length + + def get_row(self, offset): + if offset >= self.get_count(): + return False + self._resultset.seek(offset) + return self._resultset.read() + + def get_order(self): + """ Get current order, returns (field_name, gtk.SortType) """ + pass + + def set_order(self, field_name, sort_type): + """ Set current order """ + pass diff --git a/src/jarabe/journal/browse/source.py b/src/jarabe/journal/browse/source.py new file mode 100644 index 0000000..64fa2ab --- /dev/null +++ b/src/jarabe/journal/browse/source.py @@ -0,0 +1,85 @@ +# Copyright (C) 2009, Aleksey Lim +# +# 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 gobject import property, GObject, SIGNAL_RUN_FIRST, TYPE_PYOBJECT + +FIELD_UID = 0 +FIELD_TITLE = 1 +FIELD_MTIME = 2 +FIELD_TIMESTAMP = 3 +FIELD_KEEP = 4 +FIELD_BUDDIES = 5 +FIELD_ICON_COLOR = 6 +FIELD_MIME_TYPE = 7 +FIELD_PROGRESS = 8 +FIELD_ACTIVITY = 9 +FIELD_MOUNT_POINT = 10 +FIELD_ACTIVITY_ID = 11 +FIELD_BUNDLE_ID = 12 + +FIELD_FAVORITE = 30 +FIELD_ICON = 31 +FIELD_MODIFY_TIME = 32 +FIELD_THUMB = 33 + +FIELDS_LIST = {'uid': (FIELD_UID, str), + 'title': (FIELD_TITLE, str), + 'mtime': (FIELD_MTIME, str), + 'timestamp': (FIELD_TIMESTAMP, int), + 'keep': (FIELD_KEEP, int), + 'buddies': (FIELD_BUDDIES, str), + 'icon-color': (FIELD_ICON_COLOR, str), + 'mime_type': (FIELD_MIME_TYPE, str), + 'progress': (FIELD_MIME_TYPE, str), + 'activity': (FIELD_ACTIVITY, str), + 'mountpoint': (FIELD_ACTIVITY, str), + 'activity_id': (FIELD_ACTIVITY_ID, str), + 'bundle_id': (FIELD_BUNDLE_ID, str), + + 'favorite': (FIELD_FAVORITE, bool), + 'icon': (FIELD_ICON, str), + 'modify_time': (FIELD_MODIFY_TIME, str), + 'thumb': (FIELD_THUMB, gtk.gdk.Pixbuf)} + +class Source(GObject): + __gsignals__ = { + 'objects-updated': (SIGNAL_RUN_FIRST, None, []), + 'row-delayed-fetch': (SIGNAL_RUN_FIRST, None, 2*[TYPE_PYOBJECT]) + } + + def get_count(self): + """ Returns number of objects """ + pass + + def get_row(self, offset): + """ Get object + + Returns: + objects in dict {field_name: value, ...} + False can't fint object + None wait for reply signal + + """ + pass + + def get_order(self): + """ Get current order, returns (field_name, gtk.SortType) """ + pass + + def set_order(self, field_name, sort_type): + """ Set current order """ + pass diff --git a/src/jarabe/journal/browse/tableview.py b/src/jarabe/journal/browse/tableview.py new file mode 100644 index 0000000..34fe42b --- /dev/null +++ b/src/jarabe/journal/browse/tableview.py @@ -0,0 +1,264 @@ +# Copyright (C) 2009, Aleksey Lim +# +# 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 +import hippo +import math +import gobject +import logging + +from sugar.graphics import style +from sugar.graphics.roundbox import CanvasRoundBox + +COLOR_BACKGROUND = style.COLOR_WHITE +COLOR_SELECTED = style.COLOR_TEXT_FIELD_GREY + +class TableCell: + def __init__(self): + self.row = None + + def fillin(self): + pass + + def on_release(self, widget, event): + pass + +class TableView(gtk.Viewport): + def __init__(self, cell_class, rows, cols): + gobject.GObject.__init__(self) + + self._cell_class = cell_class + self._rows = rows + self._cols = cols + self._cells = [] + self._model = None + self._hover_selection = True + self._full_adjustment = None + self._full_height = 0 + self._selected_cell = None + + self._table = gtk.Table() + self._table.show() + + for y in range(self._rows + 1): + self._cells.append(self._cols * [None]) + for x in range(self._cols): + canvas = hippo.Canvas() + canvas.show() + canvas.modify_bg(gtk.STATE_NORMAL, + COLOR_BACKGROUND.get_gdk_color()) + + sel_box = CanvasRoundBox() + sel_box.props.border_color = COLOR_BACKGROUND.get_int() + canvas.set_root(sel_box) + canvas.root = sel_box + + cell = self._cell_class() + sel_box.append(cell, hippo.PACK_EXPAND) + + if self._hover_selection: + canvas.connect('enter-notify-event', + self.__enter_notify_event_cb, cell) + canvas.connect('leave-notify-event', + self.__leave_notify_event_cb) + canvas.connect('button-release-event', + self.__button_release_event_cb, cell) + + self._table.attach(canvas, x, x + 1, y, y + 1, + gtk.EXPAND | gtk.FILL, gtk.EXPAND | gtk.FILL, 0, 0) + self._cells[y][x] = (canvas, cell) + + smooth_box = gtk.ScrolledWindow() + smooth_box.set_policy(gtk.POLICY_NEVER, gtk.POLICY_NEVER) + smooth_box.show() + smooth_box.add_with_viewport(self._table) + self.add(smooth_box) + + self.connect('key-press-event', self.__key_press_event_cb) + + def do_set_scroll_adjustments(self, hadj, vadj): + if vadj is None: + return + if self._full_adjustment is not None: + self._full_adjustment.disconnect_by_func( + self.__adjustment_value_changed) + self._full_adjustment = vadj + self._full_adjustment.connect('value-changed', + self.__adjustment_value_changed) + self._setup_adjustment() + + def get_size(self): + return (self._cols, self._rows) + + def get_cursor(self): + frame = self._get_frame() + return (frame[0],) + + def set_cursor(self, cursor): + if self._full_adjustment is None: + return + #self._full_adjustment.props.value = cursor + + def get_model(self): + return self._model + + def set_model(self, model): + if self._model == model: + return + if self._model: + self._model.disconnect_by_func(self.__row_changed_cb) + self._model.disconnect_by_func(self.__table_resized_cb) + self._model = model + if model: + self._model.connect('row-changed', self.__row_changed_cb) + self._model.connect('row-inserted', self.__table_resized_cb) + self._model.connect('row-deleted', self.__table_resized_cb) + self._setup_adjustment() + + model = gobject.property(type=object, + getter=get_model, setter=set_model) + + def get_hover_selection(self): + return self._hover_selection + + def set_hover_selection(self, value): + self._hover_selection = value + + hover_selection = gobject.property(type=object, + getter=get_hover_selection, setter=set_hover_selection) + + def get_visible_range(self): + frame = self._get_frame() + return ((frame[0],), (frame[1],)) + + def do_size_allocate(self, alloc): + gtk.Viewport.do_size_allocate(self, alloc) + self._full_height = alloc.height + 100 + self._table.set_size_request(-1, self._full_height) + self._setup_adjustment() + + def _fillin_cell(self, canvas, cell): + if cell.row is None: + cell.set_visible(False) + else: + cell.fillin() + cell.set_visible(True) + + bg_color = COLOR_BACKGROUND + if self._selected_cell == cell: + if cell.get_visible(): + bg_color = COLOR_SELECTED + canvas.root.props.background_color = bg_color.get_int() + + def _setup_adjustment(self): + if self._full_adjustment is None or self._full_height == 0: + return + + adj = self._full_adjustment.props + + if self._model is None: + adj.upper = 0 + adj.value = 0 + return + + if self._cols == 0: + adj.upper = 0 + else: + count = self._model.iter_n_children(None) + adj.upper = int(math.ceil(float(count) / self._cols)) + + adj.value = min(adj.value, adj.upper - self._rows) + adj.page_size = self._rows + adj.page_increment = self._rows + self._full_adjustment.changed() + + self.__adjustment_value_changed(self._full_adjustment) + + def _get_frame(self): + return (int(self._full_adjustment.props.value) * self._cols, + (int(self._full_adjustment.props.value) + self._rows) * self._cols - 1) + + def __row_changed_cb(self, model, path, iter): + range = self._get_frame() + if path[0] < range[0] or path[0] > range[1]: + return + + y = (path[0] - range[0]) / self._cols + x = (path[0] - range[0]) % self._cols + + canvas, cell = self._cells[y][x] + cell.row = self._model.get_row(path) + self._fillin_cell(canvas, cell) + + def __table_resized_cb(self, model=None, path=None, iter=None): + self._setup_adjustment() + + def __key_press_event_cb(self, widget, event): + if self._full_adjustment is None or self._full_height == 0: + return + + adj = self._full_adjustment.props + uplimit = adj.upper - self._rows + + if event.keyval == gtk.keysyms.Up: + adj.value -= 1 + elif event.keyval == gtk.keysyms.Down: + if adj.value + 1 <= uplimit: + adj.value += 1 + elif event.keyval in (gtk.keysyms.Page_Up, gtk.keysyms.KP_Page_Up): + adj.value -= self._rows + elif event.keyval in (gtk.keysyms.Page_Down, gtk.keysyms.KP_Page_Down): + if adj.value + self._rows <= uplimit: + adj.value += self._rows + elif event.keyval in (gtk.keysyms.Home, gtk.keysyms.KP_Home): + adj.value = 0 + elif event.keyval in (gtk.keysyms.End, gtk.keysyms.KP_End): + adj.value = uplimit + else: + return False + + return True + + def __button_release_event_cb(self, widget, event, cell): + cell.on_release(widget, event) + + def __enter_notify_event_cb(self, canvas, event, cell): + if cell.get_visible(): + canvas.root.props.background_color = COLOR_SELECTED.get_int() + self._selected_cell = cell + + def __leave_notify_event_cb(self, canvas, event): + canvas.root.props.background_color = COLOR_BACKGROUND.get_int() + self._selected_cell = None + + def __adjustment_value_changed(self, adjustment): + if self._model: + count = self._model.iter_n_children(None) + else: + count = 0 + cell_num = int(adjustment.props.value) * self._cols + + for y in range(self._rows): + for x in range(self._cols): + canvas, cell = self._cells[y][x] + + cell.row = None + if cell_num < count: + cell.row = self._model.get_row((cell_num,), + self._get_frame()) + + self._fillin_cell(canvas, cell) + cell_num += 1 diff --git a/src/jarabe/journal/browse/treeview.py b/src/jarabe/journal/browse/treeview.py new file mode 100644 index 0000000..0980405 --- /dev/null +++ b/src/jarabe/journal/browse/treeview.py @@ -0,0 +1,175 @@ +# Copyright (C) 2009, Aleksey Lim +# +# 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 +import gobject +import logging + +from sugar.graphics.palette import Invoker + +_SHOW_PALETTE_TIMEOUT = 200 + +class TreeView(gtk.TreeView): + def __init__(self): + gtk.TreeView.__init__(self) + self._invoker = _TreeInvoker(self) + + def set_cursor(self, path, column, edit=False): + if path is not None: + gtk.TreeView.set_cursor(self, path, column, edit) + + def append_column(self, column): + if isinstance(column, TreeViewColumn): + column.view = self + return gtk.TreeView.append_column(self, column) + + def create_palette(self): + return self._invoker.cell_palette + +class TreeViewColumn(gtk.TreeViewColumn): + def __init__(self, title=None, cell=None, **kwargs): + gtk.TreeViewColumn.__init__(self, title, cell, **kwargs) + self.view = None + self._order_by = None + self.palette_cb = None + self.connect('clicked', self._clicked_cb) + + def set_sort_column_id(self, field): + self.props.clickable = True + self._order_by = field + + def get_sort_column_id(self): + return self._order_by + + def _clicked_cb(self, column): + if not self.view: + return + + if self.props.sort_indicator: + if self.props.sort_order == gtk.SORT_DESCENDING: + new_order = gtk.SORT_ASCENDING + else: + new_order = gtk.SORT_DESCENDING + else: + new_order = gtk.SORT_ASCENDING + + self.view.get_model().set_order(self._order_by, new_order) + +class _TreeInvoker(Invoker): + def __init__(self, tree=None): + Invoker.__init__(self) + self._position_hint = self.AT_CURSOR + + self._tree = None + self.cell_palette = None + self._palette_pos = None + self._enter_timeout = None + + self._enter_hid = None + self._motion_hid = None + self._leave_hid = None + self._button_hid = None + + if tree: + self.attach(tree) + + def get_toplevel(self): + return self._tree.get_toplevel() + + def attach(self, tree): + self._tree = tree + self._enter_hid = tree.connect('enter-notify-event', self._enter_cb) + self._motion_hid = tree.connect('motion-notify-event', self._enter_cb) + self._leave_hid = tree.connect('leave-notify-event', self._leave_cb) + self._button_hid = tree.connect('button-release-event', self._button_cb) + Invoker.attach(self, tree) + + def detach(self): + Invoker.detach(self) + self._tree.disconnect(self._enter_hid) + self._tree.disconnect(self._motion_hid) + self._tree.disconnect(self._leave_hid) + self._tree.disconnect(self._button_cb) + + def _close_palette(self): + if self._enter_timeout: + gobject.source_remove(self._enter_timeout) + self._enter_timeout = None + self.cell_palette = None + self._palette_pos = None + + def _open_palette(self, notify, force): + if self._enter_timeout: + gobject.source_remove(self._enter_timeout) + self._enter_timeout = None + + coords = self._tree.convert_widget_to_bin_window_coords( + *self._tree.get_pointer()) + + pos = self._tree.get_path_at_pos(*coords) + if not pos: + self._close_palette() + return False + + path, column, x, y = pos + if not hasattr(column, 'palette_cb') or not column.palette_cb: + self._close_palette() + return False + + row = self._tree.props.model.get_row(path) + if not row: + logging.debug('_open_palette: wait for row %s' % path) + self._enter_timeout = gobject.timeout_add(500, self._open_palette, + self.notify_mouse_enter, False) + return False + + palette = column.palette_cb(self._tree.props.model, row, x, y) + if not palette: + self._close_palette() + return False + + if self._palette_pos != (path, column) or self.cell_palette != palette: + if self.palette: + self.palette.popdown(True) + self.palette = None + + self._palette_pos = (path, column) + self.cell_palette = palette + notify() + + return False + + def notify_popup(self): + Invoker.notify_popup(self) + + def notify_popdown(self): + Invoker.notify_popdown(self) + + def _enter_cb(self, widget, event): + if self._enter_timeout: + gobject.source_remove(self._enter_timeout) + self._enter_timeout = gobject.timeout_add(_SHOW_PALETTE_TIMEOUT, + self._open_palette, self.notify_mouse_enter, False) + + def _leave_cb(self, widget, event): + self.notify_mouse_leave() + self._close_palette() + + def _button_cb(self, widget, event): + if event.button == 3: + return self._open_palette(self.notify_right_click, True) + else: + return False diff --git a/src/jarabe/journal/journalactivity.py b/src/jarabe/journal/journalactivity.py index 08a5a0f..10a89bf 100644 --- a/src/jarabe/journal/journalactivity.py +++ b/src/jarabe/journal/journalactivity.py @@ -18,7 +18,7 @@ import logging from gettext import gettext as _ import sys -import traceback +import traceback import uuid import gtk @@ -34,7 +34,7 @@ from sugar import wm from jarabe.model import bundleregistry from jarabe.journal.journaltoolbox import MainToolbox, DetailToolbox -from jarabe.journal.listview import ListView +from jarabe.journal.objectsview import ObjectsView from jarabe.journal.detailview import DetailView from jarabe.journal.volumestoolbar import VolumesToolbar from jarabe.journal import misc @@ -105,11 +105,14 @@ class JournalActivity(Window): logging.debug("STARTUP: Loading the journal") Window.__init__(self) + accel_group = gtk.AccelGroup() + self.set_data('sugar-accel-group', accel_group) + self.add_accel_group(accel_group) + 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 @@ -131,10 +134,10 @@ class JournalActivity(Window): model.updated.connect(self.__model_updated_cb) model.deleted.connect(self.__model_deleted_cb) - self._dbus_service = JournalActivityDBusService(self) + self._dbus_service = JournalActivityDBusService(self) self.iconify() - + self._critical_space_alert = None self._check_available_space() @@ -152,11 +155,11 @@ class JournalActivity(Window): self._main_toolbox = MainToolbox() self._main_view = gtk.VBox() - self._list_view = ListView() - self._list_view.connect('detail-clicked', self.__detail_clicked_cb) - self._list_view.connect('clear-clicked', self.__clear_clicked_cb) - self._main_view.pack_start(self._list_view) - self._list_view.show() + self._objects_view = ObjectsView() + self._objects_view.connect('clear-clicked', self.__clear_clicked_cb) + self._objects_view.connect('detail-clicked', self.__detail_clicked_cb) + self._main_view.pack_start(self._objects_view) + self._objects_view.show() self._volumes_toolbar = VolumesToolbar() self._volumes_toolbar.connect('volume-changed', @@ -165,6 +168,7 @@ class JournalActivity(Window): search_toolbar = self._main_toolbox.search_toolbar search_toolbar.connect('query-changed', self._query_changed_cb) + search_toolbar.connect('view-changed', self.__view_changed_cb) search_toolbar.set_mount_point('/') def _setup_secondary_view(self): @@ -195,9 +199,12 @@ class JournalActivity(Window): self.show_main_view() def _query_changed_cb(self, toolbar, query): - self._list_view.update_with_query(query) + self._objects_view.update_with_query(query) self.show_main_view() + def __view_changed_cb(self, sender, view): + self._objects_view.change_view(view) + def show_main_view(self): if self.toolbox != self._main_toolbox: self.set_toolbox(self._main_toolbox) @@ -261,7 +268,7 @@ class JournalActivity(Window): def _focus_in_event_cb(self, window, event): self.search_grab_focus() - self._list_view.update_dates() + self._objects_view.update_dates() def _check_for_bundle(self, object_id): registry = bundleregistry.get_registry() @@ -300,12 +307,12 @@ class JournalActivity(Window): 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) + self._objects_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) + self._objects_view.set_is_visible(visible) def _check_available_space(self): ''' Check available space on device diff --git a/src/jarabe/journal/journaltoolbox.py b/src/jarabe/journal/journaltoolbox.py index 201bf76..a201550 100644 --- a/src/jarabe/journal/journaltoolbox.py +++ b/src/jarabe/journal/journaltoolbox.py @@ -26,6 +26,7 @@ import gobject import gio import gtk +from sugar.graphics.radiotoolbutton import RadioToolButton from sugar.graphics.toolbox import Toolbox from sugar.graphics.toolcombobox import ToolComboBox from sugar.graphics.toolbutton import ToolButton @@ -73,7 +74,10 @@ class SearchToolbar(gtk.Toolbar): __gsignals__ = { 'query-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, - ([object])) + ([object])), + 'view-changed': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([object])) } def __init__(self): @@ -114,10 +118,34 @@ class SearchToolbar(gtk.Toolbar): #self.insert(tool_item, -1) #tool_item.show() + separator = gtk.SeparatorToolItem() + separator.props.draw = False + separator.set_expand(True) + self.insert(separator, -1) + separator.show() + + list_button = RadioToolButton(named_icon='view-list') + list_button.props.tooltip = _('List view') + list_button.props.accelerator = _('1') + list_button.connect('toggled', self.__view_button_toggled_cb, 0) + self.insert(list_button, -1) + list_button.show() + + thumb_button = RadioToolButton(named_icon='view-thumbs') + thumb_button.props.group = list_button + thumb_button.props.tooltip = _('Thumbs view') + thumb_button.props.accelerator = _('2') + thumb_button.connect('toggled', self.__view_button_toggled_cb, 1) + self.insert(thumb_button, -1) + thumb_button.show() + self._query = self._build_query() self.refresh_filters() + def __view_button_toggled_cb(self, button, view_num): + self.emit('view-changed', view_num) + def give_entry_focus(self): self._search_entry.grab_focus() diff --git a/src/jarabe/journal/listmodel.py b/src/jarabe/journal/listmodel.py index 917fbb1..6f0d4f1 100644 --- a/src/jarabe/journal/listmodel.py +++ b/src/jarabe/journal/listmodel.py @@ -65,35 +65,18 @@ class ListModel(gtk.GenericTreeModel, gtk.TreeDragSource): COLUMN_BUDDY_3: object, COLUMN_BUDDY_2: object} - _PAGE_SIZE = 10 - - def __init__(self, query): + def __init__(self, result_set): gobject.GObject.__init__(self) self._last_requested_index = None self._cached_row = None - self._result_set = model.find(query, ListModel._PAGE_SIZE) + self._result_set = result_set 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]) diff --git a/src/jarabe/journal/listview.py b/src/jarabe/journal/listview.py index 251388d..36cd06a 100644 --- a/src/jarabe/journal/listview.py +++ b/src/jarabe/journal/listview.py @@ -20,12 +20,11 @@ import time import gobject import gtk -import hippo import gconf import pango from sugar.graphics import style -from sugar.graphics.icon import CanvasIcon, Icon, CellRendererIcon +from sugar.graphics.icon import CellRendererIcon from sugar.graphics.xocolor import XoColor from sugar import util @@ -61,32 +60,14 @@ class BaseListView(gtk.Bin): __gtype_name__ = 'JournalBaseListView' __gsignals__ = { - 'clear-clicked': (gobject.SIGNAL_RUN_FIRST, - gobject.TYPE_NONE, - ([])) + 'detail-clicked': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([object])) } def __init__(self): - self._query = {} - self._model = None - self._progress_bar = None - self._last_progress_bar_pulse = None - - gobject.GObject.__init__(self) - - self.connect('destroy', self.__destroy_cb) - - 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.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() + gtk.TreeView.__init__(self) + self.props.fixed_height_mode = True self.cell_title = None self.cell_icon = None @@ -94,32 +75,45 @@ class BaseListView(gtk.Bin): self.date_column = None self._add_columns() - 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) + self.enable_model_drag_source(gtk.gdk.BUTTON1_MASK, + [('text/uri-list', 0, 0), ('journal-object-id', 0, 0)], + gtk.gdk.ACTION_COPY) + + self.cell_title.props.editable = True + self.cell_title.connect('edited', self.__cell_title_edited_cb) - # Auto-update stuff - self._fully_obscured = True - self._dirty = False - self._refresh_idle_handler = None - self._update_dates_timer = None + self.cell_icon.connect('clicked', self.__icon_clicked_cb) + self.cell_icon.connect('detail-clicked', self.__detail_clicked_cb) - model.created.connect(self.__model_created_cb) - model.updated.connect(self.__model_updated_cb) - model.deleted.connect(self.__model_deleted_cb) + cell_detail = CellRendererDetail(self) + cell_detail.connect('clicked', self.__detail_cell_clicked_cb) - def __model_created_cb(self, sender, **kwargs): - self._set_dirty() + 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.append_column(column) - def __model_updated_cb(self, sender, **kwargs): - self._set_dirty() + self.connect('notify::hover-selection', + self.__notify_hover_selection_cb) - def __model_deleted_cb(self, sender, **kwargs): - self._set_dirty() + def __notify_hover_selection_cb(self, widget, pspec): + self.cell_icon.props.show_palette = not self.props.hover_selection + + 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 def _add_columns(self): - cell_favorite = CellRendererFavorite(self.tree_view) + cell_favorite = CellRendererFavorite(self) cell_favorite.connect('clicked', self.__favorite_clicked_cb) column = gtk.TreeViewColumn('') @@ -127,9 +121,9 @@ class BaseListView(gtk.Bin): 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.append_column(column) - self.cell_icon = CellRendererActivityIcon(self.tree_view) + self.cell_icon = CellRendererActivityIcon(self) column = gtk.TreeViewColumn('') column.props.sizing = gtk.TREE_VIEW_COLUMN_FIXED @@ -137,8 +131,8 @@ class BaseListView(gtk.Bin): 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) + ListModel.COLUMN_ICON_COLOR) + self.append_column(column) self.cell_title = gtk.CellRendererText() self.cell_title.props.ellipsize = pango.ELLIPSIZE_MIDDLE @@ -152,15 +146,15 @@ class BaseListView(gtk.Bin): self._title_column.add_attribute(self.cell_title, 'markup', ListModel.COLUMN_TITLE) self._title_column.connect('clicked', self.__header_clicked_cb) - self.tree_view.append_column(self._title_column) + self.append_column(self._title_column) buddies_column = gtk.TreeViewColumn('') buddies_column.props.sizing = gtk.TREE_VIEW_COLUMN_FIXED - self.tree_view.append_column(buddies_column) + self.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, + cell_icon = CellRendererBuddy(self, column_index=column_index) buddies_column.pack_start(cell_icon) buddies_column.props.fixed_width += cell_icon.props.width @@ -186,7 +180,7 @@ class BaseListView(gtk.Bin): self.date_column.pack_start(cell_text) self.date_column.add_attribute(cell_text, 'text', ListModel.COLUMN_DATE) self.date_column.connect('clicked', self.__header_clicked_cb) - self.tree_view.append_column(self.date_column) + self.append_column(self.date_column) def __header_clicked_cb(self, column_clicked): if column_clicked == self._title_column: @@ -206,7 +200,7 @@ class BaseListView(gtk.Bin): else: self._query['order_by'] = ['+timestamp'] - self.refresh() + self._refresh() # Need to update the column indicators after the model has been reset if self._query['order_by'] == ['-timestamp']: @@ -237,19 +231,8 @@ class BaseListView(gtk.Bin): width, height_ = layout.get_size() return pango.PIXELS(width) - def do_size_allocate(self, allocation): - self.allocation = allocation - self.child.size_allocate(allocation) - - def do_size_request(self, requisition): - requisition.width, requisition.height = self.child.size_request() - - def __destroy_cb(self, widget): - if self._model is not None: - self._model.stop() - def __favorite_set_data_cb(self, column, cell, tree_model, tree_iter): - favorite = self._model[tree_iter][ListModel.COLUMN_FAVORITE] + favorite = self.get_model()[tree_iter][ListModel.COLUMN_FAVORITE] if favorite: client = gconf.client_get_default() color = XoColor(client.get_string('/desktop/sugar/user/color')) @@ -259,7 +242,7 @@ class BaseListView(gtk.Bin): cell.props.fill_color = style.COLOR_WHITE.get_svg() def __favorite_clicked_cb(self, cell, path): - row = self._model[path] + row = self.get_model()[path] metadata = model.get(row[ListModel.COLUMN_UID]) if metadata['keep'] == '1': metadata['keep'] = '0' @@ -267,212 +250,37 @@ class BaseListView(gtk.Bin): metadata['keep'] = '1' model.write(metadata, update_mtime=False) - def update_with_query(self, query_dict): - logging.debug('ListView.update_with_query') - self._query = query_dict - - if 'order_by' not in self._query: - self._query['order_by'] = ['+timestamp'] - - self.refresh() - - def refresh(self): - logging.debug('ListView.refresh query %r', self._query) - self._stop_progress_bar() - self._start_progress_bar() - - if self._model is not None: - self._model.stop() - - 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, tree_model): - self._stop_progress_bar() - - # 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) - - if len(tree_model) == 0: - if self._is_query_empty(): - self._show_message(MESSAGE_EMPTY_JOURNAL) - else: - self._show_message(MESSAGE_NO_MATCH) - else: - self._clear_message() - - def _is_query_empty(self): - # FIXME: This is a hack, we shouldn't have to update this every time - # a new search term is added. - if self._query.get('query', '') or self._query.get('mime_type', '') or \ - self._query.get('keep', '') or self._query.get('mtime', '') or \ - self._query.get('activity', ''): - return False - else: - return True - - 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): - alignment = gtk.Alignment(xalign=0.5, yalign=0.5, xscale=0.5) - self.remove(self.child) - self.add(alignment) - alignment.show() - - self._progress_bar = gtk.ProgressBar() - self._progress_bar.props.pulse_step = 0.01 - self._last_progress_bar_pulse = time.time() - alignment.add(self._progress_bar) - self._progress_bar.show() - - def _stop_progress_bar(self): - 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) - canvas.set_root(box) - - 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()) - box.append(icon) - - 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) - - 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.remove(self.child) - self.add(self._scrolled_window) - self._scrolled_window.show() - def update_dates(self): logging.debug('ListView.update_dates') - visible_range = self.tree_view.get_visible_range() + visible_range = self.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) + x, y, width, height = self.get_cell_area(path, self.date_column) + x, y = self.convert_tree_to_widget_coords(x, y) + self.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.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.refresh() - if self._update_dates_timer is None: - logging.debug('Adding date updating timer') - self._update_dates_timer = \ - gobject.timeout_add_seconds(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__ = 'JournalListView' - - __gsignals__ = { - 'detail-clicked': (gobject.SIGNAL_RUN_FIRST, - gobject.TYPE_NONE, - ([object])) - } - - def __init__(self): - BaseListView.__init__(self) - - self.cell_title.props.editable = True - self.cell_title.connect('edited', self.__cell_title_edited_cb) - - self.cell_icon.connect('clicked', self.__icon_clicked_cb) - self.cell_icon.connect('detail-clicked', self.__detail_clicked_cb) - - cell_detail = CellRendererDetail(self.tree_view) - cell_detail.connect('clicked', self.__detail_cell_clicked_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) + next_iter = self.get_model().iter_next( + self.get_model().get_iter(path)) + path = self.get_model().get_path(next_iter) def __detail_cell_clicked_cb(self, cell, path): - row = self.tree_view.get_model()[path] + row = self.get_model()[path] self.emit('detail-clicked', row[ListModel.COLUMN_UID]) def __detail_clicked_cb(self, cell, uid): self.emit('detail-clicked', uid) def __icon_clicked_cb(self, cell, path): - row = self.tree_view.get_model()[path] + row = self.get_model()[path] metadata = model.get(row[ListModel.COLUMN_UID]) misc.resume(metadata) def __cell_title_edited_cb(self, cell, path, new_text): - row = self._model[path] + row = self.get_model()[path] metadata = model.get(row[ListModel.COLUMN_UID]) metadata['title'] = new_text model.write(metadata, update_mtime=False) diff --git a/src/jarabe/journal/objectchooser.py b/src/jarabe/journal/objectchooser.py index 31bdba8..ea9c432 100644 --- a/src/jarabe/journal/objectchooser.py +++ b/src/jarabe/journal/objectchooser.py @@ -24,7 +24,7 @@ import wnck from sugar.graphics import style from sugar.graphics.toolbutton import ToolButton -from jarabe.journal.listview import BaseListView +from jarabe.journal.objectsview import ObjectsView from jarabe.journal.listmodel import ListModel from jarabe.journal.journaltoolbox import SearchToolbar from jarabe.journal.volumestoolbar import VolumesToolbar @@ -84,15 +84,16 @@ class ObjectChooser(gtk.Window): vbox.pack_start(self._toolbar, expand=False) self._toolbar.show() - self._list_view = ChooserListView() + self._list_view = ObjectsView() + self._list_view.props.hover_selection = True self._list_view.connect('entry-activated', self.__entry_activated_cb) vbox.pack_start(self._list_view) self._list_view.show() self._toolbar.set_mount_point('/') - + width = gtk.gdk.screen_width() - style.GRID_CELL_SIZE * 2 - height = gtk.gdk.screen_height() - style.GRID_CELL_SIZE * 2 + height = gtk.gdk.screen_height() - style.GRID_CELL_SIZE * 2 self.set_size_request(width, height) if what_filter: @@ -161,39 +162,3 @@ class TitleBox(VolumesToolbar): self.insert(tool_item, -1) tool_item.show() - -class ChooserListView(BaseListView): - __gtype_name__ = 'ChooserListView' - - __gsignals__ = { - 'entry-activated': (gobject.SIGNAL_RUN_FIRST, - gobject.TYPE_NONE, - ([str])), - } - - def __init__(self): - BaseListView.__init__(self) - - self.cell_icon.props.show_palette = False - self.tree_view.props.hover_selection = True - - self.tree_view.connect('button-release-event', - self.__button_release_event_cb) - - def __entry_activated_cb(self, entry): - self.emit('entry-activated', entry) - - def __button_release_event_cb(self, tree_view, event): - if event.window != tree_view.get_bin_window(): - return False - - pos = tree_view.get_path_at_pos(event.x, event.y) - if pos is None: - return False - - path, column_, x_, y_ = pos - uid = tree_view.get_model()[path][ListModel.COLUMN_UID] - self.emit('entry-activated', uid) - - return False - diff --git a/src/jarabe/journal/objectsview.py b/src/jarabe/journal/objectsview.py new file mode 100644 index 0000000..fca232b --- /dev/null +++ b/src/jarabe/journal/objectsview.py @@ -0,0 +1,315 @@ +# Copyright (C) 2009, Tomeu Vizoso, Aleksey Lim +# +# 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 time + +import gobject +import gtk +import hippo + +from sugar.graphics import style +from sugar.graphics.icon import CanvasIcon, Icon + +from jarabe.journal import model +from jarabe.journal.listview import ListView +from jarabe.journal.thumbsview import ThumbsView +from jarabe.journal.listmodel import ListModel +from jarabe.journal.browse.lazymodel import LazyModel +from jarabe.journal.browse.localsource import LocalSource +from jarabe.journal.browse.source import * + +UPDATE_INTERVAL = 300 + +MESSAGE_EMPTY_JOURNAL = 0 +MESSAGE_NO_MATCH = 1 + +VIEW_LIST = 0 +VIEW_THUMBS = 1 + +VIEW_TYPES = [ListView, ThumbsView] + +PAGE_SIZE = 10 + +class ObjectsView(gtk.Bin): + __gsignals__ = { + 'clear-clicked': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([])), + 'detail-clicked': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([object])), + 'entry-activated': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([str])), + } + + def __init__(self): + gobject.GObject.__init__(self) + + self._query = {} + self._result_set = None + self._progress_bar = None + self._last_progress_bar_pulse = None + self._model = LazyModel(FIELDS_LIST) + self._view_widgets = [] + self._view = VIEW_LIST + + self.connect('destroy', self.__destroy_cb) + + for view_class in VIEW_TYPES: + widget = gtk.ScrolledWindow() + widget.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) + widget.show() + + view = view_class() + view.modify_base(gtk.STATE_NORMAL, + style.COLOR_WHITE.get_gdk_color()) + view.connect('detail-clicked', self.__detail_clicked_cb) + view.connect('button-release-event', self.__button_release_event_cb) + view.show() + + is_view_scrollable = view.set_scroll_adjustments(None, None) + if is_view_scrollable: + widget.add(view) + else: + widget.add_with_viewport(view) + + widget.view = view + self._view_widgets.append(widget) + + # Auto-update stuff + self._fully_obscured = True + self._dirty = False + self._refresh_idle_handler = None + self._update_dates_timer = None + + model.created.connect(self.__model_created_cb) + model.updated.connect(self.__model_updated_cb) + model.deleted.connect(self.__model_deleted_cb) + + def set_hover_selection(self, hover_selection): + for i in self._view_widgets: + i.view.props.hover_selection = hover_selection + + hover_selection = gobject.property(type=bool, default=False, + setter=set_hover_selection) + + def update_with_query(self, query_dict): + logging.debug('ListView.update_with_query') + self._query = query_dict + + if 'order_by' not in self._query: + self._query['order_by'] = ['+timestamp'] + + self._refresh() + + def update_dates(self): + if self._view == VIEW_LIST: + # TODO in 0.88 VIEW_LIST will use lazymodel + self._view_widgets[VIEW_LIST].view.update_dates() + return + self._model.recalc([FIELD_MODIFY_TIME]) + + def change_view(self, view): + self._view = view + if self.child is not None: + self.remove(self.child) + self.add(self._view_widgets[view]) + self._view_widgets[view].show() + if view == VIEW_LIST: + # TODO in 0.88 VIEW_LIST will use lazymodel + return + self._model.view = self._view_widgets[view].view + + 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._refresh() + if self._update_dates_timer is None: + logging.debug('Adding date updating timer') + self._update_dates_timer = \ + gobject.timeout_add_seconds(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 _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, 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() + + def __result_set_ready_cb(self, **kwargs): + self._stop_progress_bar() + + if self._result_set.length == 0: + if self._is_query_empty(): + self._show_message(MESSAGE_EMPTY_JOURNAL) + else: + self._show_message(MESSAGE_NO_MATCH) + else: + # TODO in 0.88 VIEW_LIST will use lazymodel + self._view_widgets[VIEW_LIST].view.set_model( + ListModel(self._result_set)) + self._model.source = LocalSource(self._result_set) + self.change_view(self._view) + + def __result_set_progress_cb(self, **kwargs): + 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 _is_query_empty(self): + # FIXME: This is a hack, we shouldn't have to update this every time + # a new search term is added. + if self._query.get('query', '') or self._query.get('mime_type', '') or \ + self._query.get('keep', '') or self._query.get('mtime', '') or \ + self._query.get('activity', ''): + return False + else: + return True + + def __model_created_cb(self, sender, **kwargs): + self._set_dirty() + + def __model_updated_cb(self, sender, **kwargs): + self._set_dirty() + + def __model_deleted_cb(self, sender, **kwargs): + self._set_dirty() + + def __destroy_cb(self, widget): + if self._result_set is not None: + self._result_set.stop() + + def _start_progress_bar(self): + alignment = gtk.Alignment(xalign=0.5, yalign=0.5, xscale=0.5) + if self.child is not None: + self.remove(self.child) + self.add(alignment) + alignment.show() + + self._progress_bar = gtk.ProgressBar() + self._progress_bar.props.pulse_step = 0.01 + self._last_progress_bar_pulse = time.time() + alignment.add(self._progress_bar) + self._progress_bar.show() + + def _stop_progress_bar(self): + if self.child != self._progress_bar: + return + if self.child is not None: + self.remove(self.child) + self.add(self._view_widgets[self._view]) + self._view_widgets[self._view].show() + + def _show_message(self, message): + canvas = hippo.Canvas() + if self.child is not None: + 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) + canvas.set_root(box) + + 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()) + box.append(icon) + + 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) + + 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 _set_dirty(self): + if self._fully_obscured: + self._dirty = True + else: + self._refresh() + + def __update_dates_timer_cb(self): + self.update_dates() + return True + + def __detail_clicked_cb(self, list_view, object_id): + self.emit('detail-clicked', object_id) + + def __button_release_event_cb(self, tree_view, event): + if not tree_view.props.hover_selection: + return False + + if event.window != tree_view.get_bin_window(): + return False + + pos = tree_view.get_path_at_pos(event.x, event.y) + if pos is None: + return False + + path, column_, x_, y_ = pos + uid = tree_view.get_model()[path][FIELD_UID] + self.emit('entry-activated', uid) + + return False + + def do_size_allocate(self, allocation): + self.allocation = allocation + self.child.size_allocate(allocation) + + def do_size_request(self, requisition): + requisition.width, requisition.height = self.child.size_request() diff --git a/src/jarabe/journal/thumbsview.py b/src/jarabe/journal/thumbsview.py new file mode 100644 index 0000000..4dca8df --- /dev/null +++ b/src/jarabe/journal/thumbsview.py @@ -0,0 +1,41 @@ +# Copyright (C) 2009, Aleksey Lim +# +# 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 +import gobject +import logging +import hippo + +from jarabe.journal.browse.lazymodel import Source +from jarabe.journal.browse.tableview import TableView, TableCell + +class ThumbsCell(TableCell, hippo.CanvasBox): + def __init__(self): + TableCell.__init__(self) + hippo.CanvasBox.__init__(self, orientation=hippo.ORIENTATION_VERTICAL) + + label = hippo.CanvasWidget(widget=gtk.Button('!!!')) + self.append(label) + +class ThumbsView(TableView): + __gsignals__ = { + 'detail-clicked': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([object])), + } + + def __init__(self): + TableView.__init__(self, ThumbsCell, 3, 3) -- cgit v0.9.1