Adapt to sugar API changes. This makes the colors ugly,
[journal-activity.git] / listview.py
blob468fab6ff8c1f55326677c80d59097a349235922
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
16 import os
17 import logging
19 import hippo
20 import gobject
21 import gtk
23 from sugar.activity import activity
24 from sugar.datastore import datastore
25 from sugar.graphics import style
26 from sugar.graphics import units
27 from sugar.graphics.canvasicon import CanvasIcon
29 from collapsedentry import CollapsedEntry
30 from expandedentry import ExpandedEntry
32 class ListView(gtk.HBox):
33 __gtype_name__ = 'ListView'
35 __gproperties__ = {
36 'expanded' : (bool, None, None, False,
37 gobject.PARAM_READWRITE)
40 __gsignals__ = {
41 'entry-activated': (gobject.SIGNAL_RUN_FIRST,
42 gobject.TYPE_NONE,
43 ([object]))
46 _PAGE_SIZE = 10
47 _MAX_CACHE_PAGES = 5
48 _INITIAL_CACHE_PAGES = 3
50 def __init__(self):
51 self._expanded = False
52 self._query = {}
53 self._entries = []
54 self._cache = []
55 self._offset = 0
57 gtk.HBox.__init__(self)
58 self.set_flags(gtk.HAS_FOCUS|gtk.CAN_FOCUS)
59 self.connect('key-press-event', self._key_press_event_cb)
61 self._box = hippo.CanvasBox()
62 self._box.props.orientation = hippo.ORIENTATION_VERTICAL
63 self._box.props.background_color = style.COLOR_PANEL_GREY.get_int()
64 self._box.props.spacing = units.points_to_pixels(5)
65 self._box.props.padding = units.points_to_pixels(5)
67 canvas = hippo.Canvas()
68 canvas.set_root(self._box)
70 self.pack_start(canvas)
71 canvas.show()
73 self._vadjustment = gtk.Adjustment(value=0,
74 lower=0,
75 upper=0,
76 step_incr=1,
77 page_incr=ListView._PAGE_SIZE,
78 page_size=ListView._PAGE_SIZE)
79 self._vadjustment.connect('value-changed', self._vadjustment_value_changed_cb)
80 self._vadjustment.connect('changed', self._vadjustment_changed_cb)
82 self._vscrollbar = gtk.VScrollbar(self._vadjustment)
83 self.pack_end(self._vscrollbar, expand=False, fill=False)
84 self._vscrollbar.show()
86 self.connect('scroll-event', self._scroll_event_cb)
88 # DND stuff
89 self._pressed_button = None
90 self._press_start_x = None
91 self._press_start_y = None
92 self._last_clicked_entry = None
93 canvas.drag_source_set(0, [], 0)
94 canvas.add_events(gtk.gdk.BUTTON_PRESS_MASK |
95 gtk.gdk.POINTER_MOTION_HINT_MASK)
96 canvas.connect("motion_notify_event",
97 self._canvas_motion_notify_event_cb)
98 canvas.connect("button_press_event",
99 self._canvas_button_press_event_cb)
100 canvas.connect("drag_end", self._drag_end_cb)
101 canvas.connect("drag_data_get", self._drag_data_get_cb)
103 def _vadjustment_changed_cb(self, vadjustment):
104 logging.debug('_vadjustment_changed_cb:\n \t%r\n \t%r\n \t%r\n \t%r\n \t%r\n' % \
105 (vadjustment.props.lower, vadjustment.props.page_increment,
106 vadjustment.props.page_size, vadjustment.props.step_increment,
107 vadjustment.props.upper))
108 if (vadjustment.props.upper - vadjustment.props.lower) > vadjustment.props.page_size:
109 self._vscrollbar.show()
110 else:
111 self._vscrollbar.hide()
113 def _vadjustment_value_changed_cb(self, vadjustment):
114 import time
115 t = time.time()
116 value = int(vadjustment.props.value)
117 logging.debug('value: %i offset: %i cache: %i' %
118 (value, self._offset, len(self._cache)))
120 if value < self._offset:
121 remaining_forward_entries = 0
122 else:
123 remaining_forward_entries = self._offset + len(self._cache) - value
125 if value > self._offset + len(self._cache):
126 remaining_backwards_entries = 0
127 else:
128 remaining_backwards_entries = value - self._offset
130 total_count = int(vadjustment.props.upper)
131 last_cached_entry = self._offset + len(self._cache)
133 if remaining_forward_entries < 1 and remaining_backwards_entries < 1:
134 # Total cache miss: remake it
135 offset = max(0, value - ListView._INITIAL_CACHE_PAGES * \
136 ListView._PAGE_SIZE / 2)
137 logging.debug('remaking cache, offset: %r' % offset)
138 jobjects, total_count = datastore.find(self._query,
139 sorting=['-mtime'],
140 offset=offset,
141 limit=ListView._INITIAL_CACHE_PAGES * ListView._PAGE_SIZE)
142 self._vadjustment.props.upper = total_count - 1
143 self._vadjustment.changed()
145 self._cache = jobjects
146 self._offset = offset
148 elif remaining_forward_entries < 20 and last_cached_entry < total_count:
149 # Add one page to the end of cache
150 logging.debug('appending one more page, offset: %r' % last_cached_entry)
151 jobjects, total_count = datastore.find(self._query,
152 sorting=['-mtime'],
153 offset=last_cached_entry,
154 limit=ListView._PAGE_SIZE)
155 self._vadjustment.props.upper = total_count - 1
156 self._vadjustment.changed()
158 self._cache.extend(jobjects)
160 if len(self._cache) > ListView._MAX_CACHE_PAGES * ListView._PAGE_SIZE:
161 self._offset += ListView._PAGE_SIZE
162 self._cache = self._cache[ListView._PAGE_SIZE:]
164 elif remaining_backwards_entries < 20 and self._offset >= ListView._PAGE_SIZE:
165 # Add one page to the beginning of cache
166 logging.debug('prepending one more page, offset: %r' %
167 (self._offset - ListView._PAGE_SIZE))
168 jobjects, total_count = datastore.find(self._query,
169 sorting=['-mtime'],
170 offset=self._offset - ListView._PAGE_SIZE,
171 limit=ListView._PAGE_SIZE)
172 self._vadjustment.props.upper = total_count - 1
173 self._vadjustment.changed()
175 jobjects.extend(self._cache)
176 self._cache = jobjects
177 self._offset -= ListView._PAGE_SIZE
179 max_cache_size = ListView._MAX_CACHE_PAGES * ListView._PAGE_SIZE
180 if len(self._cache) > max_cache_size:
181 self._cache = self._cache[:max_cache_size]
183 self._refresh_view(value)
185 logging.debug('_vadjustment_value_changed_cb %r\n' % (time.time() - t))
187 def _refresh_view(self, position):
188 # Refresh view and create the entries if they don't exist yet.
189 for i in range(0, ListView._PAGE_SIZE):
190 if (position - self._offset + i) in range(0, len(self._cache)):
191 if i >= len(self._entries):
192 entry = CollapsedEntry(self._cache[position - self._offset + i])
193 entry.connect('entry-activated', self._entry_activated_cb)
194 self._box.append(entry)
195 self._entries.append(entry)
196 else:
197 entry = self._entries[i]
198 entry.jobject = self._cache[position - self._offset + i]
199 entry.set_visible(True)
200 else:
201 if i < len(self._entries):
202 entry = self._entries[i]
203 entry.set_visible(False)
205 def update_with_query(self, query):
206 self._query = query
207 self.refresh()
209 def refresh(self):
210 self._offset = 0
211 self._cache, total_count = datastore.find(self._query,
212 sorting=['-mtime'],
213 limit=3 * ListView._PAGE_SIZE)
214 self._vadjustment.props.upper = total_count - 1
215 self._vadjustment.changed()
216 self._vadjustment.props.value = 0
217 self._vadjustment.value_changed()
219 def do_set_property(self, pspec, value):
220 if pspec.name == 'expanded':
221 if self._expanded != value:
222 self._expanded = value
223 #self._update()
225 def do_get_property(self, pspec):
226 if pspec.name == 'expanded':
227 return self._expanded
229 def _entry_activated_cb(self, entry_view):
230 self.emit('entry-activated', entry_view)
232 def _scroll_event_cb(self, hbox, event):
233 if event.direction == gtk.gdk.SCROLL_UP:
234 if self._vadjustment.props.value > self._vadjustment.props.lower:
235 self._vadjustment.props.value -= 1
236 self._vadjustment.value_changed()
237 elif event.direction == gtk.gdk.SCROLL_DOWN:
238 if self._vadjustment.props.value < \
239 (self._vadjustment.props.upper - self._vadjustment.props.page_size):
240 self._vadjustment.props.value += 1
241 self._vadjustment.value_changed()
243 def do_focus(self, direction):
244 if not self.is_focus():
245 self.grab_focus()
246 return True
247 return False
249 def _key_press_event_cb(self, widget, event):
250 keyname = gtk.gdk.keyval_name(event.keyval)
251 if keyname == 'Up':
252 if self._vadjustment.props.value > self._vadjustment.props.lower:
253 self._vadjustment.props.value -= 1
254 self._vadjustment.value_changed()
255 return True
256 else:
257 return False
258 elif keyname == 'Down':
259 if self._vadjustment.props.value < \
260 (self._vadjustment.props.upper - self._vadjustment.props.page_size):
261 self._vadjustment.props.value += 1
262 self._vadjustment.value_changed()
263 return True
264 else:
265 return False
266 elif keyname == 'Page_Up':
267 new_position = max(0, self._vadjustment.props.value - ListView._PAGE_SIZE)
268 if new_position != self._vadjustment.props.value:
269 self._vadjustment.props.value = new_position
270 self._vadjustment.value_changed()
271 return True
272 else:
273 return False
274 elif keyname == 'Page_Down':
275 new_position = min(self._vadjustment.props.upper - ListView._PAGE_SIZE + 1,
276 self._vadjustment.props.value + ListView._PAGE_SIZE)
277 if new_position != self._vadjustment.props.value:
278 self._vadjustment.props.value = new_position
279 self._vadjustment.value_changed()
280 return True
281 else:
282 return False
283 else:
284 return False
286 # TODO: Dnd methods. This should be merged somehow inside hippo-canvas.
287 def _canvas_motion_notify_event_cb(self, widget, event):
288 if not self._pressed_button:
289 return True
291 # if the mouse button is not pressed, no drag should occurr
292 if not event.state & gtk.gdk.BUTTON1_MASK:
293 self._pressed_button = None
294 return True
296 logging.debug("motion_notify_event_cb")
298 if event.is_hint:
299 x, y, state = event.window.get_pointer()
300 else:
301 x = event.x
302 y = event.y
303 state = event.state
305 if widget.drag_check_threshold(int(self._press_start_x),
306 int(self._press_start_y),
307 int(x),
308 int(y)):
309 context = widget.drag_begin([('text/uri-list', 0, 0),
310 ('journal-object-id', 0, 0)],
311 gtk.gdk.ACTION_COPY,
313 event);
315 return True
317 def _drag_end_cb(self, widget, drag_context):
318 logging.debug("drag_end_cb")
319 self._pressed_button = None
320 self._press_start_x = None
321 self._press_start_y = None
322 self._last_clicked_entry = None
324 def _drag_data_get_cb(self, widget, context, selection, targetType, eventTime):
325 logging.debug("drag_data_get_cb: requested target " + selection.target)
327 jobject = self._last_clicked_entry.jobject
328 if selection.target == 'text/uri-list':
329 selection.set(selection.target, 8, jobject.file_path)
330 elif selection.target == 'journal-object-id':
331 selection.set(selection.target, 8, jobject.object_id)
333 def _canvas_button_press_event_cb(self, widget, event):
334 logging.debug("button_press_event_cb")
336 if event.button == 1 and event.type == gtk.gdk.BUTTON_PRESS:
337 self._last_clicked_entry = self._get_entry_at_coords(event.x, event.y)
338 if self._last_clicked_entry:
339 self._pressed_button = event.button
340 self._press_start_x = event.x
341 self._press_start_y = event.y
343 return False
345 def _get_entry_at_coords(self, x, y):
346 for entry in self._box.get_children():
347 frame = entry.get_children()[1]
348 frame_x, frame_y = frame.get_context().translate_to_widget(frame)
349 frame_width, frame_height = frame.get_allocation()
351 if (x >= frame_x ) and (x <= frame_x + frame_width) and \
352 (y >= frame_y ) and (y <= frame_y + frame_height):
353 return entry
354 return None