From 2a0bbabf6acfb2bdbd29e3667f9461a1c559b043 Mon Sep 17 00:00:00 2001 From: Aleksey Lim Date: Wed, 20 Jan 2010 02:27:34 +0000 Subject: Initial listview refactoring --- diff --git a/src/jarabe/journal/controler.py b/src/jarabe/journal/controler.py new file mode 100644 index 0000000..76a3737 --- /dev/null +++ b/src/jarabe/journal/controler.py @@ -0,0 +1,31 @@ +# 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 gobject + + +class _Objects(gobject.GObject): + + __gsignals__ = { + 'detail-clicked': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([str])), + } + + def __init__(self): + gobject.GObject.__init__(self) + + +objects = _Objects() diff --git a/src/jarabe/journal/entry.py b/src/jarabe/journal/entry.py new file mode 100644 index 0000000..7347fe1 --- /dev/null +++ b/src/jarabe/journal/entry.py @@ -0,0 +1,184 @@ +# 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 logging + +import gtk +import gobject +import pango + + +class Entry(gtk.TextView): + """One paragraph string entry with additional features + + * multi line mode for wrapping long lines + * having wrapping and ellipses simultaneously in inactive mode + * accent in inactive mode + + NOTE: Use text property instead of buffer, buffer's value will be + changed in inactive mode + + """ + + def __init__(self, **kwargs): + self._max_line_count = 1 + self._text = None + + gobject.GObject.__init__(self, **kwargs) + + self._tag = self.props.buffer.create_tag() + self._tag.props.weight = pango.WEIGHT_BOLD + + gtk.TextView.set_accepts_tab(self, False) + self.set_max_line_count(self._max_line_count) + + self.connect('key-press-event', self.__key_press_event_cb) + self.connect('focus-in-event', self.__focus_in_event_cb) + self.connect('focus-out-event', self.__focus_out_event_cb) + self.connect('button-release-event', self.__button_release_event_cb) + + def set_accepts_tab(self, value): + # accepts_tab cannot be set by users + assert(False) + + def get_accepts_tab(self): + return gtk.TextView.get_accepts_tab() + + accepts_tab = gobject.property( + getter=get_accepts_tab, setter=set_accepts_tab) + + def set_wrap_mode(self, value): + # accepts_tab cannot be set by users + assert(False) + + def get_wrap_mode(self): + return gtk.TextView.get_wrap_mode() + + wrap_mode = gobject.property( + getter=get_wrap_mode, setter=set_wrap_mode) + + def get_max_line_count(self): + return self._max_line_count + + def set_max_line_count(self, max_line_count): + max_line_count = max(1, max_line_count) + self._max_line_count = max_line_count + + if max_line_count == 1: + gtk.TextView.set_wrap_mode(self, gtk.WRAP_NONE) + else: + gtk.TextView.set_wrap_mode(self, gtk.WRAP_WORD) + + context = self.get_pango_context() + metrics = context.get_metrics(self.style.font_desc) + line_height = pango.PIXELS(metrics.get_ascent() + \ + metrics.get_descent()) + self.set_size_request(-1, line_height * max_line_count) + + max_line_count = gobject.property( + getter=get_max_line_count, setter=set_max_line_count) + + def get_text(self): + return self._text + + def set_text(self, value): + self._text = value + self.props.buffer.props.text = value + if not self.props.has_focus: + self._accept() + + text = gobject.property(getter=get_text, setter=set_text) + + def do_size_allocate(self, allocation): + gtk.TextView.do_size_allocate(self, allocation) + if not self.props.has_focus: + self._accept() + + def _accept(self): + if self._text is None: + return + + gtk.TextView.set_wrap_mode(self, gtk.WRAP_WORD) + + buf = self.props.buffer + buf.props.text = self._text + + def accent(): + start = buf.get_start_iter() + end = buf.get_end_iter() + buf.apply_tag(self._tag, start, end) + + def last_offset(): + iter = buf.get_start_iter() + for __ in xrange(self._max_line_count): + if not self.forward_display_line(iter): + return None + return iter.get_offset() + + accent() + offset = last_offset() + + if offset is not None: + offset = len(buf.props.text[:offset].rstrip()) - 1 + buf.props.text = buf.props.text[:offset] + '...' + + final_offset = last_offset() + if final_offset is not None and final_offset < offset + 3: + # ellipses added new line + buf.props.text = buf.props.text[:offset - 3] + '...' + + accent() + + def __button_release_event_cb(self, widget, event): + buf = self.props.buffer + if not buf.get_has_selection(): + buf.select_range(buf.get_end_iter(), buf.get_start_iter()) + return False + + def __focus_in_event_cb(self, widget, event): + self.props.buffer.props.text = self._text + + if self._max_line_count == 1: + gtk.TextView.set_wrap_mode(self, gtk.WRAP_NONE) + else: + gtk.TextView.set_wrap_mode(self, gtk.WRAP_WORD) + + return False + + def __focus_out_event_cb(self, widget, event): + self._text = self.props.buffer.props.text + self._accept() + return False + + def __key_press_event_cb(self, widget, event): + ignore_mask = [gtk.keysyms.Return] + if self._max_line_count == 1: + ignore_mask.extend([gtk.keysyms.Up, gtk.keysyms.Down]) + + if event.keyval in ignore_mask: + key_event = event + if event.keyval in [gtk.keysyms.Up]: + # change Shift mask for backwards keys + if key_event.state & gtk.gdk.SHIFT_MASK: + key_event.state &= ~gtk.gdk.SHIFT_MASK + else: + key_event.state |= gtk.gdk.SHIFT_MASK + key_event.keyval = gtk.keysyms.Tab + key_event.hardware_keycode = 0 + gtk.main_do_event(key_event) + return True + + return False diff --git a/src/jarabe/journal/expandedentry.py b/src/jarabe/journal/expandedentry.py index e73b717..2a3ead0 100644 --- a/src/jarabe/journal/expandedentry.py +++ b/src/jarabe/journal/expandedentry.py @@ -32,7 +32,7 @@ from sugar.graphics.entry import CanvasEntry from sugar.graphics.canvastextview import CanvasTextView from sugar.util import format_size -from jarabe.journal.keepicon import KeepIcon +from jarabe.journal.widgets import KeepIconCanvas from jarabe.journal.palettes import ObjectPalette, BuddyPalette from jarabe.journal import misc from jarabe.journal import model @@ -99,7 +99,7 @@ class ExpandedEntry(hippo.CanvasBox): # Header - self._keep_icon = self._create_keep_icon() + self._keep_icon = KeepIconCanvas(box_width=style.GRID_CELL_SIZE * 3 / 5) header.append(self._keep_icon) self._icon = None @@ -140,7 +140,7 @@ class ExpandedEntry(hippo.CanvasBox): return self._metadata = metadata - self._keep_icon.keep = (int(metadata.get('keep', 0)) == 1) + self._keep_icon.check_out(metadata) self._icon = self._create_icon() self._icon_box.clear() @@ -169,11 +169,6 @@ class ExpandedEntry(hippo.CanvasBox): tags.props.buffer.props.text = metadata.get('tags', '') tags.props.editable = model.is_editable(metadata) - def _create_keep_icon(self): - keep_icon = KeepIcon(False) - keep_icon.connect('activated', self._keep_icon_activated_cb) - return keep_icon - def _create_icon(self): icon = CanvasIcon(file_name=misc.get_icon_name(self._metadata)) icon.connect_after('button-release-event', @@ -405,20 +400,6 @@ class ExpandedEntry(hippo.CanvasBox): self._update_title_sid = None - def get_keep(self): - return int(self._metadata.get('keep', 0)) == 1 - - def _keep_icon_activated_cb(self, keep_icon): - if not model.is_editable(self._metadata): - return - if self.get_keep(): - self._metadata['keep'] = 0 - else: - self._metadata['keep'] = 1 - model.write(self._metadata, update_mtime=False) - - keep_icon.props.keep = self.get_keep() - def _icon_button_release_event_cb(self, button, event): logging.debug('_icon_button_release_event_cb') misc.resume(self._metadata) 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') diff --git a/src/jarabe/journal/homogeneview.py b/src/jarabe/journal/homogeneview.py new file mode 100644 index 0000000..764ecfe --- /dev/null +++ b/src/jarabe/journal/homogeneview.py @@ -0,0 +1,95 @@ +# 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 logging + +from sugar.graphics import style +from sugar.graphics.roundbox import CanvasRoundBox + +from jarabe.journal.homogenetable import VHomogeneTable + + +class Cell(gtk.EventBox): + + def __init__(self): + gtk.EventBox.__init__(self) + self.select(False) + + def do_fill_in_cell_content(self, table, metadata): + # needs to be overriden + pass + + def do_fill_in(self, table, cell_index): + table.result_set.seek(cell_index) + self.do_fill_in_cell_content(table, table.result_set.read()) + if table.hover_selection: + self.select(table.cursor == cell_index) + + def select(self, selected): + if selected: + self.modify_bg(gtk.STATE_NORMAL, + style.COLOR_SELECTION_GREY.get_gdk_color()) + else: + self.modify_bg(gtk.STATE_NORMAL, + style.COLOR_WHITE.get_gdk_color()) + + +class HomogeneView(VHomogeneTable): + + __gsignals__ = { + 'entry-activated': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([str])), + } + + def __init__(self, cell_class, **kwargs): + assert(issubclass(cell_class, Cell)) + + VHomogeneTable.__init__(self, cell_class, **kwargs) + + self._result_set = None + self.hover_selection = False + + self.connect('cursor-changed', self.__cursor_changed_cb) + + def get_result_set(self): + return self._result_set + + def set_result_set(self, result_set): + if self._result_set is result_set: + return + + self._result_set = result_set + + result_set_length = result_set.get_length() + if self.cell_count == result_set_length: + self.refill() + else: + self.cell_count = result_set_length + + result_set = property(get_result_set, set_result_set) + + def __cursor_changed_cb(self, table, old_cursor): + if not self.hover_selection: + return + old_cell = table[old_cursor] + if old_cell is not None: + old_cell.select(False) + new_cell = table[table.cursor] + if new_cell is not None: + new_cell.select(True) diff --git a/src/jarabe/journal/journalactivity.py b/src/jarabe/journal/journalactivity.py index 0559560..587b57b 100644 --- a/src/jarabe/journal/journalactivity.py +++ b/src/jarabe/journal/journalactivity.py @@ -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.view import View from jarabe.journal.detailview import DetailView from jarabe.journal.volumestoolbar import VolumesToolbar from jarabe.journal import misc @@ -42,6 +42,7 @@ from jarabe.journal.journalentrybundle import JournalEntryBundle from jarabe.journal.objectchooser import ObjectChooser from jarabe.journal.modalalert import ModalAlert from jarabe.journal import model +from jarabe.journal import controler J_DBUS_SERVICE = 'org.laptop.Journal' J_DBUS_INTERFACE = 'org.laptop.Journal' @@ -105,11 +106,15 @@ 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._view = None self._detail_view = None self._main_toolbox = None self._detail_toolbox = None @@ -152,11 +157,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._view = View() + controler.objects.connect('detail-clicked', self.__detail_clicked_cb) + self._view.connect('clear-clicked', self.__clear_clicked_cb) + self._main_view.pack_start(self._view) + self._view.show() self._volumes_toolbar = VolumesToolbar() self._volumes_toolbar.connect('volume-changed', @@ -165,6 +170,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): @@ -183,7 +189,7 @@ class JournalActivity(Window): if keyname == 'Escape': self.show_main_view() - def __detail_clicked_cb(self, list_view, object_id): + def __detail_clicked_cb(self, controler, object_id): self._show_secondary_view(object_id) def __clear_clicked_cb(self, list_view): @@ -193,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._view.update_with_query(query) self.show_main_view() + def __view_changed_cb(self, sender, view): + self._view.view = view + def show_main_view(self): if self.toolbar_box != self._main_toolbox: self.set_toolbar_box(self._main_toolbox) @@ -259,7 +268,7 @@ class JournalActivity(Window): def _focus_in_event_cb(self, window, event): self.search_grab_focus() - self._list_view.update_dates() + self._view.update_dates() def _check_for_bundle(self, object_id): registry = bundleregistry.get_registry() @@ -306,12 +315,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._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._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 fe05657..7da9471 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 @@ -41,6 +42,7 @@ from sugar import mime from jarabe.model import bundleregistry from jarabe.journal import misc from jarabe.journal import model +from jarabe.journal import view _AUTOSEARCH_TIMEOUT = 1000 @@ -73,7 +75,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 +119,36 @@ class SearchToolbar(gtk.Toolbar): #self.insert(tool_item, -1) #tool_item.show() + separator = gtk.SeparatorToolItem() + separator.props.draw = False + 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, + view.VIEW_LIST) + 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, + view.VIEW_THUMBS) + self.insert(thumb_button, -1) + thumb_button.show() + self._query = self._build_query() self.refresh_filters() + def __view_button_toggled_cb(self, button, view): + if button.props.active: + self.emit('view-changed', view) + def give_entry_focus(self): self._search_entry.grab_focus() diff --git a/src/jarabe/journal/keepicon.py b/src/jarabe/journal/keepicon.py deleted file mode 100644 index 2c692c6..0000000 --- a/src/jarabe/journal/keepicon.py +++ /dev/null @@ -1,59 +0,0 @@ -# Copyright (C) 2006, Red Hat, Inc. -# -# 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 gobject -import hippo -import gconf - -from sugar.graphics.icon import CanvasIcon -from sugar.graphics import style -from sugar.graphics.xocolor import XoColor - -class KeepIcon(CanvasIcon): - def __init__(self, keep): - CanvasIcon.__init__(self, icon_name='emblem-favorite', - box_width=style.GRID_CELL_SIZE * 3 / 5, - size=style.SMALL_ICON_SIZE) - self.connect('motion-notify-event', self.__motion_notify_event_cb) - - self._keep = None - self.set_keep(keep) - - def set_keep(self, keep): - if keep == self._keep: - return - - self._keep = keep - if keep: - client = gconf.client_get_default() - color = XoColor(client.get_string('/desktop/sugar/user/color')) - self.props.xo_color = color - else: - self.props.stroke_color = style.COLOR_BUTTON_GREY.get_svg() - self.props.fill_color = style.COLOR_TRANSPARENT.get_svg() - - def get_keep(self): - return self._keep - - keep = gobject.property(type=int, default=0, getter=get_keep, - setter=set_keep) - - def __motion_notify_event_cb(self, icon, event): - if not self._keep: - if event.detail == hippo.MOTION_DETAIL_ENTER: - icon.props.fill_color = style.COLOR_BUTTON_GREY.get_svg() - elif event.detail == hippo.MOTION_DETAIL_LEAVE: - icon.props.fill_color = style.COLOR_TRANSPARENT.get_svg() diff --git a/src/jarabe/journal/listmodel.py b/src/jarabe/journal/listmodel.py deleted file mode 100644 index d3b7e24..0000000 --- a/src/jarabe/journal/listmodel.py +++ /dev/null @@ -1,201 +0,0 @@ -# Copyright (C) 2009, Tomeu Vizoso -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA - -import logging - -import simplejson -import gobject -import gtk - -from sugar.graphics.xocolor import XoColor -from sugar.graphics import style -from sugar import util - -from jarabe.journal import model -from jarabe.journal import misc - -DS_DBUS_SERVICE = 'org.laptop.sugar.DataStore' -DS_DBUS_INTERFACE = 'org.laptop.sugar.DataStore' -DS_DBUS_PATH = '/org/laptop/sugar/DataStore' - -class ListModel(gtk.GenericTreeModel, gtk.TreeDragSource): - __gtype_name__ = 'JournalListModel' - - __gsignals__ = { - 'ready': (gobject.SIGNAL_RUN_FIRST, - gobject.TYPE_NONE, - ([])), - 'progress': (gobject.SIGNAL_RUN_FIRST, - gobject.TYPE_NONE, - ([])), - } - - COLUMN_UID = 0 - COLUMN_FAVORITE = 1 - COLUMN_ICON = 2 - COLUMN_ICON_COLOR = 3 - COLUMN_TITLE = 4 - COLUMN_DATE = 5 - COLUMN_PROGRESS = 6 - COLUMN_BUDDY_1 = 7 - COLUMN_BUDDY_2 = 8 - COLUMN_BUDDY_3 = 9 - - _COLUMN_TYPES = {COLUMN_UID: str, - COLUMN_FAVORITE: bool, - COLUMN_ICON: str, - COLUMN_ICON_COLOR: object, - COLUMN_TITLE: str, - COLUMN_DATE: str, - COLUMN_PROGRESS: int, - COLUMN_BUDDY_1: object, - COLUMN_BUDDY_3: object, - COLUMN_BUDDY_2: object} - - _PAGE_SIZE = 10 - - def __init__(self, query): - gobject.GObject.__init__(self) - - self._last_requested_index = None - self._cached_row = None - self._result_set = model.find(query, ListModel._PAGE_SIZE) - self._temp_drag_file_path = None - - # HACK: The view will tell us that it is resizing so the model can - # avoid hitting D-Bus and disk. - self.view_is_resizing = False - - self._result_set.ready.connect(self.__result_set_ready_cb) - self._result_set.progress.connect(self.__result_set_progress_cb) - - def __result_set_ready_cb(self, **kwargs): - self.emit('ready') - - def __result_set_progress_cb(self, **kwargs): - self.emit('progress') - - def setup(self): - self._result_set.setup() - - def stop(self): - self._result_set.stop() - - def get_metadata(self, path): - return model.get(self[path][ListModel.COLUMN_UID]) - - def on_get_n_columns(self): - return len(ListModel._COLUMN_TYPES) - - def on_get_column_type(self, index): - return ListModel._COLUMN_TYPES[index] - - def on_iter_n_children(self, iter): - if iter == None: - return self._result_set.length - else: - return 0 - - def on_get_value(self, index, column): - if self.view_is_resizing: - return None - - if index == self._last_requested_index: - return self._cached_row[column] - - if index >= self._result_set.length: - return None - - self._result_set.seek(index) - metadata = self._result_set.read() - - self._last_requested_index = index - self._cached_row = [] - self._cached_row.append(metadata['uid']) - self._cached_row.append(metadata.get('keep', '0') == '1') - self._cached_row.append(misc.get_icon_name(metadata)) - - if misc.is_activity_bundle(metadata): - xo_color = XoColor('%s,%s' % (style.COLOR_BUTTON_GREY.get_svg(), - style.COLOR_TRANSPARENT.get_svg())) - else: - xo_color = misc.get_icon_color(metadata) - self._cached_row.append(xo_color) - - title = gobject.markup_escape_text(metadata.get('title', None)) - self._cached_row.append('%s' % title) - - timestamp = int(metadata.get('timestamp', 0)) - self._cached_row.append(util.timestamp_to_elapsed_string(timestamp)) - - self._cached_row.append(int(metadata.get('progress', 100))) - - if metadata.get('buddies', ''): - buddies = simplejson.loads(metadata['buddies']).values() - else: - buddies = [] - - for n in xrange(0, 3): - if buddies: - nick, color = buddies.pop(0) - self._cached_row.append((nick, XoColor(color))) - else: - self._cached_row.append(None) - - return self._cached_row[column] - - def on_iter_nth_child(self, iter, n): - return n - - def on_get_path(self, iter): - return (iter) - - def on_get_iter(self, path): - return path[0] - - def on_iter_next(self, iter): - if iter != None: - if iter >= self._result_set.length - 1: - return None - return iter + 1 - return None - - def on_get_flags(self): - return gtk.TREE_MODEL_ITERS_PERSIST | gtk.TREE_MODEL_LIST_ONLY - - def on_iter_children(self, iter): - return None - - def on_iter_has_child(self, iter): - return False - - def on_iter_parent(self, iter): - return None - - def do_drag_data_get(self, path, selection): - uid = self[path][ListModel.COLUMN_UID] - if selection.target == 'text/uri-list': - # Get hold of a reference so the temp file doesn't get deleted - self._temp_drag_file_path = model.get_file(uid) - logging.debug('putting %r in selection', self._temp_drag_file_path) - selection.set(selection.target, 8, self._temp_drag_file_path) - return True - elif selection.target == 'journal-object-id': - selection.set(selection.target, 8, uid) - return True - - return False - diff --git a/src/jarabe/journal/listview.py b/src/jarabe/journal/listview.py index 9e19f70..f4406b2 100644 --- a/src/jarabe/journal/listview.py +++ b/src/jarabe/journal/listview.py @@ -1,4 +1,4 @@ -# Copyright (C) 2009, Tomeu Vizoso +# 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 @@ -14,628 +14,56 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +import gobject import logging -from gettext import gettext as _ -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.xocolor import XoColor -from sugar import util - -from jarabe.journal.listmodel import ListModel -from jarabe.journal.palettes import ObjectPalette, BuddyPalette -from jarabe.journal import model -from jarabe.journal import misc - -UPDATE_INTERVAL = 300 - -MESSAGE_EMPTY_JOURNAL = 0 -MESSAGE_NO_MATCH = 1 - -class TreeView(gtk.TreeView): - __gtype_name__ = 'JournalTreeView' - - def __init__(self): - gtk.TreeView.__init__(self) - self.set_headers_visible(False) - 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. - tree_model = self.get_model() - if tree_model is not None: - tree_model.view_is_resizing = True - try: - gtk.TreeView.do_size_request(self, requisition) - finally: - if tree_model is not None: - tree_model.view_is_resizing = False +from jarabe.journal.homogeneview import HomogeneView +from jarabe.journal.homogeneview import Cell +from jarabe.journal.widgets import * -class BaseListView(gtk.Bin): - __gtype_name__ = 'JournalBaseListView' - __gsignals__ = { - 'clear-clicked': (gobject.SIGNAL_RUN_FIRST, - gobject.TYPE_NONE, - ([])) - } +class _Cell(Cell): def __init__(self): - self._query = {} - self._model = None - self._progress_bar = None - self._last_progress_bar_pulse = None - self._scroll_position = 0. - - gobject.GObject.__init__(self) - - self.connect('map', self.__map_cb) - self.connect('unrealize', self.__unrealize_cb) - 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() - selection = self.tree_view.get_selection() - selection.set_mode(gtk.SELECTION_NONE) - self.tree_view.props.fixed_height_mode = True - self.tree_view.modify_base(gtk.STATE_NORMAL, - style.COLOR_WHITE.get_gdk_color()) - self._scrolled_window.add(self.tree_view) - self.tree_view.show() - - self.cell_title = None - self.cell_icon = None - self._title_column = None - 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) - - # 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 __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 _add_columns(self): - cell_favorite = CellRendererFavorite(self.tree_view) - cell_favorite.connect('clicked', self.__favorite_clicked_cb) - - column = gtk.TreeViewColumn() - column.props.sizing = gtk.TREE_VIEW_COLUMN_FIXED - column.props.fixed_width = cell_favorite.props.width - column.pack_start(cell_favorite) - column.set_cell_data_func(cell_favorite, self.__favorite_set_data_cb) - self.tree_view.append_column(column) - - self.cell_icon = CellRendererActivityIcon(self.tree_view) - - column = gtk.TreeViewColumn() - column.props.sizing = gtk.TREE_VIEW_COLUMN_FIXED - column.props.fixed_width = self.cell_icon.props.width - column.pack_start(self.cell_icon) - column.add_attribute(self.cell_icon, 'file-name', ListModel.COLUMN_ICON) - column.add_attribute(self.cell_icon, 'xo-color', - ListModel.COLUMN_ICON_COLOR) - self.tree_view.append_column(column) - - self.cell_title = gtk.CellRendererText() - self.cell_title.props.ellipsize = pango.ELLIPSIZE_MIDDLE - self.cell_title.props.ellipsize_set = True - - self._title_column = gtk.TreeViewColumn() - self._title_column.props.sizing = gtk.TREE_VIEW_COLUMN_FIXED - self._title_column.props.expand = True - self._title_column.props.clickable = True - self._title_column.pack_start(self.cell_title) - self._title_column.add_attribute(self.cell_title, 'markup', - ListModel.COLUMN_TITLE) - self.tree_view.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) - - for column_index in [ListModel.COLUMN_BUDDY_1, ListModel.COLUMN_BUDDY_2, - ListModel.COLUMN_BUDDY_3]: - cell_icon = CellRendererBuddy(self.tree_view, - column_index=column_index) - buddies_column.pack_start(cell_icon) - buddies_column.props.fixed_width += cell_icon.props.width - buddies_column.add_attribute(cell_icon, 'buddy', column_index) - buddies_column.set_cell_data_func(cell_icon, - self.__buddies_set_data_cb) - - cell_progress = gtk.CellRendererProgress() - cell_progress.props.ypad = style.GRID_CELL_SIZE / 4 - buddies_column.pack_start(cell_progress) - buddies_column.add_attribute(cell_progress, 'value', - ListModel.COLUMN_PROGRESS) - buddies_column.set_cell_data_func(cell_progress, - self.__progress_data_cb) - - cell_text = gtk.CellRendererText() - cell_text.props.xalign = 1 - - # Measure the required width for a date in the form of "10 hours, 10 - # minutes ago" - timestamp = time.time() - 10 * 60 - 10 * 60 * 60 - date = util.timestamp_to_elapsed_string(timestamp) - date_width = self._get_width_for_string(date) - - self.date_column = gtk.TreeViewColumn() - self.date_column.props.sizing = gtk.TREE_VIEW_COLUMN_FIXED - self.date_column.props.fixed_width = date_width - self.date_column.set_alignment(1) - self.date_column.props.resizable = True - self.date_column.props.clickable = True - self.date_column.pack_start(cell_text) - self.date_column.add_attribute(cell_text, 'text', ListModel.COLUMN_DATE) - self.tree_view.append_column(self.date_column) - - def _get_width_for_string(self, text): - # Add some extra margin - text = text + 'aaaaa' - - widget = gtk.Label('') - context = widget.get_pango_context() - layout = pango.Layout(context) - layout.set_text(text) - width, height_ = layout.get_size() - return pango.PIXELS(width) - - 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 __buddies_set_data_cb(self, column, cell, tree_model, tree_iter): - progress = tree_model[tree_iter][ListModel.COLUMN_PROGRESS] - cell.props.visible = progress >= 100 - - def __progress_data_cb(self, column, cell, tree_model, tree_iter): - progress = tree_model[tree_iter][ListModel.COLUMN_PROGRESS] - cell.props.visible = progress < 100 - - def __favorite_set_data_cb(self, column, cell, tree_model, tree_iter): - favorite = tree_model[tree_iter][ListModel.COLUMN_FAVORITE] - if favorite: - client = gconf.client_get_default() - color = XoColor(client.get_string('/desktop/sugar/user/color')) - cell.props.xo_color = color - else: - cell.props.xo_color = None - - def __favorite_clicked_cb(self, cell, path): - row = self._model[path] - metadata = model.get(row[ListModel.COLUMN_UID]) - if not model.is_editable(metadata): - return - if metadata.get('keep', 0) == '1': - metadata['keep'] = '0' - else: - 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() - - if self._model is not None: - self._model.stop() - self._dirty = False - - 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() - - self._scroll_position = self.tree_view.props.vadjustment.props.value - logging.debug('ListView.__model_ready_cb %r', self._scroll_position) - - if self.tree_view.window is not None: - # prevent glitches while later vadjustment setting, see #1235 - self.tree_view.get_bin_window().hide() - - # Cannot set it up earlier because will try to access the model - # and it needs to be ready. - self.tree_view.set_model(self._model) - - self.tree_view.props.vadjustment.props.value = self._scroll_position - self.tree_view.props.vadjustment.value_changed() - - if self.tree_view.window is not None: - # prevent glitches while later vadjustment setting, see #1235 - self.tree_view.get_bin_window().show() - - 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 __map_cb(self, widget): - logging.debug('ListView.__map_cb %r', self._scroll_position) - self.tree_view.props.vadjustment.props.value = self._scroll_position - self.tree_view.props.vadjustment.value_changed() - - def __unrealize_cb(self, widget): - self._scroll_position = self.tree_view.props.vadjustment.props.value - logging.debug('ListView.__map_cb %r', self._scroll_position) - - is_editable = self._query.get('mountpoints', '') == '/' - self.cell_title.props.editable = is_editable + Cell.__init__(self) - 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 + self._row = gtk.HBox() + self._row.props.spacing = style.DEFAULT_SPACING + self.add(self._row) - def __model_progress_cb(self, tree_model): - if self._progress_bar is None: - self._start_progress_bar() + keep = KeepIcon(box_width=style.GRID_CELL_SIZE) + self._row.pack_start(keep, expand=False) - if time.time() - self._last_progress_bar_pulse > 0.05: - self._progress_bar.pulse() - self._last_progress_bar_pulse = time.time() + icon = ObjectIcon(size=style.STANDARD_ICON_SIZE) + self._row.pack_start(icon, expand=False) - 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() + title = Title(xalign=0, yalign=0.5, xscale=1, yscale=0) + self._row.pack_start(title) - 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() + details = DetailsIcon() + self._row.pack_end(details, expand=False) - def _stop_progress_bar(self): - if self._progress_bar is None: - return - self.remove(self.child) - self.add(self._scrolled_window) - self._progress_bar = None + date = Timestamp() + self._row.pack_end(date, expand=False) - def _show_message(self, message): - canvas = hippo.Canvas() - self.remove(self.child) - self.add(canvas) - canvas.show() + buddies = Buddies(buddies_max=3, + xalign=0, yalign=0.5, xscale=1, yscale=0.15) + self._row.pack_end(buddies, expand=False) - 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) + self.show_all() - 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) + def do_fill_in_cell_content(self, table, metadata): + for i in self._row.get_children(): + i.check_out(metadata) - 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): - if self.child == self._scrolled_window: - return - self.remove(self.child) - self.add(self._scrolled_window) - self._scrolled_window.show() - - def update_dates(self): - if not self.tree_view.flags() & gtk.REALIZED: - return - visible_range = self.tree_view.get_visible_range() - if visible_range is None: - return - - logging.debug('ListView.update_dates') - - path, end_path = visible_range - tree_model = self.tree_view.get_model() - - while True: - x, y, width, height = self.tree_view.get_cell_area(path, - self.date_column) - x, y = self.tree_view.convert_tree_to_widget_coords(x, y) - self.tree_view.queue_draw_area(x, y, width, height) - if path == end_path: - break - else: - next_iter = tree_model.iter_next(tree_model.get_iter(path)) - path = tree_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): - if visible != self._fully_obscured: - return - - 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])) - } +class ListView(HomogeneView): def __init__(self): - BaseListView.__init__(self) - self._is_dragging = False - - self.tree_view.connect('drag-begin', self.__drag_begin_cb) - self.tree_view.connect('button-release-event', - self.__button_release_event_cb) - - self.cell_title.connect('edited', self.__cell_title_edited_cb) - self.cell_title.connect('editing-canceled', self.__editing_canceled_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) - - def __drag_begin_cb(self, widget, drag_context): - self._is_dragging = True - - def __button_release_event_cb(self, tree_view, event): - try: - if self._is_dragging: - return - finally: - self._is_dragging = False - - pos = tree_view.get_path_at_pos(int(event.x), int(event.y)) - if pos is None: - return - - path, column, x_, y_ = pos - if column != self._title_column: - return - - row = self.tree_view.get_model()[path] - metadata = model.get(row[ListModel.COLUMN_UID]) - self.cell_title.props.editable = model.is_editable(metadata) - - tree_view.set_cursor_on_cell(path, column, start_editing=True) - - def __detail_cell_clicked_cb(self, cell, path): - row = self.tree_view.get_model()[path] - self.emit('detail-clicked', row[ListModel.COLUMN_UID]) - - def __detail_clicked_cb(self, cell, uid): - self.emit('detail-clicked', uid) - - def __icon_clicked_cb(self, cell, path): - row = self.tree_view.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] - metadata = model.get(row[ListModel.COLUMN_UID]) - metadata['title'] = new_text - model.write(metadata, update_mtime=False) - self.cell_title.props.editable = False - - def __editing_canceled_cb(self, cell): - self.cell_title.props.editable = False - -class CellRendererFavorite(CellRendererIcon): - __gtype_name__ = 'JournalCellRendererFavorite' - - def __init__(self, tree_view): - CellRendererIcon.__init__(self, tree_view) - - self.props.width = style.GRID_CELL_SIZE - self.props.height = style.GRID_CELL_SIZE - self.props.size = style.SMALL_ICON_SIZE - self.props.icon_name = 'emblem-favorite' - self.props.mode = gtk.CELL_RENDERER_MODE_ACTIVATABLE - self.props.prelit_stroke_color = style.COLOR_BUTTON_GREY.get_svg() - self.props.prelit_fill_color = style.COLOR_BUTTON_GREY.get_svg() - -class CellRendererDetail(CellRendererIcon): - __gtype_name__ = 'JournalCellRendererDetail' - - def __init__(self, tree_view): - CellRendererIcon.__init__(self, tree_view) - - self.props.width = style.GRID_CELL_SIZE - self.props.height = style.GRID_CELL_SIZE - self.props.size = style.SMALL_ICON_SIZE - self.props.icon_name = 'go-right' - self.props.mode = gtk.CELL_RENDERER_MODE_ACTIVATABLE - self.props.stroke_color = style.COLOR_TRANSPARENT.get_svg() - self.props.fill_color = style.COLOR_BUTTON_GREY.get_svg() - self.props.prelit_stroke_color = style.COLOR_TRANSPARENT.get_svg() - self.props.prelit_fill_color = style.COLOR_BLACK.get_svg() - -class CellRendererActivityIcon(CellRendererIcon): - __gtype_name__ = 'JournalCellRendererActivityIcon' - - __gsignals__ = { - 'detail-clicked': (gobject.SIGNAL_RUN_FIRST, - gobject.TYPE_NONE, - ([str])), - } - - def __init__(self, tree_view): - self._show_palette = True - - CellRendererIcon.__init__(self, tree_view) - - self.props.width = style.GRID_CELL_SIZE - self.props.height = style.GRID_CELL_SIZE - self.props.size = style.STANDARD_ICON_SIZE - self.props.mode = gtk.CELL_RENDERER_MODE_ACTIVATABLE - - self.tree_view = tree_view - - def create_palette(self): - if not self._show_palette: - return None - - tree_model = self.tree_view.get_model() - metadata = tree_model.get_metadata(self.props.palette_invoker.path) - - palette = ObjectPalette(metadata, detail=True) - palette.connect('detail-clicked', - self.__detail_clicked_cb) - return palette - - def __detail_clicked_cb(self, palette, uid): - self.emit('detail-clicked', uid) - - def set_show_palette(self, show_palette): - self._show_palette = show_palette - - show_palette = gobject.property(type=bool, default=True, - setter=set_show_palette) - -class CellRendererBuddy(CellRendererIcon): - __gtype_name__ = 'JournalCellRendererBuddy' - - def __init__(self, tree_view, column_index): - CellRendererIcon.__init__(self, tree_view) - - self.props.width = style.STANDARD_ICON_SIZE - self.props.height = style.STANDARD_ICON_SIZE - self.props.size = style.STANDARD_ICON_SIZE - self.props.mode = gtk.CELL_RENDERER_MODE_ACTIVATABLE - - self.tree_view = tree_view - self._model_column_index = column_index - - def create_palette(self): - tree_model = self.tree_view.get_model() - row = tree_model[self.props.palette_invoker.path] - - if row[self._model_column_index] is not None: - nick, xo_color = row[self._model_column_index] - return BuddyPalette((nick, xo_color.to_string())) - else: - return None - - def set_buddy(self, buddy): - if buddy is None: - self.props.icon_name = None - else: - nick_, xo_color = buddy - self.props.icon_name = 'computer-xo' - self.props.xo_color = xo_color - - buddy = gobject.property(type=object, setter=set_buddy) - + HomogeneView.__init__(self, _Cell) + self.frame_size = (None, 1) + self.cell_size = (None, style.GRID_CELL_SIZE) diff --git a/src/jarabe/journal/objectchooser.py b/src/jarabe/journal/objectchooser.py index 49af3e6..bae7b74 100644 --- a/src/jarabe/journal/objectchooser.py +++ b/src/jarabe/journal/objectchooser.py @@ -24,8 +24,7 @@ import wnck from sugar.graphics import style from sugar.graphics.toolbutton import ToolButton -from jarabe.journal.listview import BaseListView -from jarabe.journal.listmodel import ListModel +from jarabe.journal.view import View from jarabe.journal.journaltoolbox import SearchToolbar from jarabe.journal.volumestoolbar import VolumesToolbar @@ -80,14 +79,16 @@ class ObjectChooser(gtk.Window): self._toolbar = SearchToolbar() self._toolbar.connect('query-changed', self.__query_changed_cb) + self._toolbar.connect('view-changed', self.__view_changed_cb) self._toolbar.set_size_request(-1, style.GRID_CELL_SIZE) vbox.pack_start(self._toolbar, expand=False) self._toolbar.show() - self._list_view = ChooserListView() - self._list_view.connect('entry-activated', self.__entry_activated_cb) - vbox.pack_start(self._list_view) - self._list_view.show() + self._view = View() + self._view.props.hover_selection = True + self._view.connect('entry-activated', self.__entry_activated_cb) + vbox.pack_start(self._view) + self._view.show() self._toolbar.set_mount_point('/') @@ -125,7 +126,10 @@ class ObjectChooser(gtk.Window): return self._selected_object_id def __query_changed_cb(self, toolbar, query): - self._list_view.update_with_query(query) + self._view.update_with_query(query) + + def __view_changed_cb(self, sender, view): + self._view.change_view(view) def __volume_changed_cb(self, volume_toolbar, mount_point): logging.debug('Selected volume: %r.', mount_point) @@ -134,7 +138,7 @@ class ObjectChooser(gtk.Window): 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._view.set_is_visible(visible) class TitleBox(VolumesToolbar): __gtype_name__ = 'TitleBox' @@ -161,39 +165,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/palettes.py b/src/jarabe/journal/palettes.py index e0dfbf4..10cc47a 100644 --- a/src/jarabe/journal/palettes.py +++ b/src/jarabe/journal/palettes.py @@ -33,17 +33,12 @@ from jarabe.model import friends from jarabe.model import filetransfer from jarabe.journal import misc from jarabe.journal import model +from jarabe.journal import controler class ObjectPalette(Palette): __gtype_name__ = 'ObjectPalette' - __gsignals__ = { - 'detail-clicked': (gobject.SIGNAL_RUN_FIRST, - gobject.TYPE_NONE, - ([str])), - } - def __init__(self, metadata, detail=False): self._metadata = metadata @@ -135,7 +130,7 @@ class ObjectPalette(Palette): model.delete(self._metadata['uid']) def __detail_activate_cb(self, menu_item): - self.emit('detail-clicked', self._metadata['uid']) + controler.objects.emit('detail-clicked', self._metadata['uid']) def __friend_selected_cb(self, menu_item, buddy): logging.debug('__friend_selected_cb') diff --git a/src/jarabe/journal/thumbsview.py b/src/jarabe/journal/thumbsview.py new file mode 100644 index 0000000..f2db248 --- /dev/null +++ b/src/jarabe/journal/thumbsview.py @@ -0,0 +1,101 @@ +# 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 logging + +from jarabe.journal.homogeneview import HomogeneView +from jarabe.journal.homogeneview import Cell +from jarabe.journal.widgets import * + + +TOOLBAR_WIDTH = 20 + +THUMB_WIDTH = 240 +THUMB_HEIGHT = 180 + +TEXT_HEIGHT = gtk.EventBox().create_pango_layout('W').get_pixel_size()[1] + +CELL_WIDTH = THUMB_WIDTH + TOOLBAR_WIDTH + style.DEFAULT_PADDING + \ + style.DEFAULT_SPACING +CELL_HEIGHT = THUMB_HEIGHT + TEXT_HEIGHT * 3 + style.DEFAULT_PADDING * 3 + \ + style.DEFAULT_SPACING + + +class _Cell(Cell): + + def __init__(self): + Cell.__init__(self) + + cell = gtk.HBox() + self.add(cell) + + # toolbar + + toolbar = gtk.VBox() + cell.pack_start(toolbar, expand=False) + + self._keep = KeepIcon( + box_width=style.GRID_CELL_SIZE) + toolbar.pack_start(self._keep, expand=False) + + self._details = DetailsIcon() + toolbar.pack_start(self._details, expand=False) + + # thumb + + main = gtk.VBox() + cell.pack_end(main) + + #thumb = Thumb() + #main.pack_end(thumb) + + # text + + text = gtk.VBox() + main.pack_end(text, expand=False) + + self._title = Title( + max_line_count=2, + xalign=0, yalign=0, xscale=1, yscale=0) + text.pack_start(self._title) + + self._date = Timestamp( + xalign=0.0, + ellipsize=pango.ELLIPSIZE_END) + text.pack_end(self._date, expand=False) + + self.show_all() + + def do_fill_in_cell_content(self, table, metadata): + self._keep.check_out(metadata) + self._details.check_out(metadata) + self._title.check_out(metadata) + self._date.check_out(metadata) + + +class ThumbsView(HomogeneView): + + def __init__(self): + HomogeneView.__init__(self, _Cell) + + def do_size_allocate(self, allocation): + column_count = gtk.gdk.screen_width() / CELL_WIDTH + row_count = gtk.gdk.screen_height() / CELL_HEIGHT + self.frame_size = (row_count, column_count) + + HomogeneView.do_size_allocate(self, allocation) diff --git a/src/jarabe/journal/view.py b/src/jarabe/journal/view.py new file mode 100644 index 0000000..61b11dc --- /dev/null +++ b/src/jarabe/journal/view.py @@ -0,0 +1,292 @@ +# Copyright (C) 2009, Tomeu Vizoso +# 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 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 +from sugar.graphics.icon import Icon + +from jarabe.journal import model +from jarabe.journal.listview import ListView +from jarabe.journal.thumbsview import ThumbsView + + +UPDATE_INTERVAL = 300 + +VIEW_LIST = 0 +VIEW_THUMBS = 1 +_MESSAGE_PROGRESS = 2 +_MESSAGE_EMPTY_JOURNAL = 3 +_MESSAGE_NO_MATCH = 4 + +PAGE_SIZE = 10 + + +class View(gtk.EventBox): + + __gsignals__ = { + 'clear-clicked': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([])), + 'entry-activated': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([str])), + } + + def __init__(self): + gtk.EventBox.__init__(self) + + self._query = {} + self._result_set = None + self._pages = {} + self._current_page = None + self._view = None + self._last_progress_bar_pulse = None + + self._page_ctors = { + VIEW_LIST: lambda: self._view_new(ListView), + VIEW_THUMBS: lambda: self._view_new(ThumbsView), + _MESSAGE_PROGRESS: self._progress_new, + _MESSAGE_EMPTY_JOURNAL: self._message_new, + _MESSAGE_NO_MATCH: self._message_new, + } + + self.connect('destroy', self.__destroy_cb) + + # Auto-update stuff + self._fully_obscured = True + self._dirty = False + 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) + + self.view = VIEW_LIST + + def get_view(self): + return self._pages[self._view].child + + def set_view(self, view): + if self._page == self._view: + # change view only if current page is view as well + self._page = view + self._view = view + self.view.result_set = self._result_set + + view = property(get_view, set_view) + + def set_hover_selection(self, hover_selection): + for i in self._view_widgets: + i.child.props.hover_selection = hover_selection + + hover_selection = gobject.property(type=bool, default=False, + setter=set_hover_selection) + + def _set_page(self, page): + if self._current_page == page: + return + self._current_page = page + + if page in self._pages: + child = self._pages[page] + else: + child = self._page_ctors[page]() + child.show_all() + self._pages[page] = child + + if self.child is not None: + self.remove(self.child) + self.add(child) + + def _get_page(self): + return self._current_page + + _page = property(_get_page, _set_page) + + 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 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('View._refresh query %r', self._query) + + if self._result_set is not None: + self._result_set.stop() + self._dirty = False + + 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() + + self._page = self._view + self.view.result_set = self._result_set + + def __result_set_ready_cb(self, **kwargs): + if self._result_set.length == 0: + if self._is_query_empty(): + self._page = _MESSAGE_EMPTY_JOURNAL + else: + self._page = _MESSAGE_NO_MATCH + else: + self._page = self._view + self.view.result_set = self._result_set + + 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 __result_set_progress_cb(self, **kwargs): + if self._page != _MESSAGE_PROGRESS: + self._last_progress_bar_pulse = time.time() + self._page = _MESSAGE_PROGRESS + + if time.time() - self._last_progress_bar_pulse > 0.05: + self.child.pulse() + self._last_progress_bar_pulse = time.time() + + def _view_new(self, view_class): + view = view_class() + view.modify_bg(gtk.STATE_NORMAL, style.COLOR_WHITE.get_gdk_color()) + view.connect('entry-activated', self.__entry_activated_cb) + + scrolled_view = gtk.ScrolledWindow() + scrolled_view.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) + scrolled_view.add(view) + + return scrolled_view + + def _progress_new(self): + alignment = gtk.Alignment(xalign=0.5, yalign=0.5, xscale=0.5) + + progress_bar = gtk.ProgressBar() + progress_bar.props.pulse_step = 0.01 + alignment.add(progress_bar) + + return alignment + + def _message_new(self): + canvas = hippo.Canvas() + + 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 self._page == _MESSAGE_EMPTY_JOURNAL: + text = _('Your Journal is empty') + elif self._page == _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 self._page == _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) + + return canvas + + def __clear_button_clicked_cb(self, button): + self.emit('clear-clicked') + + def update_dates(self): + self.view.refill() + + def _set_dirty(self): + if self._fully_obscured: + self._dirty = True + else: + self.refresh() + + def set_is_visible(self, visible): + if visible != self._fully_obscured: + return + + 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 + + def __entry_activated_cb(self, sender, uid): + self.emit('entry-activated', uid) diff --git a/src/jarabe/journal/widgets.py b/src/jarabe/journal/widgets.py new file mode 100644 index 0000000..7593986 --- /dev/null +++ b/src/jarabe/journal/widgets.py @@ -0,0 +1,331 @@ +# Copyright (C) 2006, Red Hat, Inc. +# Copyright (C) 2007, One Laptop Per Child +# 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 logging +from gettext import gettext as _ + +import gtk +import gobject +import hippo +import gconf +import pango +import simplejson + +from sugar.graphics import style +from sugar.graphics.icon import CanvasIcon +from sugar.graphics.xocolor import XoColor +from sugar.graphics.palette import CanvasInvoker +from sugar.graphics.roundbox import CanvasRoundBox + +from jarabe.journal.entry import Entry +from jarabe.journal.palettes import BuddyPalette +from jarabe.journal.palettes import ObjectPalette +from jarabe.journal import misc +from jarabe.journal import model +from jarabe.journal import controler + + +class KeepIconCanvas(CanvasIcon): + def __init__(self, **kwargs): + CanvasIcon.__init__(self, icon_name='emblem-favorite', + size=style.SMALL_ICON_SIZE, + **kwargs) + + self._metadata = None + self._prelight = False + self._keep_color = None + + self.connect_after('activated', self.__activated_cb) + self.connect('motion-notify-event', self.__motion_notify_event_cb) + + def check_out(self, metadata): + self._metadata = metadata + keep = metadata.get('keep', "") + if keep.isdigit(): + self._set_keep(int(keep)) + else: + self._set_keep(0) + + def _set_keep(self, keep): + if keep: + client = gconf.client_get_default() + color = client.get_string('/desktop/sugar/user/color') + self._keep_color = XoColor(color) + else: + self._keep_color = None + + self._set_colors() + + def __motion_notify_event_cb(self, icon, event): + if event.detail == hippo.MOTION_DETAIL_ENTER: + self._prelight = True + elif event.detail == hippo.MOTION_DETAIL_LEAVE: + self._prelight = False + self._set_colors() + + def _set_colors(self): + if self._prelight: + if self._keep_color is None: + self.props.stroke_color = style.COLOR_BUTTON_GREY.get_svg() + self.props.fill_color = style.COLOR_BUTTON_GREY.get_svg() + else: + stroke_color = style.Color(self._keep_color.get_stroke_color()) + fill_color = style.Color(self._keep_color.get_fill_color()) + self.props.stroke_color = fill_color.get_svg() + self.props.fill_color = stroke_color.get_svg() + else: + if self._keep_color is None: + self.props.stroke_color = style.COLOR_BUTTON_GREY.get_svg() + self.props.fill_color = style.COLOR_TRANSPARENT.get_svg() + else: + self.props.xo_color = self._keep_color + + def __activated_cb(self, icon): + if not model.is_editable(self._metadata): + return + + if self._keep_color is None: + keep = 1 + else: + keep = 0 + + self._metadata['keep'] = keep + model.write(self._metadata, update_mtime=False) + + self._set_keep(keep) + + +def KeepIcon(**kwargs): + return _CanvasToWidget(KeepIconCanvas, **kwargs) + + +class _Launcher(object): + + def __init__(self, launching, detail): + self.metadata = None + self._detail = detail + self._launching = launching + + if launching: + self.connect_after('button-release-event', + self.__button_release_event_cb) + + def create_palette(self): + if not self._launching or self.metadata is None: + return + else: + return ObjectPalette(self.metadata, detail=self._detail) + + def __button_release_event_cb(self, button, event): + if self.metadata is not None: + misc.resume(self.metadata) + return True + + +class ObjectIconCanvas(_Launcher, CanvasIcon): + + def __init__(self, launching=True, detail=True, **kwargs): + CanvasIcon.__init__(self, **kwargs) + _Launcher.__init__(self, launching, detail) + + def check_out(self, metadata): + self.metadata = metadata + + self.props.file_name = misc.get_icon_name(metadata) + + if misc.is_activity_bundle(metadata): + self.props.fill_color = style.COLOR_TRANSPARENT.get_svg() + self.props.stroke_color = style.COLOR_BUTTON_GREY.get_svg() + else: + self.props.xo_color = misc.get_icon_color(metadata) + + +def ObjectIcon(**kwargs): + return _CanvasToWidget(ObjectIconCanvas, **kwargs) + + +class Title(gtk.Alignment): + + def __init__(self, max_line_count=1, **kwargs): + gtk.Alignment.__init__(self, **kwargs) + + self._metadata = None + + self._entry = Entry(max_line_count=max_line_count) + self.add(self._entry) + + self._entry.connect_after('focus-out-event', self.__focus_out_event_cb) + + def check_out(self, metadata): + self._metadata = metadata + self._entry.props.text = metadata.get('title', _('Untitled')) + self._entry.props.editable = model.is_editable(metadata) + + def __focus_out_event_cb(self, widget, event): + old_title = self._metadata.get('title', None) + new_title = self._entry.props.text + + if old_title != new_title: + self._metadata['title'] = new_title + self._metadata['title_set_by_user'] = '1' + model.write(self._metadata, update_mtime=False) + + +class Buddies(gtk.Alignment): + + def __init__(self, buddies_max=None, **kwargs): + gtk.Alignment.__init__(self, **kwargs) + + self._buddies_max = buddies_max + + self._progress = gtk.ProgressBar() + self._progress.modify_bg(gtk.STATE_INSENSITIVE, + style.COLOR_WHITE.get_gdk_color()) + self._progress.show() + + self._buddies = gtk.HBox() + self._buddies.show() + + def check_out(self, metadata): + if self.child is not None: + self.remove(self.child) + + child = None + + if 'progress' in metadata: + child = self._progress + fraction = int(metadata['progress']) / 100. + self._progress.props.fraction = fraction + + elif 'buddies' in metadata and metadata['buddies']: + child = self._buddies + + buddies = simplejson.loads(metadata['buddies']).values() + buddies = buddies[:self._buddies_max] + + def show(icon, buddy): + icon.root.buddy = buddy + nick_, color = buddy + icon.root.props.xo_color = XoColor(color) + icon.show() + + for icon in self._buddies: + if buddies: + show(icon, buddies.pop()) + else: + icon.hide() + + for buddy in buddies: + icon = _CanvasToWidget(_BuddyIcon) + show(icon, buddy) + self._buddies.add(icon) + + if self.child is not child: + if self.child is not None: + self.remove(self.child) + if child is not None: + self.add(child) + + +class Timestamp(gtk.Label): + + def __init__(self, **kwargs): + gobject.GObject.__init__(self, **kwargs) + + def check_out(self, metadata): + self.props.label = misc.get_date(metadata) + + +class DetailsIconCanvas(CanvasIcon): + + def __init__(self): + CanvasIcon.__init__(self, + box_width=style.GRID_CELL_SIZE, + icon_name='go-right', + size=style.SMALL_ICON_SIZE, + stroke_color=style.COLOR_TRANSPARENT.get_svg()) + + self._metadata = None + + self.connect('motion-notify-event', self.__motion_notify_event_cb) + self.connect_after('activated', self.__activated_cb) + + self._set_leave_color() + + def check_out(self, metadata): + self._metadata = metadata + + def _set_leave_color(self): + self.props.fill_color = style.COLOR_BUTTON_GREY.get_svg() + + def __activated_cb(self, button): + self._set_leave_color() + controler.objects.emit('detail-clicked', self._metadata['uid']) + + def __motion_notify_event_cb(self, icon, event): + if event.detail == hippo.MOTION_DETAIL_ENTER: + icon.props.fill_color = style.COLOR_BLACK.get_svg() + elif event.detail == hippo.MOTION_DETAIL_LEAVE: + self._set_leave_color() + + +def DetailsIcon(**kwargs): + return _CanvasToWidget(DetailsIconCanvas, **kwargs) + + +class ThumbCanvas(_Launcher, hippo.CanvasWidget): + + def __init__(self, cell, **kwargs): + hippo.CanvasWidget.__init__(self, **kwargs) + _Launcher.__init__(self, cell) + + self._palette_invoker = CanvasInvoker() + self._palette_invoker.attach(self) + self.connect('destroy', self.__destroy_cb) + + def __destroy_cb(self, icon): + if self._palette_invoker is not None: + self._palette_invoker.detach() + + +class _BuddyIcon(CanvasIcon): + + def __init__(self): + CanvasIcon.__init__(self, + icon_name='computer-xo', + size=style.STANDARD_ICON_SIZE) + + self.buddy = None + + def create_palette(self): + return BuddyPalette(self.buddy) + + +class _CanvasToWidget(hippo.Canvas): + + def __init__(self, canvas_class, **kwargs): + hippo.Canvas.__init__(self) + + self.modify_bg(gtk.STATE_NORMAL, + style.COLOR_WHITE.get_gdk_color()) + + self.root = canvas_class(**kwargs) + self.set_root(self.root) + + def check_out(self, metadata): + self.root.check_out(metadata) -- cgit v0.9.1