Commit from One Laptop Per Child: Translation System by user HoboPrimate. 31 of 31...
[journal-activity.git] / listview.py
blobc9621a67f7bd2aec5816b120eb69d1382af1b658
1 # Copyright (C) 2007, One Laptop Per Child
3 # This program is free software; you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License as published by
5 # the Free Software Foundation; either version 2 of the License, or
6 # (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software
15 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
17 import logging
18 import traceback
19 import sys
20 from gettext import gettext as _
22 import hippo
23 import gobject
24 import gtk
25 import dbus
27 from sugar.graphics import style
28 from sugar.graphics.icon import CanvasIcon
30 from collapsedentry import CollapsedEntry
31 import query
33 DS_DBUS_SERVICE = 'org.laptop.sugar.DataStore'
34 DS_DBUS_INTERFACE = 'org.laptop.sugar.DataStore'
35 DS_DBUS_PATH = '/org/laptop/sugar/DataStore'
37 UPDATE_INTERVAL = 300000
39 EMPTY_JOURNAL = _("Your Journal is empty")
40 NO_MATCH = _("No matching entries ")
42 class BaseListView(gtk.HBox):
43 __gtype_name__ = 'BaseListView'
45 def __init__(self):
46 self._query = {}
47 self._result_set = None
48 self._entries = []
49 self._page_size = 0
50 self._last_value = -1
51 self._reflow_sid = 0
53 gtk.HBox.__init__(self)
54 self.set_flags(gtk.HAS_FOCUS|gtk.CAN_FOCUS)
55 self.connect('key-press-event', self._key_press_event_cb)
57 self._box = hippo.CanvasBox(
58 orientation=hippo.ORIENTATION_VERTICAL,
59 background_color=style.COLOR_WHITE.get_int())
61 self._canvas = hippo.Canvas()
62 self._canvas.set_root(self._box)
64 self.pack_start(self._canvas)
65 self._canvas.show()
67 self._vadjustment = gtk.Adjustment(value=0, lower=0, upper=0,
68 step_incr=1, page_incr=0, page_size=0)
69 self._vadjustment.connect('value-changed', self._vadjustment_value_changed_cb)
70 self._vadjustment.connect('changed', self._vadjustment_changed_cb)
72 self._vscrollbar = gtk.VScrollbar(self._vadjustment)
73 self.pack_end(self._vscrollbar, expand=False, fill=False)
74 self._vscrollbar.show()
76 self.connect('scroll-event', self._scroll_event_cb)
77 self.connect('destroy', self.__destroy_cb)
79 # DND stuff
80 self._pressed_button = None
81 self._press_start_x = None
82 self._press_start_y = None
83 self._last_clicked_entry = None
84 self._canvas.drag_source_set(0, [], 0)
85 self._canvas.add_events(gtk.gdk.BUTTON_PRESS_MASK |
86 gtk.gdk.POINTER_MOTION_HINT_MASK)
87 self._canvas.connect_after("motion_notify_event",
88 self._canvas_motion_notify_event_cb)
89 self._canvas.connect("button_press_event",
90 self._canvas_button_press_event_cb)
91 self._canvas.connect("drag_end", self._drag_end_cb)
92 self._canvas.connect("drag_data_get", self._drag_data_get_cb)
94 # Auto-update stuff
95 self._fully_obscured = True
96 self._dirty = False
97 self._refresh_idle_handler = None
98 self._update_dates_timer = None
100 bus = dbus.SessionBus()
101 datastore = dbus.Interface(
102 bus.get_object(DS_DBUS_SERVICE, DS_DBUS_PATH), DS_DBUS_INTERFACE)
103 self._datastore_created_handler = \
104 datastore.connect_to_signal('Created',
105 self.__datastore_created_cb)
106 self._datastore_updated_handler = \
107 datastore.connect_to_signal('Updated', self.__datastore_updated_cb)
109 self._datastore_deleted_handler = \
110 datastore.connect_to_signal('Deleted', self.__datastore_deleted_cb)
112 def __destroy_cb(self, widget):
113 self._datastore_created_handler.remove()
114 self._datastore_updated_handler.remove()
115 self._datastore_deleted_handler.remove()
117 if self._result_set:
118 self._result_set.destroy()
120 def _vadjustment_changed_cb(self, vadjustment):
121 logging.debug('_vadjustment_changed_cb:\n \t%r\n \t%r\n \t%r\n \t%r\n \t%r\n' % \
122 (vadjustment.props.lower, vadjustment.props.page_increment,
123 vadjustment.props.page_size, vadjustment.props.step_increment,
124 vadjustment.props.upper))
125 if vadjustment.props.upper > self._page_size:
126 self._vscrollbar.show()
127 else:
128 self._vscrollbar.hide()
130 def _vadjustment_value_changed_cb(self, vadjustment):
131 gobject.idle_add(self._do_scroll)
133 def _do_scroll(self, force=False):
134 import time
135 t = time.time()
137 value = int(self._vadjustment.props.value)
139 if value == self._last_value and not force:
140 return
141 self._last_value = value
143 self._result_set.seek(value)
144 jobjects = self._result_set.read(self._page_size)
146 if self._result_set.length != self._vadjustment.props.upper:
147 self._vadjustment.props.upper = self._result_set.length
148 self._vadjustment.changed()
150 self._refresh_view(jobjects)
151 self._dirty = False
153 logging.debug('_do_scroll %r %r\n' % (value, (time.time() - t)))
155 return False
157 def _refresh_view(self, jobjects):
158 logging.debug('ListView %r' % self)
159 # Indicate when the Journal is empty
160 if len(jobjects) == 0:
161 self._show_message(EMPTY_JOURNAL)
162 return
164 # Refresh view and create the entries if they don't exist yet.
165 for i in range(0, self._page_size):
166 try:
167 if i < len(jobjects):
168 if i >= len(self._entries):
169 entry = self.create_entry()
170 self._box.append(entry)
171 self._entries.append(entry)
172 entry.jobject = jobjects[i]
173 else:
174 entry = self._entries[i]
175 entry.jobject = jobjects[i]
176 entry.set_visible(True)
177 elif i < len(self._entries):
178 entry = self._entries[i]
179 entry.set_visible(False)
180 except Exception:
181 logging.error('Exception while displaying entry:\n' + \
182 ''.join(traceback.format_exception(*sys.exc_info())))
184 def create_entry(self):
185 """ Create a descendant of BaseCollapsedEntry
187 raise NotImplementedError
189 def update_with_query(self, query):
190 logging.debug('ListView.update_with_query')
191 self._query = query
192 if self._page_size > 0:
193 self.refresh()
195 def refresh(self):
196 if self._result_set:
197 self._result_set.destroy()
198 self._result_set = query.find(self._query)
199 self._vadjustment.props.upper = self._result_set.length
200 self._vadjustment.changed()
202 self._vadjustment.props.value = min(self._vadjustment.props.value,
203 self._result_set.length - self._page_size)
204 if self._result_set.length == 0:
205 if self._query.get('query', '') or \
206 self._query.get('mime_type', '') or \
207 self._query.get('mtime', ''):
208 self._show_message(NO_MATCH)
209 else:
210 self._show_message(EMPTY_JOURNAL)
211 else:
212 self._clear_message()
213 self._do_scroll(force=True)
215 def _scroll_event_cb(self, hbox, event):
216 if event.direction == gtk.gdk.SCROLL_UP:
217 if self._vadjustment.props.value > self._vadjustment.props.lower:
218 self._vadjustment.props.value -= 1
219 elif event.direction == gtk.gdk.SCROLL_DOWN:
220 max_value = self._result_set.length - self._page_size
221 if self._vadjustment.props.value < max_value:
222 self._vadjustment.props.value += 1
224 def do_focus(self, direction):
225 if not self.is_focus():
226 self.grab_focus()
227 return True
228 return False
230 def _key_press_event_cb(self, widget, event):
231 keyname = gtk.gdk.keyval_name(event.keyval)
233 if keyname == 'Up':
234 if self._vadjustment.props.value > self._vadjustment.props.lower:
235 self._vadjustment.props.value -= 1
236 elif keyname == 'Down':
237 max_value = self._result_set.length - self._page_size
238 if self._vadjustment.props.value < max_value:
239 self._vadjustment.props.value += 1
240 elif keyname == 'Page_Up' or keyname == 'KP_Page_Up':
241 new_position = max(0, self._vadjustment.props.value - self._page_size)
242 if new_position != self._vadjustment.props.value:
243 self._vadjustment.props.value = new_position
244 elif keyname == 'Page_Down' or keyname == 'KP_Page_Down':
245 new_position = min(self._result_set.length - self._page_size,
246 self._vadjustment.props.value + self._page_size)
247 if new_position != self._vadjustment.props.value:
248 self._vadjustment.props.value = new_position
249 elif keyname == 'Home' or keyname == 'KP_Home':
250 new_position = 0
251 if new_position != self._vadjustment.props.value:
252 self._vadjustment.props.value = new_position
253 elif keyname == 'End' or keyname == 'KP_End':
254 new_position = max(0, self._result_set.length - self._page_size)
255 if new_position != self._vadjustment.props.value:
256 self._vadjustment.props.value = new_position
257 else:
258 return False
260 return True
262 def do_size_allocate(self, allocation):
263 gtk.HBox.do_size_allocate(self, allocation)
264 new_page_size = int(allocation.height / style.GRID_CELL_SIZE)
266 logging.debug("do_size_allocate: %r" % new_page_size)
268 if new_page_size != self._page_size:
269 self._page_size = new_page_size
270 self._queue_reflow()
272 def _queue_reflow(self):
273 if not self._reflow_sid:
274 self._reflow_sid = gobject.idle_add(self._reflow_idle_cb)
276 def _reflow_idle_cb(self):
277 self._box.clear()
278 self._entries = []
280 self._vadjustment.props.page_size = self._page_size
281 self._vadjustment.props.page_increment = self._page_size
282 self._vadjustment.changed()
284 if self._result_set is None:
285 self._result_set = query.find(self._query)
287 max_value = max(0, self._result_set.length - self._page_size)
288 if self._vadjustment.props.value > max_value:
289 self._vadjustment.props.value = max_value
290 else:
291 self._do_scroll(force=True)
293 self._reflow_sid = 0
295 def _show_message(self, message):
296 box = hippo.CanvasBox(orientation=hippo.ORIENTATION_VERTICAL,
297 background_color=style.COLOR_WHITE.get_int(),
298 yalign=hippo.ALIGNMENT_CENTER)
299 icon = CanvasIcon(size=style.LARGE_ICON_SIZE,
300 file_name='activity/activity-journal.svg',
301 stroke_color = style.COLOR_BUTTON_GREY.get_svg(),
302 fill_color = style.COLOR_TRANSPARENT.get_svg())
303 text = hippo.CanvasText(text=message,
304 xalign=hippo.ALIGNMENT_CENTER,
305 font_desc=style.FONT_NORMAL.get_pango_desc(),
306 color = style.COLOR_BUTTON_GREY.get_int())
308 box.append(icon)
309 box.append(text)
310 self._canvas.set_root(box)
312 def _clear_message(self):
313 self._canvas.set_root(self._box)
315 # TODO: Dnd methods. This should be merged somehow inside hippo-canvas.
316 def _canvas_motion_notify_event_cb(self, widget, event):
317 if not self._pressed_button:
318 return True
320 # if the mouse button is not pressed, no drag should occurr
321 if not event.state & gtk.gdk.BUTTON1_MASK:
322 self._pressed_button = None
323 return True
325 logging.debug("motion_notify_event_cb")
327 if event.is_hint:
328 x, y, state_ = event.window.get_pointer()
329 else:
330 x = event.x
331 y = event.y
333 if widget.drag_check_threshold(int(self._press_start_x),
334 int(self._press_start_y),
335 int(x),
336 int(y)):
337 context_ = widget.drag_begin([('text/uri-list', 0, 0),
338 ('journal-object-id', 0, 0)],
339 gtk.gdk.ACTION_COPY,
341 event)
342 return True
344 def _drag_end_cb(self, widget, drag_context):
345 logging.debug("drag_end_cb")
346 self._pressed_button = None
347 self._press_start_x = None
348 self._press_start_y = None
349 self._last_clicked_entry = None
351 def _drag_data_get_cb(self, widget, context, selection, targetType, eventTime):
352 logging.debug("drag_data_get_cb: requested target " + selection.target)
354 jobject = self._last_clicked_entry.jobject
355 if selection.target == 'text/uri-list':
356 selection.set(selection.target, 8, jobject.file_path)
357 elif selection.target == 'journal-object-id':
358 selection.set(selection.target, 8, jobject.object_id)
360 def _canvas_button_press_event_cb(self, widget, event):
361 logging.debug("button_press_event_cb")
363 if event.button == 1 and event.type == gtk.gdk.BUTTON_PRESS:
364 self._last_clicked_entry = self._get_entry_at_coords(event.x, event.y)
365 if self._last_clicked_entry:
366 self._pressed_button = event.button
367 self._press_start_x = event.x
368 self._press_start_y = event.y
370 return False
372 def _get_entry_at_coords(self, x, y):
373 for entry in self._box.get_children():
374 entry_x, entry_y = entry.get_context().translate_to_widget(entry)
375 entry_width, entry_height = entry.get_allocation()
377 if (x >= entry_x ) and (x <= entry_x + entry_width) and \
378 (y >= entry_y ) and (y <= entry_y + entry_height):
379 return entry
380 return None
382 def update_dates(self):
383 logging.debug('ListView.update_dates')
384 for entry in self._entries:
385 entry.update_date()
387 def __datastore_created_cb(self, uid):
388 self._set_dirty()
390 def __datastore_updated_cb(self, uid):
391 self._set_dirty()
393 def __datastore_deleted_cb(self, uid):
394 self._set_dirty()
396 def _set_dirty(self):
397 if self._fully_obscured:
398 self._dirty = True
399 else:
400 self._schedule_refresh()
402 def _schedule_refresh(self):
403 if self._refresh_idle_handler is None:
404 logging.debug('Add refresh idle callback')
405 self._refresh_idle_handler = \
406 gobject.idle_add(self.__refresh_idle_cb)
408 def __refresh_idle_cb(self):
409 self.refresh()
410 if self._refresh_idle_handler is not None:
411 logging.debug('Remove refresh idle callback')
412 gobject.source_remove(self._refresh_idle_handler)
413 self._refresh_idle_handler = None
414 return False
416 def set_is_visible(self, visible):
417 logging.debug('canvas_visibility_notify_event_cb %r' % visible)
418 if visible:
419 self._fully_obscured = False
420 if self._dirty:
421 self._schedule_refresh()
422 if self._update_dates_timer is None:
423 logging.debug('Adding date updating timer')
424 self._update_dates_timer = \
425 gobject.timeout_add(UPDATE_INTERVAL,
426 self.__update_dates_timer_cb)
427 else:
428 self._fully_obscured = True
429 if self._update_dates_timer is not None:
430 logging.debug('Remove date updating timer')
431 gobject.source_remove(self._update_dates_timer)
432 self._update_dates_timer = None
434 def __update_dates_timer_cb(self):
435 self.update_dates()
436 return True
438 class ListView(BaseListView):
439 __gtype_name__ = 'ListView'
441 __gsignals__ = {
442 'detail-clicked': (gobject.SIGNAL_RUN_FIRST,
443 gobject.TYPE_NONE,
444 ([object]))
447 def __init__(self):
448 BaseListView.__init__(self)
450 def create_entry(self):
451 entry = CollapsedEntry()
452 entry.connect('detail-clicked', self.__entry_activated_cb)
453 return entry
455 def __entry_activated_cb(self, entry):
456 self.emit('detail-clicked', entry)