diff options
Diffstat (limited to 'src/jarabe/journal/lazymodel.py')
-rw-r--r-- | src/jarabe/journal/lazymodel.py | 420 |
1 files changed, 420 insertions, 0 deletions
diff --git a/src/jarabe/journal/lazymodel.py b/src/jarabe/journal/lazymodel.py new file mode 100644 index 0000000..d590fbc --- /dev/null +++ b/src/jarabe/journal/lazymodel.py @@ -0,0 +1,420 @@ +# 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=None): + """ 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] + + if calc_columns is not None: + 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._last_count = 0 + self._cache = {} + self._frame = (0, -1) + self._in_process = {} + self._postponed = [] + + 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 self._source is None: + 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 self._source is None: + 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() + + # TODO how to handle large update, + # commented for now since in TableView + # it works fine w/o these updates + #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._last_count != count: + self.emit('rows-reordered', (0, ), self.get_iter((0, )), None) + + if self._frame[0] >= count: + self._frame = (0, -1) + elif self._frame[1] >= count: + for i in range(count, self._frame[1]): + if i in self._cache: + del self._cache[i] + self._frame = (self._frame[0], count-1) + + self._cache = {} + + if self._last_count == count: + 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 field in row: + del row[field] + self.emit('row-changed', (i, ), self.get_iter((i, ))) + + def get_row(self, pos, frame=None): + if self._source is None: + 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, metadata): + if not offset in self._in_process: + logging.debug('__delayed_fetch_cb: no offset=%s' % offset) + return + + logging.debug('__delayed_fetch_cb: get %s' % offset) + + path = (offset, ) + iterator = self.get_iter(path) + row = Row(self, path, iterator, metadata) + + if self.in_frame(offset): + self._cache[offset] = row + + del self._in_process[offset] + self.emit('row-changed', path, iterator) + 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 is not None and row != False: + self.emit('row-changed', row.path, row.iterator) + else: + break + + def _get_row(self, offset, frame): + + def fetch(): + row = self._source.get_row(offset) + + if row is None or row == False: + 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 is not None: + 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 i in self._cache: + del self._cache[i] + for i in range(intersect_max+1, self._frame[1]+1): + if i in self._cache: + 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 order is None 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] + + 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, iterator): + if iterator is None and not self._closing: + return self._source.get_count() + else: + return 0 + + def on_get_value(self, offset, column): + if self._view is None or offset >= self._source.get_count(): + return None + + # return value only if iterator came from visible range + # (on setting model, gtk.TreeView scans all items) + vrange = self._view.get_visible_range() + if vrange and offset >= vrange[0][0] and offset <= vrange[1][0]: + row = self._get_row(offset, (vrange[0][0], vrange[1][0])) + return row is not None and row != False and row[column] + + return None + + def on_iter_nth_child(self, iterator, n): + return n + + def on_get_path(self, iterator): + return iterator + + 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, iterator): + if iterator is not None: + if iterator >= self._source.get_count() - 1 or self._closing: + return None + return iterator + 1 + return None + + def on_get_flags(self): + return gtk.TREE_MODEL_ITERS_PERSIST | gtk.TREE_MODEL_LIST_ONLY + + def on_iter_children(self, iterator): + return None + + def on_iter_has_child(self, iterator): + return False + + def on_iter_parent(self, iterator): + return None + + +class Row: + + def __init__(self, model, path, iterator, metadata): + self.model = model + self.iterator = iterator + self.path = path + self.metadata = metadata + self.row = [None] * len(model.columns_by_name) + self._calced_row = {} + + for name, value in metadata.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 key in self._calced_row: + 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.metadata[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.metadata[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.metadata[key] + + def __contains__(self, key): + if isinstance(key, int): + return key < len(self.row) or key in self._calced_row + else: + return self.metadata.__contains__(key) + + def has_key(self, key): + return self.__contains__(key) + + def get(self, key, default=None): + if key in self: + return self.__getitem__(key) + else: + return default |