Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/src/jarabe/journal/homogenetable.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/jarabe/journal/homogenetable.py')
-rw-r--r--src/jarabe/journal/homogenetable.py662
1 files changed, 662 insertions, 0 deletions
diff --git a/src/jarabe/journal/homogenetable.py b/src/jarabe/journal/homogenetable.py
new file mode 100644
index 0000000..64a22ec
--- /dev/null
+++ b/src/jarabe/journal/homogenetable.py
@@ -0,0 +1,662 @@
+# Copyright (C) 2010, 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 math
+import bisect
+import logging
+
+# Having spare rows let us making smooth scrolling w/o empty spaces
+_SPARE_ROWS_COUNT = 2
+
+
+class VHomogeneTable(gtk.Container):
+ """
+ Grid widget with homogeneously placed children of the same class.
+
+ Grid has fixed number of columns that are visible all time and unlimited
+ rows number. There are frame cells - visible at particular moment - frame
+ cells and virtual (widget is model less itself and only ask callback
+ object about right cell's value) ones - just cells. User can scroll up/down
+ grid to see all virtual cells and the same frame cell could represent
+ content of various virtual cells (widget will call cell_fill_in_cb callback
+ to refill frame cell content) in different time moments.
+
+ By default widget doesn't have any cells, to make it useful, assign proper
+ value to either frame_size or cell_size property. Also set cell_count to
+ set number of virual rows.
+
+ """
+ __gsignals__ = {
+ 'set-scroll-adjustments': (gobject.SIGNAL_RUN_FIRST, None,
+ [gtk.Adjustment, gtk.Adjustment]),
+ 'cursor-changed': (gobject.SIGNAL_RUN_FIRST, None, [object]),
+ }
+
+ def __init__(self, cell_class, **kwargs):
+ assert(hasattr(cell_class, 'do_fill_in'))
+
+ self._cell_class = cell_class
+ self._row_cache = []
+ self._cell_cache = []
+ self._cell_cache_pos = 0
+ self._adjustment = None
+ self._adjustment_value_changed_id = None
+ self._bin_window = None
+ self._cell_count = 0
+ self._cell_height = 0
+ self._frame_size = [None, None]
+ self._cell_size = [None, None]
+ self._selected_index = None
+ self._editable = True
+ self._pending_allocate = None
+
+ gtk.Container.__init__(self, **kwargs)
+
+ # when focused cell is out of visible frame,
+ # table itslef will be focused to follow gtk focusing scheme
+ self.props.can_focus = True
+
+ self.connect('key-press-event', self.__key_press_event_cb)
+
+ def set_frame_size(self, value):
+ value = list(value)
+ if self._frame_size == value:
+ return
+
+ if value[0] is not None:
+ self._cell_size[1] = None
+ if value[1] is not None:
+ self._cell_size[0] = None
+
+ self._frame_size = value
+ self._resize_table()
+
+ """Set persistent number of frame rows/columns, value is (rows, columns)
+ Cells will be resized while resizing widget.
+ Mutually exclusive to cell_size."""
+ frame_size = gobject.property(setter=set_frame_size)
+
+ def set_cell_size(self, value):
+ value = list(value)
+ if self._cell_size == value:
+ return
+
+ if value[0] is not None:
+ self._frame_size[1] = None
+ if value[1] is not None:
+ self._frame_size[0] = None
+
+ self._cell_size = value
+ self._resize_table()
+
+ """Set persistent cell sizes, value is (width, height)
+ Number of cells will be changed while resizing widget.
+ Mutually exclusive to frame_size."""
+ cell_size = gobject.property(setter=set_cell_size)
+
+ def get_cell_count(self):
+ return self._cell_count
+
+ def set_cell_count(self, count):
+ if self._cell_count == count:
+ return
+ self._cell_count = count
+ self.refill()
+ self._setup_adjustment(dry_run=False)
+
+ """Number of virtual cells
+ Defines maximal number of virtual rows, the minimal has being described
+ by frame_size/cell_size values."""
+ cell_count = gobject.property(getter=get_cell_count, setter=set_cell_count)
+
+ def get_cell(self, cell_index):
+ """Get cell widget by index
+ Method returns non-None values only for visible cells."""
+ cell = self._get_cell(cell_index)
+ if cell is None:
+ return None
+ else:
+ return cell.widget
+
+ def __getitem__(self, cell_index):
+ return self.get_cell(cell_index)
+
+ def get_cursor(self):
+ return self._selected_index
+
+ def set_cursor(self, cell_index):
+ cell_index = min(max(0, cell_index), self.cell_count - 1)
+ if cell_index == self.cursor:
+ return
+ self.scroll_to_cell(cell_index)
+ self._set_cursor(cell_index)
+
+ """Selected cell"""
+ cursor = gobject.property(getter=get_cursor, setter=set_cursor)
+
+ def get_editable(self):
+ return self._editable
+
+ def set_editable(self, value):
+ self._editable = value
+
+ """Can cells be focused"""
+ editable = gobject.property(getter=get_editable, setter=set_editable)
+
+ def get_editing(self):
+ if not self._editable or self._selected_index is None or \
+ self.props.has_focus:
+ return False
+ cell = self._get_cell(self._selected_index)
+ if cell is None:
+ return False
+ else:
+ return cell.widget.get_focus_child()
+
+ def set_editing(self, value):
+ if value == self.editing:
+ return
+ if value:
+ if not self.props.has_focus:
+ self.grab_focus()
+ cell = self._get_cell(self._selected_index)
+ if cell is not None:
+ cell.widget.child_focus(gtk.DIR_TAB_FORWARD)
+ else:
+ self.grab_focus()
+
+ """Selected cell got focused"""
+ editing = gobject.property(getter=get_editing, setter=set_editing)
+
+ def get_cell_at_pos(self, x, y):
+ """Get cell index at pos which is relative to VHomogeneTable widget"""
+ if self._empty:
+ return None
+
+ x, y = self.get_pointer()
+ x = min(max(0, x), self.allocation.width)
+ y = min(max(0, y), self.allocation.height) + self._pos_y
+
+ return self._get_cell_at_pos(x, y)
+
+ def scroll_to_cell(self, cell_index):
+ """Scroll VHomogeneTable to position where cell is viewable"""
+ if self._empty:
+ return
+
+ self.editing = False
+
+ row = cell_index / self._column_count
+ pos = row * self._cell_height
+
+ if pos < self._pos_y:
+ self._pos_y = pos
+ elif pos + self._cell_height >= self._pos_y + self._page:
+ self._pos_y = pos + self._cell_height - self._page
+ else:
+ return
+
+ self._pos_changed()
+
+ def refill(self):
+ """Force VHomogeneTable widget to run filling method for all cells"""
+ for cell in self._cell_cache:
+ cell.invalidate_pos()
+ cell.index = -1
+ self._allocate_rows(force=True)
+
+ # gtk.Widget overrides
+
+ def do_realize(self):
+ self.set_flags(gtk.REALIZED)
+
+ self.window = gtk.gdk.Window(
+ self.get_parent_window(),
+ window_type=gtk.gdk.WINDOW_CHILD,
+ x=self.allocation.x,
+ y=self.allocation.y,
+ width=self.allocation.width,
+ height=self.allocation.height,
+ wclass=gtk.gdk.INPUT_OUTPUT,
+ colormap=self.get_colormap(),
+ event_mask=gtk.gdk.VISIBILITY_NOTIFY_MASK)
+ self.window.set_user_data(self)
+
+ self._bin_window = gtk.gdk.Window(
+ self.window,
+ window_type=gtk.gdk.WINDOW_CHILD,
+ x=0,
+ y=-self._pos_y,
+ width=self.allocation.width,
+ height=self._max_y,
+ colormap=self.get_colormap(),
+ wclass=gtk.gdk.INPUT_OUTPUT,
+ event_mask=(self.get_events() | gtk.gdk.EXPOSURE_MASK |
+ gtk.gdk.SCROLL_MASK))
+ self._bin_window.set_user_data(self)
+
+ self.set_style(self.style.attach(self.window))
+ self.style.set_background(self.window, gtk.STATE_NORMAL)
+ self.style.set_background(self._bin_window, gtk.STATE_NORMAL)
+
+ for row in self._row_cache:
+ for cell in row:
+ cell.widget.set_parent_window(self._bin_window)
+
+ if self._pending_allocate is not None:
+ self._allocate_rows(force=self._pending_allocate)
+ self._pending_allocate = None
+ #self.queue_resize()
+
+ def do_size_allocate(self, allocation):
+ resize_tabel = self.allocation != allocation
+ self.allocation = allocation
+
+ if resize_tabel:
+ self._resize_table()
+
+ if self.flags() & gtk.REALIZED:
+ self.window.move_resize(*allocation)
+
+ def do_unrealize(self):
+ self._bin_window.set_user_data(None)
+ self._bin_window.destroy()
+ self._bin_window = None
+ gtk.Container.do_unrealize(self)
+
+ def do_style_set(self, style):
+ gtk.Widget.do_style_set(self, style)
+ if self.flags() & gtk.REALIZED:
+ self.style.set_background(self._bin_window, gtk.STATE_NORMAL)
+
+ def do_expose_event(self, event):
+ if event.window != self._bin_window:
+ return False
+ gtk.Container.do_expose_event(self, event)
+ return False
+
+ def do_map(self):
+ self.set_flags(gtk.MAPPED)
+
+ for row in self._row_cache:
+ for cell in row:
+ cell.widget.map()
+
+ self._bin_window.show()
+ self.window.show()
+
+ def do_size_request(self, req):
+ req.width = 0
+ req.height = 0
+
+ for row in self._row_cache:
+ for cell in row:
+ cell.widget.size_request()
+
+ def do_set_scroll_adjustments(self, hadjustment, vadjustment):
+ if vadjustment is None or vadjustment == self._adjustment:
+ return
+
+ if self._adjustment is not None:
+ self._adjustment.disconnect(self._adjustment_value_changed_id)
+
+ self._adjustment = vadjustment
+ self._setup_adjustment(dry_run=True)
+
+ self._adjustment_value_changed_id = vadjustment.connect(
+ 'value-changed', self.__adjustment_value_changed_cb)
+
+ # gtk.Container overrides
+
+ def do_forall(self, include_internals, callback, data):
+ for row in self._row_cache:
+ for cell in row:
+ callback(cell.widget, data)
+
+ def do_add(self, widget):
+ # container is not intended to add children manually
+ assert(False)
+
+ def do_remove(self, widget):
+ # container is not intended to remove children manually
+ pass
+
+ def do_set_focus_child(self, widget):
+ if widget is not None:
+ x, y, __, __ = widget.allocation
+ self.cursor = self._get_cell_at_pos(x, y)
+
+ def do_focus(self, type):
+ if self.editing:
+ cell = self._get_cell(self._selected_index)
+ if cell is None:
+ logging.error('cannot find _selected_index cell')
+ elif not cell.widget.child_focus(type):
+ self.grab_focus()
+ return True
+ else:
+ if self.props.has_focus:
+ return False
+ else:
+ if self._selected_index is None:
+ x, y = self.get_pointer()
+ self._set_cursor(self.get_cell_at_pos(x, y))
+ self.grab_focus()
+ return True
+
+ @property
+ def _frame_range(self):
+ if self._empty:
+ return xrange(0)
+ else:
+ first = self._pos_y / self._cell_height * self._column_count
+ last = int(math.ceil(float(self._pos_y + self._page) / \
+ self._cell_height) * self._column_count)
+ return xrange(first, min(last, self.cell_count))
+
+ @property
+ def _empty(self):
+ return not self._row_cache
+
+ @property
+ def _column_count(self):
+ if self._row_cache:
+ return len(self._row_cache[0])
+ else:
+ return 0
+
+ @property
+ def _row_count(self):
+ if self._column_count == 0:
+ return 0
+ else:
+ rows = math.ceil(float(self.cell_count) / self._column_count)
+ return max(self._frame_row_count, rows)
+
+ @property
+ def _frame_row_count(self):
+ return len(self._row_cache) - _SPARE_ROWS_COUNT
+
+ @property
+ def _page(self):
+ return self._frame_row_count * self._cell_height
+
+ @property
+ def _pos_y(self):
+ if self._adjustment is None or math.isnan(self._adjustment.value):
+ return 0
+ else:
+ return max(0, int(self._adjustment.value))
+
+ @_pos_y.setter
+ def _pos_y(self, value):
+ if self._adjustment is not None:
+ self._adjustment.value = value
+
+ @property
+ def _max_pos_y(self):
+ if self._adjustment is None:
+ return 0
+ else:
+ return max(0, self._max_y - self._page)
+
+ @property
+ def _max_y(self):
+ if self._adjustment is None:
+ return self.allocation.height
+ else:
+ return int(self._adjustment.upper)
+
+ def _get_cell(self, cell_index):
+ if cell_index is None:
+ return None
+ column = cell_index % self._column_count
+ base_index = cell_index - column
+ for row in self._row_cache:
+ if row[0].is_valid() and row[0].index == base_index:
+ return row[column]
+ return None
+
+ def _set_cursor(self, cell_index):
+ old_cursor = self._selected_index
+ self._selected_index = cell_index
+ if old_cursor != self._selected_index:
+ self.emit('cursor-changed', old_cursor)
+
+ def _get_cell_at_pos(self, x, y):
+ cell_row = y / self._cell_height
+ cell_column = x / (self.allocation.width / self._column_count)
+ cell_index = cell_row * self._column_count + cell_column
+ return min(cell_index, self.cell_count - 1)
+
+ def _pos_changed(self):
+ if self._adjustment is not None:
+ self._adjustment.value_changed()
+
+ def _abandon_cells(self):
+ for row in self._row_cache:
+ for cell in row:
+ cell.widget.unparent()
+ self._cell_cache_pos = 0
+ self._row_cache = []
+
+ def _pop_a_cell(self):
+ if self._cell_cache_pos < len(self._cell_cache):
+ cell = self._cell_cache[self._cell_cache_pos]
+ self._cell_cache_pos += 1
+ else:
+ cell = _Cell()
+ cell.widget = self._cell_class()
+ self._cell_cache.append(cell)
+ self._cell_cache_pos = len(self._cell_cache)
+
+ cell.invalidate_pos()
+ return cell
+
+ def _resize_table(self):
+ x, y, width, height = self.allocation
+ if x < 0 or y < 0:
+ return
+
+ frame_row_count, column_count = self._frame_size
+ cell_width, cell_height = self._cell_size
+
+ if frame_row_count is None:
+ if cell_height is None:
+ return
+ frame_row_count = max(1, height / cell_height)
+ if column_count is None:
+ if cell_width is None:
+ return
+ column_count = max(1, width / cell_width)
+
+ if (column_count != self._column_count or \
+ frame_row_count != self._frame_row_count):
+ self._abandon_cells()
+ for i_ in range(frame_row_count + _SPARE_ROWS_COUNT):
+ row = []
+ for j_ in range(column_count):
+ cell = self._pop_a_cell()
+ if self.flags() & gtk.REALIZED:
+ cell.widget.set_parent_window(self._bin_window)
+ cell.widget.set_parent(self)
+ row.append(cell)
+ self._row_cache.append(row)
+ else:
+ for row in self._row_cache:
+ for cell in row:
+ cell.invalidate_pos()
+
+ self._cell_height = height / self._frame_row_count
+ self._setup_adjustment(dry_run=True)
+
+ if self.flags() & gtk.REALIZED:
+ self._bin_window.resize(self.allocation.width, self._max_y)
+
+ self._allocate_rows(force=True)
+
+ def _setup_adjustment(self, dry_run):
+ if self._adjustment is None:
+ return
+
+ self._adjustment.lower = 0
+ self._adjustment.upper = self._row_count * self._cell_height
+ self._adjustment.page_size = self._page
+ self._adjustment.changed()
+
+ if self._pos_y > self._max_pos_y:
+ self._pos_y = self._max_pos_y
+ if not dry_run:
+ self._adjustment.value_changed()
+
+ def _allocate_cells(self, row, cell_y):
+ cell_x = 0
+ cell_row = cell_y / self._cell_height
+ cell_index = cell_row * self._column_count
+
+ for cell_column, cell in enumerate(row):
+ if cell.index != cell_index:
+ if cell_index < self.cell_count:
+ cell.widget.do_fill_in(self, cell_index)
+ cell.widget.show()
+ else:
+ cell.widget.hide()
+ cell.index = cell_index
+
+ cell_alloc = gtk.gdk.Rectangle(cell_x, cell_y)
+ cell_alloc.width = self.allocation.width / self._column_count
+ cell_alloc.height = self._cell_height
+ cell.widget.size_request()
+ cell.widget.size_allocate(cell_alloc)
+
+ cell_x += cell_alloc.width
+ cell_index += 1
+
+ def _allocate_rows(self, force):
+ if self._empty:
+ return
+
+ if not self.flags() & gtk.REALIZED:
+ self._pending_allocate = self._pending_allocate or force
+ return
+
+ pos = self._pos_y
+ if pos < 0 or pos > self._max_pos_y:
+ return
+
+ spare_rows = []
+ visible_rows = []
+ page_end = pos + self._page
+
+ if force:
+ spare_rows = [] + self._row_cache
+ else:
+ for row in self._row_cache:
+ row_y = row[0].widget.allocation.y
+ if row_y < 0 or row_y > page_end or \
+ (row_y + self._cell_height) < pos:
+ spare_rows.append(row)
+ else:
+ bisect.insort_right(visible_rows, _IndexedRow(row))
+
+ if visible_rows or spare_rows:
+
+ def try_insert_spare_row(cell_y, end_y):
+ while cell_y < end_y:
+ if not spare_rows:
+ logging.error('spare_rows should not be empty')
+ return
+ row = spare_rows.pop()
+ self._allocate_cells(row, cell_y)
+ cell_y = cell_y + self._cell_height
+
+ # visible_rows could not be continuous
+ # lets try to add spare rows to missed points
+ cell_y = int(pos) - int(pos) % self._cell_height
+ for i in visible_rows:
+ cell = i.row[0].widget.allocation
+ try_insert_spare_row(cell_y, cell.y)
+ cell_y = cell.y + cell.height
+
+ try_insert_spare_row(cell_y, page_end)
+
+ if self.editing and self._selected_index not in self._frame_range:
+ self.editing = False
+
+ self._bin_window.move(0, int(-pos))
+ self._bin_window.process_updates(True)
+
+ def __adjustment_value_changed_cb(self, adjustment):
+ self._allocate_rows(force=False)
+
+ def __key_press_event_cb(self, widget, event):
+ if self._empty or self.cursor is None:
+ return
+
+ page = self._column_count * self._frame_row_count
+
+ if event.keyval == gtk.keysyms.Return and self.editable:
+ self.editing = not self.editing
+ elif event.keyval == gtk.keysyms.Left:
+ self.cursor -= 1
+ elif event.keyval == gtk.keysyms.Right:
+ self.cursor += 1
+ elif event.keyval == gtk.keysyms.Up:
+ if self.cursor >= self._column_count:
+ self.cursor -= self._column_count
+ elif event.keyval == gtk.keysyms.Down:
+ if self.cursor / self._column_count < \
+ (self.cell_count - 1) / self._column_count:
+ self.cursor += self._column_count
+ elif event.keyval in (gtk.keysyms.Page_Up, gtk.keysyms.KP_Page_Up):
+ self.cursor -= page
+ elif event.keyval in (gtk.keysyms.Page_Down, gtk.keysyms.KP_Page_Down):
+ self.cursor += page
+ elif event.keyval in (gtk.keysyms.Home, gtk.keysyms.KP_Home):
+ self.cursor = 0
+ elif event.keyval in (gtk.keysyms.End, gtk.keysyms.KP_End):
+ self.cursor = self.cell_count - 1
+ else:
+ return False
+
+ return True
+
+
+class _IndexedRow:
+
+ def __init__(self, row):
+ self.row = row
+
+ def __lt__(self, other):
+ return self.row[0].widget.allocation.y < \
+ other.row[0].widget.allocation.y
+
+
+class _Cell:
+ widget = None
+ index = -1
+
+ def invalidate_pos(self):
+ self.widget.size_allocate(gtk.gdk.Rectangle(-1, -1, 0, 0))
+
+ def is_valid(self):
+ return self.index >= 0 and self.widget is not None and \
+ self.widget.allocation >= 0 and self.widget.allocation >= 0
+
+
+VHomogeneTable.set_set_scroll_adjustments_signal('set-scroll-adjustments')