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
20 from gettext
import gettext
as _
27 from sugar
.graphics
import style
28 from sugar
.graphics
.icon
import CanvasIcon
30 from collapsedentry
import CollapsedEntry
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'
47 self
._result
_set
= None
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
)
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
)
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
)
95 self
._fully
_obscured
= True
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()
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()
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):
137 value
= int(self
._vadjustment
.props
.value
)
139 if value
== self
._last
_value
and not force
:
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
)
153 logging
.debug('_do_scroll %r %r\n' % (value
, (time
.time() - t
)))
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
)
164 # Refresh view and create the entries if they don't exist yet.
165 for i
in range(0, self
._page
_size
):
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
]
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)
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')
192 if self
._page
_size
> 0:
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
)
210 self
._show
_message
(EMPTY_JOURNAL
)
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():
230 def _key_press_event_cb(self
, widget
, event
):
231 keyname
= gtk
.gdk
.keyval_name(event
.keyval
)
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':
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
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
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
):
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
291 self
._do
_scroll
(force
=True)
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())
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
:
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
325 logging
.debug("motion_notify_event_cb")
328 x
, y
, state_
= event
.window
.get_pointer()
333 if widget
.drag_check_threshold(int(self
._press
_start
_x
),
334 int(self
._press
_start
_y
),
337 context_
= widget
.drag_begin([('text/uri-list', 0, 0),
338 ('journal-object-id', 0, 0)],
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
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
):
382 def update_dates(self
):
383 logging
.debug('ListView.update_dates')
384 for entry
in self
._entries
:
387 def __datastore_created_cb(self
, uid
):
390 def __datastore_updated_cb(self
, uid
):
393 def __datastore_deleted_cb(self
, uid
):
396 def _set_dirty(self
):
397 if self
._fully
_obscured
:
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
):
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
416 def set_is_visible(self
, visible
):
417 logging
.debug('canvas_visibility_notify_event_cb %r' % visible
)
419 self
._fully
_obscured
= False
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
)
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
):
438 class ListView(BaseListView
):
439 __gtype_name__
= 'ListView'
442 'detail-clicked': (gobject
.SIGNAL_RUN_FIRST
,
448 BaseListView
.__init
__(self
)
450 def create_entry(self
):
451 entry
= CollapsedEntry()
452 entry
.connect('detail-clicked', self
.__entry
_activated
_cb
)
455 def __entry_activated_cb(self
, entry
):
456 self
.emit('detail-clicked', entry
)