Merge lp:~gnome-zeitgeist/gnome-activity-journal/new-core into lp:gnome-activity-journal
- new-core
- Merge into trunk
Proposed by
Randal Barlow
Status: | Merged |
---|---|
Merge reported by: | Randal Barlow |
Merged at revision: | not available |
Proposed branch: | lp:~gnome-zeitgeist/gnome-activity-journal/new-core |
Merge into: | lp:gnome-activity-journal |
Diff against target: |
8485 lines (+2480/-4200) (has conflicts) 12 files modified
src/activityviews.py (+1159/-0) src/common.py (+0/-582) src/daywidgets.py (+0/-791) src/eventgatherer.py (+0/-224) src/histogram.py (+0/-625) src/infopane.py (+0/-504) src/main.py (+180/-181) src/store.py (+288/-0) src/supporting_widgets.py (+853/-0) src/thumb.py (+0/-348) src/view.py (+0/-273) src/widgets.py (+0/-672) Contents conflict in src/timeline.py |
To merge this branch: | bzr merge lp:~gnome-zeitgeist/gnome-activity-journal/new-core |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Randal Barlow | Pending | ||
Review via email: mp+24652@code.launchpad.net |
Commit message
Description of the change
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === added file 'data/multiview_icon.png' |
2 | Binary files data/multiview_icon.png 1970-01-01 00:00:00 +0000 and data/multiview_icon.png 2010-05-04 05:28:16 +0000 differ |
3 | === added file 'data/thumbview_icon.png' |
4 | Binary files data/thumbview_icon.png 1970-01-01 00:00:00 +0000 and data/thumbview_icon.png 2010-05-04 05:28:16 +0000 differ |
5 | === added file 'data/timelineview_icon.png' |
6 | Binary files data/timelineview_icon.png 1970-01-01 00:00:00 +0000 and data/timelineview_icon.png 2010-05-04 05:28:16 +0000 differ |
7 | === added file 'src/activityviews.py' |
8 | --- src/activityviews.py 1970-01-01 00:00:00 +0000 |
9 | +++ src/activityviews.py 2010-05-04 05:28:16 +0000 |
10 | @@ -0,0 +1,1159 @@ |
11 | +# -.- coding: utf-8 -.- |
12 | +# |
13 | +# GNOME Activity Journal |
14 | +# |
15 | +# Copyright © 2009-2010 Seif Lotfy <seif@lotfy.com> |
16 | +# Copyright © 2010 Randal Barlow <email.tehk@gmail.com> |
17 | +# Copyright © 2010 Siegfried Gevatter <siegfried@gevatter.com> |
18 | +# Copyright © 2010 Markus Korn <thekorn@gmx.de> |
19 | +# |
20 | +# This program is free software: you can redistribute it and/or modify |
21 | +# it under the terms of the GNU General Public License as published by |
22 | +# the Free Software Foundation, either version 3 of the License, or |
23 | +# (at your option) any later version. |
24 | +# |
25 | +# This program is distributed in the hope that it will be useful, |
26 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
27 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
28 | +# GNU General Public License for more details. |
29 | +# |
30 | +# You should have received a copy of the GNU General Public License |
31 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
32 | + |
33 | +import datetime |
34 | +import gobject |
35 | +import gst |
36 | +import gtk |
37 | +import math |
38 | +import pango |
39 | +import threading |
40 | + |
41 | +from bookmarker import bookmarker |
42 | +from common import * |
43 | +import content_objects |
44 | +from config import settings |
45 | +from sources import SUPPORTED_SOURCES |
46 | +from store import ContentStruct, CLIENT |
47 | +from supporting_widgets import DayLabel, ContextMenu, StaticPreviewTooltip, VideoPreviewTooltip, Pane |
48 | + |
49 | +from zeitgeist.datamodel import ResultType, StorageState, TimeRange |
50 | + |
51 | + |
52 | +class _GenericViewWidget(gtk.VBox): |
53 | + day = None |
54 | + |
55 | + def __init__(self): |
56 | + gtk.VBox.__init__(self) |
57 | + self.daylabel = DayLabel() |
58 | + self.pack_start(self.daylabel, False, False) |
59 | + self.connect("style-set", self.change_style) |
60 | + |
61 | + def set_day(self, day, store): |
62 | + self.store = store |
63 | + if self.day: |
64 | + self.day.disconnect(self.day_signal_id) |
65 | + self.day = day |
66 | + self.day_signal_id = self.day.connect("update", self.update_day) |
67 | + self.update_day(day) |
68 | + |
69 | + def update_day(self, day): |
70 | + self.daylabel.set_date(day.date) |
71 | + self.view.set_day(self.day) |
72 | + |
73 | + def click(self, widget, event): |
74 | + if event.button in (1, 3): |
75 | + self.emit("unfocus-day") |
76 | + |
77 | + def change_style(self, widget, style): |
78 | + rc_style = self.style |
79 | + color = rc_style.bg[gtk.STATE_NORMAL] |
80 | + color = shade_gdk_color(color, 102/100.0) |
81 | + self.view.modify_bg(gtk.STATE_NORMAL, color) |
82 | + self.view.modify_base(gtk.STATE_NORMAL, color) |
83 | + |
84 | + |
85 | +##################### |
86 | +## MultiView code |
87 | +## This doesnt work, No idea why |
88 | +##################### |
89 | + |
90 | +class MultiViewContainer(gtk.HBox): |
91 | + |
92 | + days = [] |
93 | + num_pages = 3 |
94 | + day_signal_id = [None] * num_pages |
95 | + |
96 | + def __init__(self): |
97 | + super(MultiViewContainer, self).__init__() |
98 | + self.pages = [] |
99 | + for i in range(self.num_pages): |
100 | + group = DayViewContainer() |
101 | + evbox = gtk.EventBox() |
102 | + evbox.add(group) |
103 | + self.pages.append(group) |
104 | + padding = 6 if i != self.num_pages-1 and i != 0 else 0 |
105 | + self.pack_start(evbox, True, True, padding) |
106 | + self.connect("style-set", self.change_style) |
107 | + |
108 | + def set_day(self, day, store): |
109 | + if self.days: |
110 | + for i, _day in enumerate(self.__days(self.days[0], store)): |
111 | + signal = self.day_signal_id[i] |
112 | + if signal: |
113 | + _day.disconnect(signal) |
114 | + self.days = self.__days(day, store) |
115 | + for i, day in enumerate(self.days): |
116 | + self.day_signal_id[i] = day.connect("update", self.update_day) |
117 | + self.update_day() |
118 | + |
119 | + def __days(self, day, store): |
120 | + days = [] |
121 | + for i in range(self.num_pages): |
122 | + days += [day] |
123 | + day = day.previous(store) |
124 | + return days |
125 | + |
126 | + def update_day(self, *args): |
127 | + #print "UPDATED", i, dayobj |
128 | + #if i != None and dayobj: |
129 | + # print "DO IT" |
130 | + # return self.pages[i].set_day(dayobj) |
131 | + for page, day in map(None, reversed(self.pages), self.days): |
132 | + page.set_day(day) |
133 | + |
134 | + def change_style(self, this, old_style): |
135 | + style = this.style |
136 | + for widget in self: |
137 | + color = style.bg[gtk.STATE_NORMAL] |
138 | + bgcolor = shade_gdk_color(color, 102/100.0) |
139 | + widget.modify_bg(gtk.STATE_NORMAL, bgcolor) |
140 | + |
141 | + |
142 | + |
143 | +class DayViewContainer(gtk.VBox): |
144 | + event_templates = ( |
145 | + Event.new_for_values(interpretation=Interpretation.VISIT_EVENT.uri), |
146 | + Event.new_for_values(interpretation=Interpretation.MODIFY_EVENT.uri), |
147 | + Event.new_for_values(interpretation=Interpretation.CREATE_EVENT.uri), |
148 | + Event.new_for_values(interpretation=Interpretation.OPEN_EVENT.uri), |
149 | + ) |
150 | + # Do day label stuff here please |
151 | + def __init__(self): |
152 | + super(DayViewContainer, self).__init__() |
153 | + self.daylabel = DayLabel() |
154 | + self.pack_start(self.daylabel, False, False) |
155 | + self.dayviews = (DayView(_("Morning")), DayView(_("Afternoon")), DayView(_("Evening"))) |
156 | + self.scrolled_window = gtk.ScrolledWindow() |
157 | + self.scrolled_window.set_shadow_type(gtk.SHADOW_NONE) |
158 | + viewport = gtk.Viewport() |
159 | + viewport.set_shadow_type(gtk.SHADOW_NONE) |
160 | + box = gtk.VBox() |
161 | + for dayview in self.dayviews: |
162 | + box.pack_start(dayview, False, False) |
163 | + viewport.add(box) |
164 | + self.scrolled_window.add(viewport) |
165 | + |
166 | + self.pack_end(self.scrolled_window, True, True) |
167 | + self.scrolled_window.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) |
168 | + self.show_all() |
169 | + |
170 | + def set_day(self, day): |
171 | + self.daylabel.set_date(day.date) |
172 | + morning = [] |
173 | + afternoon = [] |
174 | + evening = [] |
175 | + for item in day.filter(self.event_templates, result_type=ResultType.MostRecentSubjects): |
176 | + if not item.content_object:continue |
177 | + t = time.localtime(int(item.event.timestamp)/1000) |
178 | + if t.tm_hour < 11: |
179 | + morning.append(item) |
180 | + elif t.tm_hour < 17: |
181 | + afternoon.append(item) |
182 | + else: |
183 | + evening.append(item) |
184 | + self.dayviews[0].set_items(morning) |
185 | + self.dayviews[1].set_items(afternoon) |
186 | + self.dayviews[2].set_items(evening) |
187 | + |
188 | + |
189 | +class DayView(gtk.VBox): |
190 | + |
191 | + def __init__(self, title=""): |
192 | + super(DayView, self).__init__() |
193 | + #self.add(gtk.Button("LOL")) |
194 | + # Create the title label |
195 | + self.label = gtk.Label(title) |
196 | + self.label.set_alignment(0.03, 0.5) |
197 | + self.pack_start(self.label, False, False, 6) |
198 | + # Create the main container |
199 | + self.view = None |
200 | + |
201 | + # Connect to relevant signals |
202 | + self.connect("style-set", self.on_style_change) |
203 | + self.show_all() |
204 | + # Populate the widget with content |
205 | + |
206 | + def on_style_change(self, widget, style): |
207 | + """ Update used colors according to the system theme. """ |
208 | + color = self.style.bg[gtk.STATE_NORMAL] |
209 | + fcolor = self.style.fg[gtk.STATE_NORMAL] |
210 | + color = combine_gdk_color(color, fcolor) |
211 | + self.label.modify_fg(gtk.STATE_NORMAL, color) |
212 | + |
213 | + def clear(self): |
214 | + if self.view: |
215 | + self.remove(self.view) |
216 | + self.view.destroy() |
217 | + del self.view |
218 | + self.view = gtk.VBox() |
219 | + self.pack_start(self.view) |
220 | + |
221 | + def set_items(self, items): |
222 | + self.clear() |
223 | + categories = {} |
224 | + for struct in items: |
225 | + if not struct.content_object: continue |
226 | + subject = struct.event.subjects[0] |
227 | + if not categories.has_key(subject.interpretation): |
228 | + categories[subject.interpretation] = [] |
229 | + categories[subject.interpretation].append(struct) |
230 | + if not categories: |
231 | + self.hide_all() |
232 | + else: |
233 | + ungrouped_events = [] |
234 | + for key in sorted(categories.iterkeys()): |
235 | + events = categories[key] |
236 | + if len(events) > 3: |
237 | + box = CategoryBox(key, list(reversed(events))) |
238 | + self.view.pack_start(box) |
239 | + else: |
240 | + ungrouped_events += events |
241 | + box = CategoryBox(None, ungrouped_events) |
242 | + self.view.pack_start(box) |
243 | + self.show_all() |
244 | + |
245 | + |
246 | +class CategoryBox(gtk.HBox): |
247 | + |
248 | + def __init__(self, category, event_structs, pinnable = False): |
249 | + super(CategoryBox, self).__init__() |
250 | + self.view = gtk.VBox(True) |
251 | + self.vbox = gtk.VBox() |
252 | + for struct in event_structs: |
253 | + if not struct.content_object:continue |
254 | + item = Item(struct, pinnable) |
255 | + hbox = gtk.HBox () |
256 | + #label = gtk.Label("") |
257 | + #hbox.pack_start(label, False, False, 7) |
258 | + hbox.pack_start(item, True, True, 0) |
259 | + self.view.pack_start(hbox, False, False, 0) |
260 | + hbox.show_all() |
261 | + #label.show() |
262 | + self.pack_end(hbox) |
263 | + |
264 | + # If this isn't a set of ungrouped events, give it a label |
265 | + if category: |
266 | + # Place the items into a box and simulate left padding |
267 | + self.box = gtk.HBox() |
268 | + #label = gtk.Label("") |
269 | + self.box.pack_start(self.view) |
270 | + |
271 | + hbox = gtk.HBox() |
272 | + # Add the title button |
273 | + if category in SUPPORTED_SOURCES: |
274 | + text = SUPPORTED_SOURCES[category].group_label(len(event_structs)) |
275 | + else: |
276 | + text = "Unknown" |
277 | + |
278 | + label = gtk.Label() |
279 | + label.set_markup("<span>%s</span>" % text) |
280 | + #label.set_ellipsize(pango.ELLIPSIZE_END) |
281 | + |
282 | + hbox.pack_start(label, True, True, 0) |
283 | + |
284 | + label = gtk.Label() |
285 | + label.set_markup("<span>(%d)</span>" % len(event_structs)) |
286 | + label.set_alignment(1.0,0.5) |
287 | + label.set_alignment(1.0,0.5) |
288 | + hbox.pack_end(label, False, False, 2) |
289 | + |
290 | + hbox.set_border_width(3) |
291 | + |
292 | + self.expander = gtk.Expander() |
293 | + self.expander.set_label_widget(hbox) |
294 | + |
295 | + self.vbox.pack_start(self.expander, False, False) |
296 | + self.expander.add(self.box)# |
297 | + |
298 | + self.pack_start(self.vbox, True, True, 24) |
299 | + |
300 | + self.expander.show_all() |
301 | + self.show() |
302 | + hbox.show_all() |
303 | + label.show_all() |
304 | + self.view.show() |
305 | + |
306 | + else: |
307 | + self.box = self.view |
308 | + self.vbox.pack_end(self.box) |
309 | + self.box.show() |
310 | + self.show() |
311 | + |
312 | + self.pack_start(self.vbox, True, True, 16) |
313 | + |
314 | + self.show_all() |
315 | + |
316 | + def on_toggle(self, view, bool): |
317 | + if bool: |
318 | + self.box.show() |
319 | + else: |
320 | + self.box.hide() |
321 | + pinbox.show_all() |
322 | + |
323 | + |
324 | +class Item(gtk.HBox): |
325 | + |
326 | + def __init__(self, content_struct, allow_pin = False): |
327 | + event = content_struct.event |
328 | + gtk.HBox.__init__(self) |
329 | + self.set_border_width(2) |
330 | + self.allow_pin = allow_pin |
331 | + self.btn = gtk.Button() |
332 | + self.search_results = [] |
333 | + self.in_search = False |
334 | + self.subject = event.subjects[0] |
335 | + self.content_obj = content_struct.content_object |
336 | + # self.content_obj = GioFile.create(self.subject.uri) |
337 | + self.time = float(event.timestamp) / 1000 |
338 | + self.time = time.strftime("%H:%M", time.localtime(self.time)) |
339 | + |
340 | + if self.content_obj is not None: |
341 | + self.icon = self.content_obj.get_icon( |
342 | + can_thumb=settings.get('small_thumbnails', False), border=0) |
343 | + else: |
344 | + self.icon = None |
345 | + self.btn.set_relief(gtk.RELIEF_NONE) |
346 | + self.btn.set_focus_on_click(False) |
347 | + self.__init_widget() |
348 | + self.show_all() |
349 | + self.markup = None |
350 | + |
351 | + def __init_widget(self): |
352 | + self.label = gtk.Label() |
353 | + text = self.content_obj.text.replace("&", "&") |
354 | + self.label.set_markup(text) |
355 | + self.label.set_ellipsize(pango.ELLIPSIZE_MIDDLE) |
356 | + self.label.set_alignment(0.0, 0.5) |
357 | + |
358 | + if self.icon: img = gtk.image_new_from_pixbuf(self.icon) |
359 | + else: img = None |
360 | + hbox = gtk.HBox() |
361 | + if img: hbox.pack_start(img, False, False, 1) |
362 | + hbox.pack_start(self.label, True, True, 4) |
363 | + |
364 | + if self.allow_pin: |
365 | + # TODO: get the name "pin" from theme when icons are properly installed |
366 | + img = gtk.image_new_from_file(get_icon_path("hicolor/24x24/status/pin.png")) |
367 | + self.pin = gtk.Button() |
368 | + self.pin.add(img) |
369 | + self.pin.set_tooltip_text(_("Remove Pin")) |
370 | + self.pin.set_focus_on_click(False) |
371 | + self.pin.set_relief(gtk.RELIEF_NONE) |
372 | + self.pack_end(self.pin, False, False) |
373 | + self.pin.connect("clicked", lambda x: self.set_bookmarked(False)) |
374 | + #hbox.pack_end(img, False, False) |
375 | + evbox = gtk.EventBox() |
376 | + self.btn.add(hbox) |
377 | + evbox.add(self.btn) |
378 | + self.pack_start(evbox) |
379 | + |
380 | + self.btn.connect("clicked", self.launch) |
381 | + self.btn.connect("button_press_event", self._show_item_popup) |
382 | + |
383 | + def realize_cb(widget): |
384 | + evbox.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.HAND2)) |
385 | + |
386 | + self.btn.connect("realize", realize_cb) |
387 | + |
388 | + self.init_multimedia_tooltip() |
389 | + |
390 | + def init_multimedia_tooltip(self): |
391 | + """add multimedia tooltip to multimedia files |
392 | + multimedia tooltip is shown for all images, all videos and pdfs |
393 | + |
394 | + TODO: make loading of multimedia thumbs async |
395 | + """ |
396 | + if isinstance(self.content_obj, GioFile) and self.content_obj.has_preview(): |
397 | + icon_names = self.content_obj.icon_names |
398 | + self.set_property("has-tooltip", True) |
399 | + self.connect("query-tooltip", self._handle_tooltip) |
400 | + if "video-x-generic" in icon_names and gst is not None: |
401 | + self.set_tooltip_window(VideoPreviewTooltip) |
402 | + else: |
403 | + self.set_tooltip_window(StaticPreviewTooltip) |
404 | + |
405 | + def _handle_tooltip(self, widget, x, y, keyboard_mode, tooltip): |
406 | + # nothing to do here, we always show the multimedia tooltip |
407 | + # if we like video/sound preview later on we can start them here |
408 | + tooltip_window = self.get_tooltip_window() |
409 | + return tooltip_window.preview(self.content_obj) |
410 | + |
411 | + def _show_item_popup(self, widget, ev): |
412 | + if ev.button == 3: |
413 | + items = [self.content_obj] |
414 | + ContextMenu.do_popup(ev.time, items) |
415 | + |
416 | + def set_bookmarked(self, bool_): |
417 | + uri = unicode(self.subject.uri) |
418 | + if bool_: |
419 | + bookmarker.bookmark(uri) |
420 | + else: |
421 | + bookmarker.unbookmark(uri) |
422 | + |
423 | + |
424 | + def launch(self, *discard): |
425 | + if self.content_obj is not None: |
426 | + self.content_obj.launch() |
427 | + |
428 | + |
429 | +##################### |
430 | +## ThumbView code |
431 | +##################### |
432 | +class ThumbViewContainer(_GenericViewWidget): |
433 | + day_signal_id = None |
434 | + |
435 | + def __init__(self): |
436 | + _GenericViewWidget.__init__(self) |
437 | + self.scrolledwindow = gtk.ScrolledWindow() |
438 | + self.scrolledwindow.set_shadow_type(gtk.SHADOW_NONE) |
439 | + self.scrolledwindow.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) |
440 | + self.view = ThumbView() |
441 | + self.scrolledwindow.add_with_viewport(self.view) |
442 | + self.scrolledwindow.get_children()[0].set_shadow_type(gtk.SHADOW_NONE) |
443 | + self.pack_end(self.scrolledwindow) |
444 | + self.show_all() |
445 | + |
446 | + |
447 | +class _ThumbViewRenderer(gtk.GenericCellRenderer): |
448 | + """ |
449 | + A IconView renderer to be added to a cellayout. It displays a pixbuf and |
450 | + data based on the event property |
451 | + """ |
452 | + |
453 | + __gtype_name__ = "_ThumbViewRenderer" |
454 | + __gproperties__ = { |
455 | + "content_obj" : |
456 | + (gobject.TYPE_PYOBJECT, |
457 | + "event to be displayed", |
458 | + "event to be displayed", |
459 | + gobject.PARAM_READWRITE, |
460 | + ), |
461 | + } |
462 | + |
463 | + width = 96 |
464 | + height = 72 |
465 | + properties = {} |
466 | + |
467 | + @property |
468 | + def content_obj(self): |
469 | + return self.get_property("content_obj") |
470 | + |
471 | + @property |
472 | + def emblems(self): |
473 | + return self.content_obj.emblems |
474 | + |
475 | + @property |
476 | + def pixbuf(self): |
477 | + return self.content_obj.thumbview_pixbuf |
478 | + |
479 | + @property |
480 | + def event(self): |
481 | + return self.content_obj.event |
482 | + |
483 | + def __init__(self): |
484 | + super(_ThumbViewRenderer, self).__init__() |
485 | + self.properties = {} |
486 | + self.set_fixed_size(self.width, self.height) |
487 | + self.set_property("mode", gtk.CELL_RENDERER_MODE_ACTIVATABLE) |
488 | + |
489 | + def do_set_property(self, pspec, value): |
490 | + self.properties[pspec.name] = value |
491 | + |
492 | + def do_get_property(self, pspec): |
493 | + return self.properties[pspec.name] |
494 | + |
495 | + def on_get_size(self, widget, area): |
496 | + if area: |
497 | + #return (area.x, area.y, area.width, area.height) |
498 | + return (0, 0, area.width, area.height) |
499 | + return (0,0,0,0) |
500 | + |
501 | + def on_render(self, window, widget, background_area, cell_area, expose_area, flags): |
502 | + """ |
503 | + The primary rendering function. It calls either the classes rendering functions |
504 | + or special one defined in the rendering_functions dict |
505 | + """ |
506 | + x = cell_area.x |
507 | + y = cell_area.y |
508 | + w = cell_area.width |
509 | + h = cell_area.height |
510 | + pixbuf_w = self.pixbuf.get_width() if self.pixbuf else 0 |
511 | + pixbuf_h = self.pixbuf.get_height() if self.pixbuf else 0 |
512 | + if (pixbuf_w, pixbuf_h) == content_objects.SIZE_THUMBVIEW: |
513 | + render_pixbuf(window, x, y, self.pixbuf) |
514 | + else: |
515 | + self.file_render_pixbuf(window, widget, x, y, w, h) |
516 | + render_emblems(window, x, y, w, h, self.emblems) |
517 | + path = widget.get_path_at_pos(cell_area.x, cell_area.y) |
518 | + if path != None: |
519 | + try: |
520 | + if widget.active_list[path[0]]: |
521 | + gobject.timeout_add(2, self.render_info_box, window, widget, cell_area, expose_area, self.event) |
522 | + except:pass |
523 | + return True |
524 | + |
525 | + @staticmethod |
526 | + def insert_file_markup(text): |
527 | + text = text.replace("&", "&") |
528 | + text = "<span size='6400'>" + text + "</span>" |
529 | + return text |
530 | + |
531 | + def file_render_pixbuf(self, window, widget, x, y, w, h): |
532 | + """ |
533 | + Renders a icon and file name for non-thumb objects |
534 | + """ |
535 | + context = window.cairo_create() |
536 | + pixbuf = self.pixbuf |
537 | + if pixbuf: |
538 | + imgw, imgh = pixbuf.get_width(), pixbuf.get_height() |
539 | + ix = x + (self.width - imgw) |
540 | + iy = y + self.height - imgh |
541 | + context.rectangle(x, y, w, h) |
542 | + context.set_source_rgb(1, 1, 1) |
543 | + context.fill_preserve() |
544 | + if pixbuf: |
545 | + context.set_source_pixbuf(pixbuf, ix, iy) |
546 | + context.fill() |
547 | + draw_frame(context, x, y, w, h) |
548 | + context = window.cairo_create() |
549 | + text = self.insert_file_markup(self.content_obj.thumbview_text) |
550 | + |
551 | + layout = widget.create_pango_layout(text) |
552 | + draw_text(context, layout, text, x+5, y+5, self.width-10) |
553 | + |
554 | + @staticmethod |
555 | + def render_info_box(window, widget, cell_area, expose_area, event): |
556 | + """ |
557 | + Renders a info box when the item is active |
558 | + """ |
559 | + x = cell_area.x |
560 | + y = cell_area.y - 10 |
561 | + w = cell_area.width |
562 | + h = cell_area.height |
563 | + context = window.cairo_create() |
564 | + t0 = get_event_typename(event) |
565 | + t1 = event.subjects[0].text |
566 | + text = ("<span size='10240'>%s</span>\n<span size='8192'>%s</span>" % (t0, t1)).replace("&", "&") |
567 | + layout = widget.create_pango_layout(text) |
568 | + layout.set_markup(text) |
569 | + textw, texth = layout.get_pixel_size() |
570 | + popuph = max(h/3 + 5, texth) |
571 | + nw = w + 26 |
572 | + x = x - (nw - w)/2 |
573 | + width, height = window.get_geometry()[2:4] |
574 | + popupy = min(y+h+10, height-popuph-5-1) - 5 |
575 | + draw_speech_bubble(context, layout, x, popupy, nw, popuph) |
576 | + context.fill() |
577 | + return False |
578 | + |
579 | + def on_start_editing(self, event, widget, path, background_area, cell_area, flags): |
580 | + pass |
581 | + |
582 | + def on_activate(self, event, widget, path, background_area, cell_area, flags): |
583 | + self.content_obj.launch() |
584 | + return True |
585 | + |
586 | + |
587 | +class ThumbIconView(gtk.IconView): |
588 | + """ |
589 | + A iconview which uses a custom cellrenderer to render square pixbufs |
590 | + based on zeitgeist events |
591 | + """ |
592 | + last_active = -1 |
593 | + child_width = _ThumbViewRenderer.width |
594 | + child_height = _ThumbViewRenderer.height |
595 | + def __init__(self): |
596 | + super(ThumbIconView, self).__init__() |
597 | + self.active_list = [] |
598 | + self.popupmenu = ContextMenu |
599 | + self.add_events(gtk.gdk.LEAVE_NOTIFY_MASK) |
600 | + self.connect("button-press-event", self.on_button_press) |
601 | + self.connect("motion-notify-event", self.on_motion_notify) |
602 | + self.connect("leave-notify-event", self.on_leave_notify) |
603 | + self.set_selection_mode(gtk.SELECTION_NONE) |
604 | + self.set_column_spacing(6) |
605 | + self.set_row_spacing(6) |
606 | + pcolumn = gtk.TreeViewColumn("Preview") |
607 | + render = _ThumbViewRenderer() |
608 | + self.pack_end(render) |
609 | + self.add_attribute(render, "content_obj", 0) |
610 | + self.set_margin(10) |
611 | + |
612 | + def _set_model_in_thread(self, items): |
613 | + """ |
614 | + A threaded which generates pixbufs and emblems for a list of events. |
615 | + It takes those properties and appends them to the view's model |
616 | + """ |
617 | + lock = threading.Lock() |
618 | + self.active_list = [] |
619 | + liststore = gtk.ListStore(gobject.TYPE_PYOBJECT) |
620 | + gtk.gdk.threads_enter() |
621 | + self.set_model(liststore) |
622 | + gtk.gdk.threads_leave() |
623 | + |
624 | + for item in items: |
625 | + obj = item.content_object |
626 | + if not obj: continue |
627 | + gtk.gdk.threads_enter() |
628 | + lock.acquire() |
629 | + self.active_list.append(False) |
630 | + liststore.append((obj,)) |
631 | + lock.release() |
632 | + gtk.gdk.threads_leave() |
633 | + |
634 | + def set_model_from_list(self, items): |
635 | + """ |
636 | + Sets creates/sets a model from a list of zeitgeist events |
637 | + :param events: a list of :class:`Events <zeitgeist.datamodel.Event>` |
638 | + """ |
639 | + self.last_active = -1 |
640 | + if not items: |
641 | + self.set_model(None) |
642 | + return |
643 | + thread = threading.Thread(target=self._set_model_in_thread, args=(items,)) |
644 | + thread.start() |
645 | + |
646 | + def on_button_press(self, widget, event): |
647 | + if event.button == 3: |
648 | + val = self.get_item_at_pos(int(event.x), int(event.y)) |
649 | + if val: |
650 | + path, cell = val |
651 | + model = self.get_model() |
652 | + obj = model[path[0]][0] |
653 | + self.popupmenu.do_popup(event.time, [obj]) |
654 | + return False |
655 | + |
656 | + def on_leave_notify(self, widget, event): |
657 | + try: |
658 | + self.active_list[self.last_active] = False |
659 | + except IndexError:pass |
660 | + self.last_active = -1 |
661 | + self.queue_draw() |
662 | + |
663 | + def on_motion_notify(self, widget, event): |
664 | + val = self.get_item_at_pos(int(event.x), int(event.y)) |
665 | + if val: |
666 | + path, cell = val |
667 | + if path[0] != self.last_active: |
668 | + self.active_list[self.last_active] = False |
669 | + self.active_list[path[0]] = True |
670 | + self.last_active = path[0] |
671 | + self.queue_draw() |
672 | + return True |
673 | + |
674 | + def query_tooltip(self, widget, x, y, keyboard_mode, tooltip): |
675 | + """ |
676 | + Displays a tooltip based on x, y |
677 | + """ |
678 | + path = self.get_path_at_pos(int(x), int(y)) |
679 | + if path: |
680 | + model = self.get_model() |
681 | + uri = model[path[0]][3].uri |
682 | + interpretation = model[path[0]][3].subjects[0].interpretation |
683 | + tooltip_window = widget.get_tooltip_window() |
684 | + if interpretation == Interpretation.VIDEO.uri: |
685 | + self.set_tooltip_window(VideoPreviewTooltip) |
686 | + else: |
687 | + self.set_tooltip_window(StaticPreviewTooltip) |
688 | + gio_file = GioFile.create(uri) |
689 | + return tooltip_window.preview(gio_file) |
690 | + return False |
691 | + |
692 | + |
693 | +class ThumbView(gtk.VBox): |
694 | + """ |
695 | + A container for three image views representing periods in time |
696 | + """ |
697 | + event_templates = ( |
698 | + Event.new_for_values(interpretation=Interpretation.VISIT_EVENT.uri), |
699 | + Event.new_for_values(interpretation=Interpretation.MODIFY_EVENT.uri), |
700 | + Event.new_for_values(interpretation=Interpretation.CREATE_EVENT.uri), |
701 | + Event.new_for_values(interpretation=Interpretation.OPEN_EVENT.uri), |
702 | + ) |
703 | + def __init__(self): |
704 | + """Woo""" |
705 | + gtk.VBox.__init__(self) |
706 | + self.views = [ThumbIconView() for x in xrange(3)] |
707 | + self.labels = [gtk.Label() for x in xrange(3)] |
708 | + for i in xrange(3): |
709 | + text = TIMELABELS[i] |
710 | + line = 50 - len(text) |
711 | + self.labels[i].set_markup( |
712 | + "\n <span size='10336'>%s <s>%s</s></span>" % (text, " "*line)) |
713 | + self.labels[i].set_justify(gtk.JUSTIFY_RIGHT) |
714 | + self.labels[i].set_alignment(0, 0) |
715 | + self.pack_start(self.labels[i], False, False) |
716 | + self.pack_start(self.views[i], False, False) |
717 | + self.connect("style-set", self.change_style) |
718 | + |
719 | + def set_phase_items(self, i, items): |
720 | + """ |
721 | + Set a time phases events |
722 | + |
723 | + :param i: a index for the three items in self.views. 0:Morning,1:AfterNoon,2:Evening |
724 | + :param events: a list of :class:`Events <zeitgeist.datamodel.Event>` |
725 | + """ |
726 | + view = self.views[i] |
727 | + label = self.labels[i] |
728 | + if not items or len(items) == 0: |
729 | + view.set_model_from_list(None) |
730 | + return False |
731 | + view.show_all() |
732 | + label.show_all() |
733 | + view.set_model_from_list(items) |
734 | + |
735 | + if len(items) == 0: |
736 | + view.hide_all() |
737 | + label.hide_all() |
738 | + |
739 | + def set_day(self, day): |
740 | + morning = [] |
741 | + afternoon = [] |
742 | + evening = [] |
743 | + for item in day.filter(self.event_templates, result_type=ResultType.MostRecentSubjects): |
744 | + #if not item.content_object:continue |
745 | + t = time.localtime(int(item.event.timestamp)/1000) |
746 | + if t.tm_hour < 11: |
747 | + morning.append(item) |
748 | + elif t.tm_hour < 17: |
749 | + afternoon.append(item) |
750 | + else: |
751 | + evening.append(item) |
752 | + self.set_phase_items(0, morning) |
753 | + self.set_phase_items(1, afternoon) |
754 | + self.set_phase_items(2, evening) |
755 | + |
756 | + def change_style(self, widget, style): |
757 | + rc_style = self.style |
758 | + parent = self.get_parent() |
759 | + if parent: |
760 | + parent = self.get_parent() |
761 | + color = rc_style.bg[gtk.STATE_NORMAL] |
762 | + parent.modify_bg(gtk.STATE_NORMAL, color) |
763 | + for view in self.views: view.modify_base(gtk.STATE_NORMAL, color) |
764 | + color = rc_style.text[4] |
765 | + color = shade_gdk_color(color, 0.95) |
766 | + for label in self.labels: |
767 | + label.modify_fg(0, color) |
768 | + |
769 | +################ |
770 | +## TimelineView |
771 | +################ |
772 | +class TimelineViewContainer(_GenericViewWidget): |
773 | + |
774 | + def __init__(self): |
775 | + _GenericViewWidget.__init__(self) |
776 | + self.ruler = _TimelineHeader() |
777 | + self.scrolledwindow = gtk.ScrolledWindow() |
778 | + self.scrolledwindow.set_shadow_type(gtk.SHADOW_NONE) |
779 | + self.scrolledwindow.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_ALWAYS) |
780 | + self.view = TimelineView() |
781 | + self.scrolledwindow.add(self.view) |
782 | + self.pack_end(self.scrolledwindow) |
783 | + self.pack_end(self.ruler, False, False) |
784 | + self.view.set_events(gtk.gdk.POINTER_MOTION_MASK | gtk.gdk.POINTER_MOTION_HINT_MASK) |
785 | + |
786 | + def change_style(self, widget, style): |
787 | + _GenericViewWidget.change_style(self, widget, style) |
788 | + rc_style = self.style |
789 | + color = rc_style.bg[gtk.STATE_NORMAL] |
790 | + color = shade_gdk_color(color, 102/100.0) |
791 | + self.ruler.modify_bg(gtk.STATE_NORMAL, color) |
792 | + |
793 | + |
794 | + |
795 | + |
796 | +class _TimelineRenderer(gtk.GenericCellRenderer): |
797 | + """ |
798 | + Renders timeline columns, and text for a for properties |
799 | + """ |
800 | + |
801 | + __gtype_name__ = "TimelineRenderer" |
802 | + __gproperties__ = { |
803 | + "content_obj" : |
804 | + (gobject.TYPE_PYOBJECT, |
805 | + "event to be displayed", |
806 | + "event to be displayed", |
807 | + gobject.PARAM_READWRITE, |
808 | + ), |
809 | + } |
810 | + |
811 | + width = 32 |
812 | + height = 48 |
813 | + barsize = 5 |
814 | + properties = {} |
815 | + |
816 | + textcolor = {gtk.STATE_NORMAL : ("#ff", "#ff"), |
817 | + gtk.STATE_SELECTED : ("#ff", "#ff")} |
818 | + |
819 | + @property |
820 | + def content_obj(self): |
821 | + return self.get_property("content_obj") |
822 | + |
823 | + @property |
824 | + def phases(self): |
825 | + return self.content_obj.phases |
826 | + |
827 | + @property |
828 | + def event(self): |
829 | + return self.content_obj.event |
830 | + |
831 | + @property |
832 | + def colors(self): |
833 | + """ A tuple of two colors, the first being the base the outer being the outline""" |
834 | + return self.content_obj.type_color_representation |
835 | + |
836 | + @property |
837 | + def text(self): |
838 | + return self.content_obj.timelineview_text |
839 | + |
840 | + @property |
841 | + def pixbuf(self): |
842 | + return self.content_obj.timelineview_pixbuf |
843 | + |
844 | + def __init__(self): |
845 | + super(_TimelineRenderer, self).__init__() |
846 | + self.properties = {} |
847 | + self.set_fixed_size(self.width, self.height) |
848 | + self.set_property("mode", gtk.CELL_RENDERER_MODE_ACTIVATABLE) |
849 | + |
850 | + def do_set_property(self, pspec, value): |
851 | + self.properties[pspec.name] = value |
852 | + |
853 | + def do_get_property(self, pspec): |
854 | + return self.properties[pspec.name] |
855 | + |
856 | + def on_get_size(self, widget, area): |
857 | + if area: |
858 | + return (0, 0, area.width, area.height) |
859 | + return (0,0,0,0) |
860 | + |
861 | + def on_render(self, window, widget, background_area, cell_area, expose_area, flags): |
862 | + """ |
863 | + The primary rendering function. It calls either the classes rendering functions |
864 | + or special one defined in the rendering_functions dict |
865 | + """ |
866 | + x = int(cell_area.x) |
867 | + y = int(cell_area.y) |
868 | + w = int(cell_area.width) |
869 | + h = int(cell_area.height) |
870 | + self.render_phases(window, widget, x, y, w, h, flags) |
871 | + return True |
872 | + |
873 | + def render_phases(self, window, widget, x, y, w, h, flags): |
874 | + context = window.cairo_create() |
875 | + phases = self.phases |
876 | + for start, end in phases: |
877 | + context.set_source_rgb(*self.colors[0]) |
878 | + start = int(start * w) |
879 | + end = max(int(end * w), 8) |
880 | + if start + 8 > w: |
881 | + start = w - 8 |
882 | + context.rectangle(x+ start, y, end, self.barsize) |
883 | + context.fill() |
884 | + context.set_source_rgb(*self.colors[1]) |
885 | + context.set_line_width(1) |
886 | + context.rectangle(x + start+0.5, y+0.5, end, self.barsize) |
887 | + context.stroke() |
888 | + x = int(phases[0][0]*w) |
889 | + # Pixbuf related junk which is really dirty |
890 | + self.render_text_with_pixbuf(window, widget, x, y, w, h, flags) |
891 | + return True |
892 | + |
893 | + def render_text_with_pixbuf(self, window, widget, x, y, w, h, flags): |
894 | + uri = self.content_obj.uri |
895 | + imgw, imgh = self.pixbuf.get_width(), self.pixbuf.get_height() |
896 | + x = max(x + imgw/2 + 4, 0 + imgw + 4) |
897 | + x, y = self.render_text(window, widget, x, y, w, h, flags) |
898 | + x -= imgw + 4 |
899 | + y += self.barsize + 3 |
900 | + pixbuf_w = self.pixbuf.get_width() if self.pixbuf else 0 |
901 | + pixbuf_h = self.pixbuf.get_height() if self.pixbuf else 0 |
902 | + if (pixbuf_w, pixbuf_h) == content_objects.SIZE_TIMELINEVIEW: |
903 | + drawframe = True |
904 | + else: drawframe = False |
905 | + render_pixbuf(window, x, y, self.pixbuf, drawframe=drawframe) |
906 | + |
907 | + def render_text(self, window, widget, x, y, w, h, flags): |
908 | + w = window.get_geometry()[2] |
909 | + y+= 2 |
910 | + x += 5 |
911 | + state = gtk.STATE_SELECTED if gtk.CELL_RENDERER_SELECTED & flags else gtk.STATE_NORMAL |
912 | + color1, color2 = self.textcolor[state] |
913 | + text = self._make_timelineview_text(self.text) |
914 | + text = text % (color1.to_string(), color2.to_string()) |
915 | + layout = widget.create_pango_layout("") |
916 | + layout.set_markup(text) |
917 | + textw, texth = layout.get_pixel_size() |
918 | + if textw + x > w: |
919 | + layout.set_ellipsize(pango.ELLIPSIZE_MIDDLE) |
920 | + layout.set_width(200*1024) |
921 | + textw, texth = layout.get_pixel_size() |
922 | + if x + textw > w: |
923 | + x = w - textw |
924 | + context = window.cairo_create() |
925 | + pcontext = pangocairo.CairoContext(context) |
926 | + pcontext.set_source_rgb(0, 0, 0) |
927 | + pcontext.move_to(x, y + self.barsize) |
928 | + pcontext.show_layout(layout) |
929 | + return x, y |
930 | + |
931 | + @staticmethod |
932 | + def _make_timelineview_text(text): |
933 | + """ |
934 | + :returns: a string of text markup used in timeline widget and elsewhere |
935 | + """ |
936 | + text = text.split("\n") |
937 | + if len(text) > 1: |
938 | + p1 = text[0] |
939 | + p2 = text[1] |
940 | + else: |
941 | + p1 = text[0] |
942 | + p2 = " " |
943 | + t1 = "<span color='%s'><b>" + p1 + "</b></span>" |
944 | + t2 = "<span color='%s'>" + p2 + "</span> " |
945 | + return (str(t1) + "\n" + str(t2) + "").replace("&", "&") |
946 | + |
947 | + def on_start_editing(self, event, widget, path, background_area, cell_area, flags): |
948 | + pass |
949 | + |
950 | + def on_activate(self, event, widget, path, background_area, cell_area, flags): |
951 | + pass |
952 | + |
953 | + |
954 | +class TimelineView(gtk.TreeView): |
955 | + @staticmethod |
956 | + def make_area_from_event(timestamp, duration): |
957 | + """ |
958 | + Generates a time box based on a objects timestamp and duration over 1. |
959 | + Multiply the results by the width to get usable positions |
960 | + |
961 | + :param timestamp: a timestamp int or string from which to calulate the start position |
962 | + :param duration: the length to calulate the width |
963 | + """ |
964 | + w = max(duration/3600.0/1000.0/24.0, 0) |
965 | + x = ((int(timestamp)/1000.0 - time.timezone)%86400)/3600/24.0 |
966 | + return [x, w] |
967 | + |
968 | + child_width = _TimelineRenderer.width |
969 | + child_height = _TimelineRenderer.height |
970 | + |
971 | + def __init__(self): |
972 | + super(TimelineView, self).__init__() |
973 | + self.popupmenu = ContextMenu |
974 | + self.add_events(gtk.gdk.LEAVE_NOTIFY_MASK) |
975 | + self.connect("button-press-event", self.on_button_press) |
976 | + # self.connect("motion-notify-event", self.on_motion_notify) |
977 | + # self.connect("leave-notify-event", self.on_leave_notify) |
978 | + self.connect("row-activated" , self.on_activate) |
979 | + self.connect("style-set", self.change_style) |
980 | + pcolumn = gtk.TreeViewColumn("Timeline") |
981 | + self.render = render = _TimelineRenderer() |
982 | + pcolumn.pack_start(render) |
983 | + self.append_column(pcolumn) |
984 | + pcolumn.add_attribute(render, "content_obj", 0) |
985 | + self.set_headers_visible(False) |
986 | + self.set_property("has-tooltip", True) |
987 | + self.set_tooltip_window(StaticPreviewTooltip) |
988 | + |
989 | + |
990 | + def set_model_from_list(self, items): |
991 | + """ |
992 | + Sets creates/sets a model from a list of zeitgeist events |
993 | + |
994 | + :param events: a list of :class:`Events <zeitgeist.datamodel.Event>` |
995 | + """ |
996 | + if not items: |
997 | + self.set_model(None) |
998 | + return |
999 | + liststore = gtk.ListStore(gobject.TYPE_PYOBJECT) |
1000 | + for row in items: |
1001 | + item = row[0][0] |
1002 | + obj = item.content_object |
1003 | + if not obj: continue |
1004 | + obj.phases = [self.make_area_from_event(item.event.timestamp, stop) for (item, stop) in row] |
1005 | + obj.phases.sort(key=lambda x: x[0]) |
1006 | + liststore.append((obj,)) |
1007 | + self.set_model(liststore) |
1008 | + |
1009 | + def set_day(self, day): |
1010 | + items = day.get_time_map() |
1011 | + self.set_model_from_list(items) |
1012 | + |
1013 | + def on_button_press(self, widget, event): |
1014 | + if event.button == 3: |
1015 | + path = self.get_dest_row_at_pos(int(event.x), int(event.y)) |
1016 | + if path: |
1017 | + model = self.get_model() |
1018 | + obj = model[path[0]][0] |
1019 | + self.popupmenu.do_popup(event.time, [obj]) |
1020 | + return True |
1021 | + return False |
1022 | + |
1023 | + def on_leave_notify(self, widget, event): |
1024 | + return True |
1025 | + |
1026 | + def on_motion_notify(self, widget, event): |
1027 | + return True |
1028 | + |
1029 | + def on_activate(self, widget, path, column): |
1030 | + model = self.get_model() |
1031 | + model[path][0].launch() |
1032 | + |
1033 | + def change_style(self, widget, old_style): |
1034 | + """ |
1035 | + Sets the widgets style and coloring |
1036 | + """ |
1037 | + layout = self.create_pango_layout("") |
1038 | + layout.set_markup("<b>qPqPqP|</b>\nqPqPqP|") |
1039 | + tw, th = layout.get_pixel_size() |
1040 | + self.render.height = max(_TimelineRenderer.height, th + 3 + _TimelineRenderer.barsize) |
1041 | + if self.window: |
1042 | + width = self.window.get_geometry()[2] - 4 |
1043 | + self.render.width = max(_TimelineRenderer.width, width) |
1044 | + self.render.set_fixed_size(self.render.width, self.render.height) |
1045 | + def change_color(color, inc): |
1046 | + color = shade_gdk_color(color, inc/100.0) |
1047 | + return color |
1048 | + normal = (self.style.text[gtk.STATE_NORMAL], change_color(self.style.text[gtk.STATE_INSENSITIVE], 70)) |
1049 | + selected = (self.style.text[gtk.STATE_SELECTED], self.style.text[gtk.STATE_SELECTED]) |
1050 | + self.render.textcolor[gtk.STATE_NORMAL] = normal |
1051 | + self.render.textcolor[gtk.STATE_SELECTED] = selected |
1052 | + |
1053 | + |
1054 | +class _TimelineHeader(gtk.DrawingArea): |
1055 | + time_text = {4:"4:00", 8:"8:00", 12:"12:00", 16:"16:00", 20:"20:00"} |
1056 | + odd_line_height = 6 |
1057 | + even_line_height = 12 |
1058 | + |
1059 | + line_color = (0, 0, 0, 1) |
1060 | + def __init__(self): |
1061 | + super(_TimelineHeader, self).__init__() |
1062 | + self.connect("expose-event", self.expose) |
1063 | + self.connect("style-set", self.change_style) |
1064 | + self.set_size_request(100, 12) |
1065 | + |
1066 | + def expose(self, widget, event): |
1067 | + window = widget.window |
1068 | + context = widget.window.cairo_create() |
1069 | + layout = self.create_pango_layout(" ") |
1070 | + width = event.area.width |
1071 | + widget.style.set_background(window, gtk.STATE_NORMAL) |
1072 | + context.set_source_rgba(*self.line_color) |
1073 | + context.set_line_width(2) |
1074 | + self.draw_lines(window, context, layout, width) |
1075 | + |
1076 | + def draw_text(self, window, context, layout, x, text): |
1077 | + x = int(x) |
1078 | + color = self.style.text[gtk.STATE_NORMAL] |
1079 | + markup = "<span color='%s'>%s</span>" % (color.to_string(), text) |
1080 | + pcontext = pangocairo.CairoContext(context) |
1081 | + layout.set_markup(markup) |
1082 | + xs, ys = layout.get_pixel_size() |
1083 | + pcontext.move_to(x - xs/2, 0) |
1084 | + pcontext.show_layout(layout) |
1085 | + |
1086 | + def draw_line(self, window, context, x, even): |
1087 | + x = int(x)+0.5 |
1088 | + height = self.even_line_height if even else self.odd_line_height |
1089 | + context.move_to(x, 0) |
1090 | + context.line_to(x, height) |
1091 | + context.stroke() |
1092 | + |
1093 | + def draw_lines(self, window, context, layout, width): |
1094 | + xinc = width/24 |
1095 | + for hour in xrange(1, 24): |
1096 | + if self.time_text.has_key(hour): |
1097 | + self.draw_text(window, context, layout, xinc*hour, self.time_text[hour]) |
1098 | + else: |
1099 | + self.draw_line(window, context, xinc*hour, bool(hour % 2)) |
1100 | + |
1101 | + def change_style(self, widget, old_style): |
1102 | + layout = self.create_pango_layout("") |
1103 | + layout.set_markup("<b>qPqPqP|</b>") |
1104 | + tw, th = layout.get_pixel_size() |
1105 | + self.set_size_request(tw*5, th+4) |
1106 | + self.line_color = get_gtk_rgba(widget.style, "bg", 0, 0.94) |
1107 | + |
1108 | +## |
1109 | +# Pinned Pane |
1110 | + |
1111 | +class PinBox(DayView): |
1112 | + |
1113 | + def __init__(self): |
1114 | + # Setup event criteria for querying |
1115 | + self.event_timerange = TimeRange.until_now() |
1116 | + # Initialize the widget |
1117 | + super(PinBox, self).__init__(_("Pinned items")) |
1118 | + # Connect to relevant signals |
1119 | + bookmarker.connect("reload", self.set_from_templates) |
1120 | + self.set_from_templates() |
1121 | + |
1122 | + @property |
1123 | + def event_templates(self): |
1124 | + if not bookmarker.bookmarks: |
1125 | + # Abort, or we will query with no templates and get lots of |
1126 | + # irrelevant events. |
1127 | + return None |
1128 | + |
1129 | + templates = [] |
1130 | + for bookmark in bookmarker.bookmarks: |
1131 | + templates.append(Event.new_for_values(subject_uri=bookmark)) |
1132 | + return templates |
1133 | + |
1134 | + def set_from_templates(self, *args, **kwargs): |
1135 | + if bookmarker.bookmarks: |
1136 | + CLIENT.find_event_ids_for_templates(self.event_templates, self.do_set, |
1137 | + self.event_timerange, |
1138 | + StorageState.Any, 10000, ResultType.MostRecentSubjects) |
1139 | + |
1140 | + def do_set(self, event_ids): |
1141 | + objs = [] |
1142 | + for id_ in event_ids: |
1143 | + objs += [ContentStruct(id_)] |
1144 | + self.set_items(objs) |
1145 | + # Make the pin icons visible |
1146 | + self.view.show_all() |
1147 | + self.show_all() |
1148 | + |
1149 | + def set_items(self, items): |
1150 | + self.clear() |
1151 | + box = CategoryBox(None, items, True) |
1152 | + self.view.pack_start(box) |
1153 | + |
1154 | + |
1155 | +class PinnedPane(Pane): |
1156 | + def __init__(self): |
1157 | + super(PinnedPane, self).__init__() |
1158 | + vbox = gtk.VBox() |
1159 | + self.pinbox = PinBox() |
1160 | + vbox.pack_start(self.pinbox, False, False) |
1161 | + self.add(vbox) |
1162 | + self.set_size_request(200, -1) |
1163 | + self.set_label_align(1,0) |
1164 | + |
1165 | + |
1166 | +## gobject registration |
1167 | +gobject.type_register(_TimelineRenderer) |
1168 | +gobject.type_register(_ThumbViewRenderer) |
1169 | + |
1170 | |
1171 | === added file 'src/common.py' |
1172 | --- src/common.py 1970-01-01 00:00:00 +0000 |
1173 | +++ src/common.py 2010-05-04 05:28:16 +0000 |
1174 | @@ -0,0 +1,582 @@ |
1175 | +# -.- coding: utf-8 -.- |
1176 | +# |
1177 | +# Filename |
1178 | +# |
1179 | +# Copyright © 2010 Randal Barlow <email.tehk@gmail.com> |
1180 | +# Copyright © 2010 Siegfried Gevatter <siegfried@gevatter.com> |
1181 | +# |
1182 | +# This program is free software: you can redistribute it and/or modify |
1183 | +# it under the terms of the GNU General Public License as published by |
1184 | +# the Free Software Foundation, either version 3 of the License, or |
1185 | +# (at your option) any later version. |
1186 | +# |
1187 | +# This program is distributed in the hope that it will be useful, |
1188 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1189 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1190 | +# GNU General Public License for more details. |
1191 | +# |
1192 | +# You should have received a copy of the GNU General Public License |
1193 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
1194 | + |
1195 | +""" |
1196 | +Common Functions and classes which are used to create the alternative views, |
1197 | +and handle colors, pixbufs, and text |
1198 | +""" |
1199 | + |
1200 | +import cairo |
1201 | +import gobject |
1202 | +import gtk |
1203 | +import os |
1204 | +import pango |
1205 | +import pangocairo |
1206 | +import time |
1207 | +import math |
1208 | +import operator |
1209 | +import subprocess |
1210 | + |
1211 | +from gio_file import GioFile, SIZE_LARGE, SIZE_NORMAL, SIZE_THUMBVIEW, SIZE_TIMELINEVIEW, ICONS |
1212 | +from config import get_data_path, get_icon_path |
1213 | + |
1214 | +from zeitgeist.datamodel import Interpretation, Event |
1215 | + |
1216 | + |
1217 | +# Caches desktop files |
1218 | +DESKTOP_FILES = {} |
1219 | +DESKTOP_FILE_PATHS = [] |
1220 | +try: |
1221 | + desktop_file_paths = os.environ["XDG_DATA_DIRS"].split(":") |
1222 | + for path in desktop_file_paths: |
1223 | + if path.endswith("/"): |
1224 | + DESKTOP_FILE_PATHS.append(path + "applications/") |
1225 | + else: |
1226 | + DESKTOP_FILE_PATHS.append(path + "/applications/") |
1227 | +except KeyError:pass |
1228 | + |
1229 | +# Placeholder pixbufs for common sizes |
1230 | +PLACEHOLDER_PIXBUFFS = { |
1231 | + 24 : gtk.gdk.pixbuf_new_from_file_at_size(get_icon_path("hicolor/scalable/apps/gnome-activity-journal.svg"), 24, 24), |
1232 | + 16 : gtk.gdk.pixbuf_new_from_file_at_size(get_icon_path("hicolor/scalable/apps/gnome-activity-journal.svg"), 16, 16) |
1233 | + } |
1234 | + |
1235 | +# Color magic |
1236 | +TANGOCOLORS = [ |
1237 | + (252/255.0, 234/255.0, 79/255.0),#0 |
1238 | + (237/255.0, 212/255.0, 0/255.0), |
1239 | + (196/255.0, 160/255.0, 0/255.0), |
1240 | + |
1241 | + (252/255.0, 175/255.0, 62/255.0),#3 |
1242 | + (245/255.0, 121/255.0, 0/255.0), |
1243 | + (206/255.0, 92/255.0, 0/255.0), |
1244 | + |
1245 | + (233/255.0, 185/255.0, 110/255.0),#6 |
1246 | + (193/255.0, 125/255.0, 17/255.0), |
1247 | + (143/255.0, 89/255.0, 02/255.0), |
1248 | + |
1249 | + (138/255.0, 226/255.0, 52/255.0),#9 |
1250 | + (115/255.0, 210/255.0, 22/255.0), |
1251 | + ( 78/255.0, 154/255.0, 06/255.0), |
1252 | + |
1253 | + (114/255.0, 159/255.0, 207/255.0),#12 |
1254 | + ( 52/255.0, 101/255.0, 164/255.0), |
1255 | + ( 32/255.0, 74/255.0, 135/255.0), |
1256 | + |
1257 | + (173/255.0, 127/255.0, 168/255.0),#15 |
1258 | + (117/255.0, 80/255.0, 123/255.0), |
1259 | + ( 92/255.0, 53/255.0, 102/255.0), |
1260 | + |
1261 | + (239/255.0, 41/255.0, 41/255.0),#18 |
1262 | + (204/255.0, 0/255.0, 0/255.0), |
1263 | + (164/255.0, 0/255.0, 0/255.0), |
1264 | + |
1265 | + (136/255.0, 138/255.0, 133/255.0),#21 |
1266 | + ( 85/255.0, 87/255.0, 83/255.0), |
1267 | + ( 46/255.0, 52/255.0, 54/255.0), |
1268 | +] |
1269 | + |
1270 | +FILETYPES = { |
1271 | + Interpretation.VIDEO.uri : 0, |
1272 | + Interpretation.MUSIC.uri : 3, |
1273 | + Interpretation.DOCUMENT.uri : 12, |
1274 | + Interpretation.IMAGE.uri : 15, |
1275 | + Interpretation.SOURCECODE.uri : 12, |
1276 | + Interpretation.UNKNOWN.uri : 21, |
1277 | + Interpretation.IM_MESSAGE.uri : 21, |
1278 | + Interpretation.EMAIL.uri : 21 |
1279 | +} |
1280 | + |
1281 | +FILETYPESNAMES = { |
1282 | + Interpretation.VIDEO.uri : _("Video"), |
1283 | + Interpretation.MUSIC.uri : _("Music"), |
1284 | + Interpretation.DOCUMENT.uri : _("Document"), |
1285 | + Interpretation.IMAGE.uri : _("Image"), |
1286 | + Interpretation.SOURCECODE.uri : _("Source Code"), |
1287 | + Interpretation.UNKNOWN.uri : _("Unknown"), |
1288 | + Interpretation.IM_MESSAGE.uri : _("IM Message"), |
1289 | + Interpretation.EMAIL.uri :_("Email"), |
1290 | + |
1291 | +} |
1292 | + |
1293 | +MEDIAINTERPRETATIONS = [ |
1294 | + Interpretation.VIDEO.uri, |
1295 | + Interpretation.IMAGE.uri, |
1296 | +] |
1297 | + |
1298 | +TIMELABELS = [_("Morning"), _("Afternoon"), _("Evening")] |
1299 | +ICON_THEME = gtk.icon_theme_get_default() |
1300 | + |
1301 | +def get_file_color(ftype, fmime): |
1302 | + """Uses hashing to choose a shade from a hue in the color tuple above |
1303 | + |
1304 | + :param ftype: a :class:`Event <zeitgeist.datamodel.Interpretation>` |
1305 | + :param fmime: a mime type string |
1306 | + """ |
1307 | + if ftype in FILETYPES.keys(): |
1308 | + i = FILETYPES[ftype] |
1309 | + l = int(math.fabs(hash(fmime))) % 3 |
1310 | + return TANGOCOLORS[min(i+l, len(TANGOCOLORS)-1)] |
1311 | + return (136/255.0, 138/255.0, 133/255.0) |
1312 | + |
1313 | +## |
1314 | +## Zeitgeist event helper functions |
1315 | + |
1316 | +def get_event_typename(event): |
1317 | + """ |
1318 | + :param event: a :class:`Event <zeitgeist.datamodel.Event>` |
1319 | + |
1320 | + :returns: a plain text version of a interpretation |
1321 | + """ |
1322 | + try: |
1323 | + return Interpretation[event.subjects[0].interpretation].display_name |
1324 | + except KeyError: |
1325 | + pass |
1326 | + return FILETYPESNAMES[event.subjects[0].interpretation] |
1327 | + |
1328 | +## |
1329 | +# Cairo drawing functions |
1330 | + |
1331 | +def draw_frame(context, x, y, w, h): |
1332 | + """ |
1333 | + Draws a 2 pixel frame around a area defined by x, y, w, h using a cairo context |
1334 | + |
1335 | + :param context: a cairo context |
1336 | + :param x: x position of the frame |
1337 | + :param y: y position of the frame |
1338 | + :param w: width of the frame |
1339 | + :param h: height of the frame |
1340 | + """ |
1341 | + x, y = int(x)+0.5, int(y)+0.5 |
1342 | + w, h = int(w), int(h) |
1343 | + context.set_line_width(1) |
1344 | + context.rectangle(x-1, y-1, w+2, h+2) |
1345 | + context.set_source_rgba(0.5, 0.5, 0.5)#0.3, 0.3, 0.3) |
1346 | + context.stroke() |
1347 | + context.set_source_rgba(0.7, 0.7, 0.7) |
1348 | + context.rectangle(x, y, w, h) |
1349 | + context.stroke() |
1350 | + context.set_source_rgba(0.4, 0.4, 0.4) |
1351 | + context.rectangle(x+1, y+1, w-2, h-2) |
1352 | + context.stroke() |
1353 | + |
1354 | +def draw_rounded_rectangle(context, x, y, w, h, r=5): |
1355 | + """Draws a rounded rectangle |
1356 | + |
1357 | + :param context: a cairo context |
1358 | + :param x: x position of the rectangle |
1359 | + :param y: y position of the rectangle |
1360 | + :param w: width of the rectangle |
1361 | + :param h: height of the rectangle |
1362 | + :param r: radius of the rectangle |
1363 | + """ |
1364 | + context.new_sub_path() |
1365 | + context.arc(r+x, r+y, r, math.pi, 3 * math.pi /2) |
1366 | + context.arc(w-r+x, r+y, r, 3 * math.pi / 2, 0) |
1367 | + context.arc(w-r+x, h-r+y, r, 0, math.pi/2) |
1368 | + context.arc(r+x, h-r+y, r, math.pi/2, math.pi) |
1369 | + context.close_path() |
1370 | + return context |
1371 | + |
1372 | +def draw_speech_bubble(context, layout, x, y, w, h): |
1373 | + """ |
1374 | + Draw a speech bubble at a position |
1375 | + |
1376 | + Arguments: |
1377 | + :param context: a cairo context |
1378 | + :param layout: a pango layout |
1379 | + :param x: x position of the bubble |
1380 | + :param y: y position of the bubble |
1381 | + :param w: width of the bubble |
1382 | + :param h: height of the bubble |
1383 | + """ |
1384 | + layout.set_width((w-10)*1024) |
1385 | + layout.set_ellipsize(pango.ELLIPSIZE_MIDDLE) |
1386 | + textw, texth = layout.get_pixel_size() |
1387 | + context.new_path() |
1388 | + context.move_to(x + 0.45*w, y+h*0.1 + 2) |
1389 | + context.line_to(x + 0.5*w, y) |
1390 | + context.line_to(x + 0.55*w, y+h*0.1 + 2) |
1391 | + h = max(texth + 5, h) |
1392 | + draw_rounded_rectangle(context, x, y+h*0.1, w, h, r = 5) |
1393 | + context.close_path() |
1394 | + context.set_line_width(2) |
1395 | + context.set_source_rgb(168/255.0, 165/255.0, 134/255.0) |
1396 | + context.stroke_preserve() |
1397 | + context.set_source_rgb(253/255.0, 248/255.0, 202/255.0) |
1398 | + context.fill() |
1399 | + pcontext = pangocairo.CairoContext(context) |
1400 | + pcontext.set_source_rgb(0, 0, 0) |
1401 | + pcontext.move_to(x+5, y+5) |
1402 | + pcontext.show_layout(layout) |
1403 | + |
1404 | +def draw_text(context, layout, markup, x, y, maxw = 0, color = (0.3, 0.3, 0.3)): |
1405 | + """ |
1406 | + Draw text using a cairo context and a pango layout |
1407 | + |
1408 | + Arguments: |
1409 | + :param context: a cairo context |
1410 | + :param layout: a pango layout |
1411 | + :param x: x position of the bubble |
1412 | + :param y: y position of the bubble |
1413 | + :param maxw: the max text width in pixels |
1414 | + :param color: a rgb tuple |
1415 | + """ |
1416 | + pcontext = pangocairo.CairoContext(context) |
1417 | + layout.set_markup(markup) |
1418 | + layout.set_ellipsize(pango.ELLIPSIZE_MIDDLE) |
1419 | + pcontext.set_source_rgba(*color) |
1420 | + if maxw: |
1421 | + layout.set_width(maxw*1024) |
1422 | + pcontext.move_to(x, y) |
1423 | + pcontext.show_layout(layout) |
1424 | + |
1425 | +def render_pixbuf(window, x, y, pixbuf, drawframe = True): |
1426 | + """ |
1427 | + Renders a pixbuf to be displayed on the cell |
1428 | + |
1429 | + Arguments: |
1430 | + :param window: a gdk window |
1431 | + :param x: x position |
1432 | + :param y: y position |
1433 | + :param drawframe: if true we draw a frame around the pixbuf |
1434 | + """ |
1435 | + imgw, imgh = pixbuf.get_width(), pixbuf.get_height() |
1436 | + context = window.cairo_create() |
1437 | + context.rectangle(x, y, imgw, imgh) |
1438 | + if drawframe: |
1439 | + context.set_source_rgb(1, 1, 1) |
1440 | + context.fill_preserve() |
1441 | + context.set_source_pixbuf(pixbuf, x, y) |
1442 | + context.fill() |
1443 | + if drawframe: # Draw a pretty frame |
1444 | + draw_frame(context, x, y, imgw, imgh) |
1445 | + |
1446 | +def render_emblems(window, x, y, w, h, emblems): |
1447 | + """ |
1448 | + Renders emblems on the four corners of the rectangle |
1449 | + |
1450 | + Arguments: |
1451 | + :param window: a gdk window |
1452 | + :param x: x position |
1453 | + :param y: y position |
1454 | + :param w: the width of the rectangle |
1455 | + :param y: the height of the rectangle |
1456 | + :param emblems: a list of pixbufs |
1457 | + """ |
1458 | + # w = max(self.width, w) |
1459 | + corners = [[x, y], |
1460 | + [x+w, y], |
1461 | + [x, y+h], |
1462 | + [x+w-4, y+h-4]] |
1463 | + context = window.cairo_create() |
1464 | + for i in xrange(len(emblems)): |
1465 | + i = i % len(emblems) |
1466 | + pixbuf = emblems[i] |
1467 | + if pixbuf: |
1468 | + pbw, pbh = pixbuf.get_width()/2, pixbuf.get_height()/2 |
1469 | + context.set_source_pixbuf(pixbuf, corners[i][0]-pbw, corners[i][1]-pbh) |
1470 | + context.rectangle(corners[i][0]-pbw, corners[i][1]-pbh, pbw*2, pbh*2) |
1471 | + context.fill() |
1472 | + |
1473 | +## |
1474 | +## Color functions |
1475 | + |
1476 | +def shade_gdk_color(color, shade): |
1477 | + """ |
1478 | + Shades a color by a fraction |
1479 | + |
1480 | + Arguments: |
1481 | + :param color: a gdk color |
1482 | + :param shade: fraction by which to shade the color |
1483 | + |
1484 | + :returns: a :class:`Color <gtk.gdk.Color>` |
1485 | + """ |
1486 | + f = lambda num: min((num * shade, 65535.0)) |
1487 | + if gtk.pygtk_version >= (2, 16, 0): |
1488 | + color.red = f(color.red) |
1489 | + color.green = f(color.green) |
1490 | + color.blue = f(color.blue) |
1491 | + else: |
1492 | + red = int(f(color.red)) |
1493 | + green = int(f(color.green)) |
1494 | + blue = int(f(color.blue)) |
1495 | + color = gtk.gdk.Color(red=red, green=green, blue=blue) |
1496 | + return color |
1497 | + |
1498 | +def combine_gdk_color(color, fcolor): |
1499 | + """ |
1500 | + Combines a color with another color |
1501 | + |
1502 | + Arguments: |
1503 | + :param color: a gdk color |
1504 | + :param fcolor: a gdk color to combine with color |
1505 | + |
1506 | + :returns: a :class:`Color <gtk.gdk.Color>` |
1507 | + """ |
1508 | + if gtk.pygtk_version >= (2, 16, 0): |
1509 | + color.red = (2*color.red + fcolor.red)/3 |
1510 | + color.green = (2*color.green + fcolor.green)/3 |
1511 | + color.blue = (2*color.blue + fcolor.blue)/3 |
1512 | + else: |
1513 | + red = int(((2*color.red + fcolor.red)/3)) |
1514 | + green = int(((2*color.green + fcolor.green)/3)) |
1515 | + blue = int(((2*color.blue + fcolor.blue)/3)) |
1516 | + color = gtk.gdk.Color(red=red, green=green, blue=blue) |
1517 | + return color |
1518 | + |
1519 | +def get_gtk_rgba(style, palette, i, shade = 1, alpha = 1): |
1520 | + """Takes a gtk style and returns a RGB tuple |
1521 | + |
1522 | + Arguments: |
1523 | + :param style: a gtk_style object |
1524 | + :param palette: a string representing the palette you want to pull a color from |
1525 | + Example: "bg", "fg" |
1526 | + :param shade: how much you want to shade the color |
1527 | + |
1528 | + :returns: a rgba tuple |
1529 | + """ |
1530 | + f = lambda num: (num/65535.0) * shade |
1531 | + color = getattr(style, palette)[i] |
1532 | + if isinstance(color, gtk.gdk.Color): |
1533 | + red = f(color.red) |
1534 | + green = f(color.green) |
1535 | + blue = f(color.blue) |
1536 | + return (min(red, 1), min(green, 1), min(blue, 1), alpha) |
1537 | + else: raise TypeError("Not a valid gtk.gdk.Color") |
1538 | + |
1539 | + |
1540 | +## |
1541 | +## Pixbuff work |
1542 | +## |
1543 | + |
1544 | +def new_grayscale_pixbuf(pixbuf): |
1545 | + """ |
1546 | + Makes a pixbuf grayscale |
1547 | + |
1548 | + :param pixbuf: a :class:`Pixbuf <gtk.gdk.Pixbuf>` |
1549 | + |
1550 | + :returns: a :class:`Pixbuf <gtk.gdk.Pixbuf>` |
1551 | + |
1552 | + """ |
1553 | + pixbuf2 = pixbuf.copy() |
1554 | + pixbuf.saturate_and_pixelate(pixbuf2, 0.0, False) |
1555 | + return pixbuf2 |
1556 | + |
1557 | +def crop_pixbuf(pixbuf, x, y, width, height): |
1558 | + """ |
1559 | + Crop a pixbuf |
1560 | + |
1561 | + Arguments: |
1562 | + :param pixbuf: a :class:`Pixbuf <gtk.gdk.Pixbuf>` |
1563 | + :param x: the x position to crop from in the source |
1564 | + :param y: the y position to crop from in the source |
1565 | + :param width: crop width |
1566 | + :param height: crop height |
1567 | + |
1568 | + :returns: a :class:`Pixbuf <gtk.gdk.Pixbuf>` |
1569 | + """ |
1570 | + dest_pixbuf = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, True, 8, width, height) |
1571 | + pixbuf.copy_area(x, y, width, height, dest_pixbuf, 0, 0) |
1572 | + return dest_pixbuf |
1573 | + |
1574 | +def scale_to_fill(pixbuf, neww, newh): |
1575 | + """ |
1576 | + Scales/crops a new pixbuf to a width and height at best fit and returns it |
1577 | + |
1578 | + Arguments: |
1579 | + :param pixbuf: a :class:`Pixbuf <gtk.gdk.Pixbuf>` |
1580 | + :param neww: new width of the new pixbuf |
1581 | + :param newh: a new height of the new pixbuf |
1582 | + |
1583 | + :returns: a :class:`Pixbuf <gtk.gdk.Pixbuf>` |
1584 | + """ |
1585 | + imagew, imageh = pixbuf.get_width(), pixbuf.get_height() |
1586 | + if (imagew, imageh) != (neww, newh): |
1587 | + imageratio = float(imagew) / float(imageh) |
1588 | + newratio = float(neww) / float(newh) |
1589 | + if imageratio > newratio: |
1590 | + transformw = int(round(newh * imageratio)) |
1591 | + pixbuf = pixbuf.scale_simple(transformw, newh, gtk.gdk.INTERP_BILINEAR) |
1592 | + pixbuf = crop_pixbuf(pixbuf, 0, 0, neww, newh) |
1593 | + elif imageratio < newratio: |
1594 | + transformh = int(round(neww / imageratio)) |
1595 | + pixbuf = pixbuf.scale_simple(neww, transformh, gtk.gdk.INTERP_BILINEAR) |
1596 | + pixbuf = crop_pixbuf(pixbuf, 0, 0, neww, newh) |
1597 | + else: |
1598 | + pixbuf = pixbuf.scale_simple(neww, newh, gtk.gdk.INTERP_BILINEAR) |
1599 | + return pixbuf |
1600 | + |
1601 | + |
1602 | +class PixbufCache(dict): |
1603 | + """ |
1604 | + A pixbuf cache dict which stores, loads, and saves pixbufs to a cache and to |
1605 | + the users filesystem. The naming scheme for thumb files are use hash |
1606 | + |
1607 | + There are huge flaws with this object. It does not have a ceiling, and it |
1608 | + does not remove thumbnails from the file system. Essentially meaning the |
1609 | + cache directory can grow forever. |
1610 | + """ |
1611 | + def __init__(self, *args, **kwargs): |
1612 | + super(PixbufCache, self).__init__() |
1613 | + |
1614 | + def check_cache(self, uri): |
1615 | + return self[uri] |
1616 | + |
1617 | + def get_buff(self, key): |
1618 | + thumbpath = os.path.expanduser("~/.cache/GAJ/1_" + str(hash(key))) |
1619 | + if os.path.exists(thumbpath): |
1620 | + self[key] = (gtk.gdk.pixbuf_new_from_file(thumbpath), True) |
1621 | + return self[key] |
1622 | + return None |
1623 | + |
1624 | + def __getitem__(self, key): |
1625 | + if self.has_key(key): |
1626 | + return super(PixbufCache, self).__getitem__(key) |
1627 | + return self.get_buff(key) |
1628 | + |
1629 | + def __setitem__(self, key, (pb, isthumb)): |
1630 | + dir_ = os.path.expanduser("~/.cache/GAJ/") |
1631 | + if not os.path.exists(os.path.expanduser("~/.cache/GAJ/")): |
1632 | + os.makedirs(dir_) |
1633 | + path = dir_ + str(hash(isthumb)) + "_" + str(hash(key)) |
1634 | + if not os.path.exists(path): |
1635 | + open(path, 'w').close() |
1636 | + pb.save(path, "png") |
1637 | + return super(PixbufCache, self).__setitem__(key, (pb, isthumb)) |
1638 | + |
1639 | + def get_pixbuf_from_uri(self, uri, size=SIZE_LARGE, iconscale=1, w=0, h=0): |
1640 | + """ |
1641 | + Returns a pixbuf and True if a thumbnail was found, else False. Uses the |
1642 | + Pixbuf Cache for thumbnail compatible files. If the pixbuf is a thumb |
1643 | + it is cached. |
1644 | + |
1645 | + Arguments: |
1646 | + :param uri: a uri on the disk |
1647 | + :param size: a size tuple from thumbfactory |
1648 | + :param iconscale: a factor to reduce icons by (not thumbs) |
1649 | + :param w: resulting width |
1650 | + :param h: resulting height |
1651 | + |
1652 | + Warning! This function is in need of a serious clean up. |
1653 | + |
1654 | + :returns: a tuple containing a :class:`Pixbuf <gtk.gdk.Pixbuf>` and bool |
1655 | + which is True if a thumbnail was found |
1656 | + """ |
1657 | + try: |
1658 | + cached = self.check_cache(uri) |
1659 | + except gobject.GError: |
1660 | + cached = None |
1661 | + if cached: |
1662 | + return cached |
1663 | + gfile = GioFile.create(uri) |
1664 | + thumb = True |
1665 | + if gfile: |
1666 | + if gfile.has_preview(): |
1667 | + pb = gfile.get_thumbnail(size=size) |
1668 | + else: |
1669 | + iconsize = int(size[0]*iconscale) |
1670 | + pb = gfile.get_icon(size=iconsize) |
1671 | + thumb = False |
1672 | + else: pb = None |
1673 | + if not pb: |
1674 | + pb = ICON_THEME.lookup_icon(gtk.STOCK_MISSING_IMAGE, int(size[0]*iconscale), gtk.ICON_LOOKUP_FORCE_SVG).load_icon() |
1675 | + thumb = False |
1676 | + if thumb: |
1677 | + pb = scale_to_fill(pb, w, h) |
1678 | + self[uri] = (pb, thumb) |
1679 | + return pb, thumb |
1680 | + |
1681 | +PIXBUFCACHE = PixbufCache() |
1682 | + |
1683 | +def get_icon_for_name(name, size): |
1684 | + """ |
1685 | + return a icon for a name |
1686 | + """ |
1687 | + size = int(size) |
1688 | + ICONS[(size, size)] |
1689 | + if ICONS[(size, size)].has_key(name): |
1690 | + return ICONS[(size, size)][name] |
1691 | + info = ICON_THEME.lookup_icon(name, size, gtk.ICON_LOOKUP_USE_BUILTIN) |
1692 | + if not info: |
1693 | + return None |
1694 | + location = info.get_filename() |
1695 | + return get_icon_for_uri(location, size) |
1696 | + |
1697 | +def get_icon_for_uri(uri, size): |
1698 | + pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(uri, size, size) |
1699 | + ICONS[(size, size)][uri] = pixbuf |
1700 | + return pixbuf |
1701 | + |
1702 | +def get_icon_from_object_at_uri(uri, size): |
1703 | + """ |
1704 | + Returns a icon from a event at size |
1705 | + |
1706 | + :param uri: a uri string |
1707 | + :param size: a int representing the size in pixels of the icon |
1708 | + |
1709 | + :returns: a :class:`Pixbuf <gtk.gdk.Pixbuf>` |
1710 | + """ |
1711 | + gfile = GioFile.create(uri) |
1712 | + if gfile: |
1713 | + pb = gfile.get_icon(size=size) |
1714 | + if pb: |
1715 | + return pb |
1716 | + return False |
1717 | + |
1718 | + |
1719 | + |
1720 | +## |
1721 | +## Other useful methods |
1722 | +## |
1723 | + |
1724 | +def is_command_available(command): |
1725 | + """ |
1726 | + Checks whether the given command is available, by looking for it in |
1727 | + the PATH. |
1728 | + |
1729 | + This is useful for ensuring that optional dependencies on external |
1730 | + applications are fulfilled. |
1731 | + """ |
1732 | + assert len(" a".split()) == 1, "No arguments are accepted in command" |
1733 | + for directory in os.environ["PATH"].split(os.pathsep): |
1734 | + if os.path.exists(os.path.join(directory, command)): |
1735 | + return True |
1736 | + return False |
1737 | + |
1738 | +def launch_command(command, arguments=None): |
1739 | + """ |
1740 | + Launches a program as an independent process. |
1741 | + """ |
1742 | + if not arguments: |
1743 | + arguments = [] |
1744 | + null = os.open(os.devnull, os.O_RDWR) |
1745 | + subprocess.Popen([command] + arguments, stdout=null, stderr=null, |
1746 | + close_fds=True) |
1747 | + |
1748 | +def launch_string_command(command): |
1749 | + """ |
1750 | + Launches a program as an independent from a string |
1751 | + """ |
1752 | + command = command.split(" ") |
1753 | + null = os.open(os.devnull, os.O_RDWR) |
1754 | + subprocess.Popen(command, stdout=null, stderr=null, |
1755 | + close_fds=True) |
1756 | + |
1757 | |
1758 | === removed file 'src/common.py' |
1759 | --- src/common.py 2010-04-19 04:51:23 +0000 |
1760 | +++ src/common.py 1970-01-01 00:00:00 +0000 |
1761 | @@ -1,582 +0,0 @@ |
1762 | -# -.- coding: utf-8 -.- |
1763 | -# |
1764 | -# Filename |
1765 | -# |
1766 | -# Copyright © 2010 Randal Barlow <email.tehk@gmail.com> |
1767 | -# Copyright © 2010 Siegfried Gevatter <siegfried@gevatter.com> |
1768 | -# |
1769 | -# This program is free software: you can redistribute it and/or modify |
1770 | -# it under the terms of the GNU General Public License as published by |
1771 | -# the Free Software Foundation, either version 3 of the License, or |
1772 | -# (at your option) any later version. |
1773 | -# |
1774 | -# This program is distributed in the hope that it will be useful, |
1775 | -# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1776 | -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1777 | -# GNU General Public License for more details. |
1778 | -# |
1779 | -# You should have received a copy of the GNU General Public License |
1780 | -# along with this program. If not, see <http://www.gnu.org/licenses/>. |
1781 | - |
1782 | -""" |
1783 | -Common Functions and classes which are used to create the alternative views, |
1784 | -and handle colors, pixbufs, and text |
1785 | -""" |
1786 | - |
1787 | -import cairo |
1788 | -import gobject |
1789 | -import gtk |
1790 | -import os |
1791 | -import pango |
1792 | -import pangocairo |
1793 | -import time |
1794 | -import math |
1795 | -import operator |
1796 | -import subprocess |
1797 | - |
1798 | -from gio_file import GioFile, SIZE_LARGE, SIZE_NORMAL, SIZE_THUMBVIEW, SIZE_TIMELINEVIEW, ICONS |
1799 | -from config import get_data_path, get_icon_path |
1800 | - |
1801 | -from zeitgeist.datamodel import Interpretation, Event |
1802 | - |
1803 | - |
1804 | -# Caches desktop files |
1805 | -DESKTOP_FILES = {} |
1806 | -DESKTOP_FILE_PATHS = [] |
1807 | -try: |
1808 | - desktop_file_paths = os.environ["XDG_DATA_DIRS"].split(":") |
1809 | - for path in desktop_file_paths: |
1810 | - if path.endswith("/"): |
1811 | - DESKTOP_FILE_PATHS.append(path + "applications/") |
1812 | - else: |
1813 | - DESKTOP_FILE_PATHS.append(path + "/applications/") |
1814 | -except KeyError:pass |
1815 | - |
1816 | -# Placeholder pixbufs for common sizes |
1817 | -PLACEHOLDER_PIXBUFFS = { |
1818 | - 24 : gtk.gdk.pixbuf_new_from_file_at_size(get_icon_path("hicolor/scalable/apps/gnome-activity-journal.svg"), 24, 24), |
1819 | - 16 : gtk.gdk.pixbuf_new_from_file_at_size(get_icon_path("hicolor/scalable/apps/gnome-activity-journal.svg"), 16, 16) |
1820 | - } |
1821 | - |
1822 | -# Color magic |
1823 | -TANGOCOLORS = [ |
1824 | - (252/255.0, 234/255.0, 79/255.0),#0 |
1825 | - (237/255.0, 212/255.0, 0/255.0), |
1826 | - (196/255.0, 160/255.0, 0/255.0), |
1827 | - |
1828 | - (252/255.0, 175/255.0, 62/255.0),#3 |
1829 | - (245/255.0, 121/255.0, 0/255.0), |
1830 | - (206/255.0, 92/255.0, 0/255.0), |
1831 | - |
1832 | - (233/255.0, 185/255.0, 110/255.0),#6 |
1833 | - (193/255.0, 125/255.0, 17/255.0), |
1834 | - (143/255.0, 89/255.0, 02/255.0), |
1835 | - |
1836 | - (138/255.0, 226/255.0, 52/255.0),#9 |
1837 | - (115/255.0, 210/255.0, 22/255.0), |
1838 | - ( 78/255.0, 154/255.0, 06/255.0), |
1839 | - |
1840 | - (114/255.0, 159/255.0, 207/255.0),#12 |
1841 | - ( 52/255.0, 101/255.0, 164/255.0), |
1842 | - ( 32/255.0, 74/255.0, 135/255.0), |
1843 | - |
1844 | - (173/255.0, 127/255.0, 168/255.0),#15 |
1845 | - (117/255.0, 80/255.0, 123/255.0), |
1846 | - ( 92/255.0, 53/255.0, 102/255.0), |
1847 | - |
1848 | - (239/255.0, 41/255.0, 41/255.0),#18 |
1849 | - (204/255.0, 0/255.0, 0/255.0), |
1850 | - (164/255.0, 0/255.0, 0/255.0), |
1851 | - |
1852 | - (136/255.0, 138/255.0, 133/255.0),#21 |
1853 | - ( 85/255.0, 87/255.0, 83/255.0), |
1854 | - ( 46/255.0, 52/255.0, 54/255.0), |
1855 | -] |
1856 | - |
1857 | -FILETYPES = { |
1858 | - Interpretation.VIDEO.uri : 0, |
1859 | - Interpretation.MUSIC.uri : 3, |
1860 | - Interpretation.DOCUMENT.uri : 12, |
1861 | - Interpretation.IMAGE.uri : 15, |
1862 | - Interpretation.SOURCECODE.uri : 12, |
1863 | - Interpretation.UNKNOWN.uri : 21, |
1864 | - Interpretation.IM_MESSAGE.uri : 21, |
1865 | - Interpretation.EMAIL.uri : 21 |
1866 | -} |
1867 | - |
1868 | -FILETYPESNAMES = { |
1869 | - Interpretation.VIDEO.uri : _("Video"), |
1870 | - Interpretation.MUSIC.uri : _("Music"), |
1871 | - Interpretation.DOCUMENT.uri : _("Document"), |
1872 | - Interpretation.IMAGE.uri : _("Image"), |
1873 | - Interpretation.SOURCECODE.uri : _("Source Code"), |
1874 | - Interpretation.UNKNOWN.uri : _("Unknown"), |
1875 | - Interpretation.IM_MESSAGE.uri : _("IM Message"), |
1876 | - Interpretation.EMAIL.uri :_("Email"), |
1877 | - |
1878 | -} |
1879 | - |
1880 | -MEDIAINTERPRETATIONS = [ |
1881 | - Interpretation.VIDEO.uri, |
1882 | - Interpretation.IMAGE.uri, |
1883 | -] |
1884 | - |
1885 | -TIMELABELS = [_("Morning"), _("Afternoon"), _("Evening")] |
1886 | -ICON_THEME = gtk.icon_theme_get_default() |
1887 | - |
1888 | -def get_file_color(ftype, fmime): |
1889 | - """Uses hashing to choose a shade from a hue in the color tuple above |
1890 | - |
1891 | - :param ftype: a :class:`Event <zeitgeist.datamodel.Interpretation>` |
1892 | - :param fmime: a mime type string |
1893 | - """ |
1894 | - if ftype in FILETYPES.keys(): |
1895 | - i = FILETYPES[ftype] |
1896 | - l = int(math.fabs(hash(fmime))) % 3 |
1897 | - return TANGOCOLORS[min(i+l, len(TANGOCOLORS)-1)] |
1898 | - return (136/255.0, 138/255.0, 133/255.0) |
1899 | - |
1900 | -## |
1901 | -## Zeitgeist event helper functions |
1902 | - |
1903 | -def get_event_typename(event): |
1904 | - """ |
1905 | - :param event: a :class:`Event <zeitgeist.datamodel.Event>` |
1906 | - |
1907 | - :returns: a plain text version of a interpretation |
1908 | - """ |
1909 | - try: |
1910 | - return Interpretation[event.subjects[0].interpretation].display_name |
1911 | - except KeyError: |
1912 | - pass |
1913 | - return FILETYPESNAMES[event.subjects[0].interpretation] |
1914 | - |
1915 | -## |
1916 | -# Cairo drawing functions |
1917 | - |
1918 | -def draw_frame(context, x, y, w, h): |
1919 | - """ |
1920 | - Draws a 2 pixel frame around a area defined by x, y, w, h using a cairo context |
1921 | - |
1922 | - :param context: a cairo context |
1923 | - :param x: x position of the frame |
1924 | - :param y: y position of the frame |
1925 | - :param w: width of the frame |
1926 | - :param h: height of the frame |
1927 | - """ |
1928 | - x, y = int(x)+0.5, int(y)+0.5 |
1929 | - w, h = int(w), int(h) |
1930 | - context.set_line_width(1) |
1931 | - context.rectangle(x-1, y-1, w+2, h+2) |
1932 | - context.set_source_rgba(0.5, 0.5, 0.5)#0.3, 0.3, 0.3) |
1933 | - context.stroke() |
1934 | - context.set_source_rgba(0.7, 0.7, 0.7) |
1935 | - context.rectangle(x, y, w, h) |
1936 | - context.stroke() |
1937 | - context.set_source_rgba(0.4, 0.4, 0.4) |
1938 | - context.rectangle(x+1, y+1, w-2, h-2) |
1939 | - context.stroke() |
1940 | - |
1941 | -def draw_rounded_rectangle(context, x, y, w, h, r=5): |
1942 | - """Draws a rounded rectangle |
1943 | - |
1944 | - :param context: a cairo context |
1945 | - :param x: x position of the rectangle |
1946 | - :param y: y position of the rectangle |
1947 | - :param w: width of the rectangle |
1948 | - :param h: height of the rectangle |
1949 | - :param r: radius of the rectangle |
1950 | - """ |
1951 | - context.new_sub_path() |
1952 | - context.arc(r+x, r+y, r, math.pi, 3 * math.pi /2) |
1953 | - context.arc(w-r+x, r+y, r, 3 * math.pi / 2, 0) |
1954 | - context.arc(w-r+x, h-r+y, r, 0, math.pi/2) |
1955 | - context.arc(r+x, h-r+y, r, math.pi/2, math.pi) |
1956 | - context.close_path() |
1957 | - return context |
1958 | - |
1959 | -def draw_speech_bubble(context, layout, x, y, w, h): |
1960 | - """ |
1961 | - Draw a speech bubble at a position |
1962 | - |
1963 | - Arguments: |
1964 | - :param context: a cairo context |
1965 | - :param layout: a pango layout |
1966 | - :param x: x position of the bubble |
1967 | - :param y: y position of the bubble |
1968 | - :param w: width of the bubble |
1969 | - :param h: height of the bubble |
1970 | - """ |
1971 | - layout.set_width((w-10)*1024) |
1972 | - layout.set_ellipsize(pango.ELLIPSIZE_MIDDLE) |
1973 | - textw, texth = layout.get_pixel_size() |
1974 | - context.new_path() |
1975 | - context.move_to(x + 0.45*w, y+h*0.1 + 2) |
1976 | - context.line_to(x + 0.5*w, y) |
1977 | - context.line_to(x + 0.55*w, y+h*0.1 + 2) |
1978 | - h = max(texth + 5, h) |
1979 | - draw_rounded_rectangle(context, x, y+h*0.1, w, h, r = 5) |
1980 | - context.close_path() |
1981 | - context.set_line_width(2) |
1982 | - context.set_source_rgb(168/255.0, 165/255.0, 134/255.0) |
1983 | - context.stroke_preserve() |
1984 | - context.set_source_rgb(253/255.0, 248/255.0, 202/255.0) |
1985 | - context.fill() |
1986 | - pcontext = pangocairo.CairoContext(context) |
1987 | - pcontext.set_source_rgb(0, 0, 0) |
1988 | - pcontext.move_to(x+5, y+5) |
1989 | - pcontext.show_layout(layout) |
1990 | - |
1991 | -def draw_text(context, layout, markup, x, y, maxw = 0, color = (0.3, 0.3, 0.3)): |
1992 | - """ |
1993 | - Draw text using a cairo context and a pango layout |
1994 | - |
1995 | - Arguments: |
1996 | - :param context: a cairo context |
1997 | - :param layout: a pango layout |
1998 | - :param x: x position of the bubble |
1999 | - :param y: y position of the bubble |
2000 | - :param maxw: the max text width in pixels |
2001 | - :param color: a rgb tuple |
2002 | - """ |
2003 | - pcontext = pangocairo.CairoContext(context) |
2004 | - layout.set_markup(markup) |
2005 | - layout.set_ellipsize(pango.ELLIPSIZE_MIDDLE) |
2006 | - pcontext.set_source_rgba(*color) |
2007 | - if maxw: |
2008 | - layout.set_width(maxw*1024) |
2009 | - pcontext.move_to(x, y) |
2010 | - pcontext.show_layout(layout) |
2011 | - |
2012 | -def render_pixbuf(window, x, y, pixbuf, drawframe = True): |
2013 | - """ |
2014 | - Renders a pixbuf to be displayed on the cell |
2015 | - |
2016 | - Arguments: |
2017 | - :param window: a gdk window |
2018 | - :param x: x position |
2019 | - :param y: y position |
2020 | - :param drawframe: if true we draw a frame around the pixbuf |
2021 | - """ |
2022 | - imgw, imgh = pixbuf.get_width(), pixbuf.get_height() |
2023 | - context = window.cairo_create() |
2024 | - context.rectangle(x, y, imgw, imgh) |
2025 | - if drawframe: |
2026 | - context.set_source_rgb(1, 1, 1) |
2027 | - context.fill_preserve() |
2028 | - context.set_source_pixbuf(pixbuf, x, y) |
2029 | - context.fill() |
2030 | - if drawframe: # Draw a pretty frame |
2031 | - draw_frame(context, x, y, imgw, imgh) |
2032 | - |
2033 | -def render_emblems(window, x, y, w, h, emblems): |
2034 | - """ |
2035 | - Renders emblems on the four corners of the rectangle |
2036 | - |
2037 | - Arguments: |
2038 | - :param window: a gdk window |
2039 | - :param x: x position |
2040 | - :param y: y position |
2041 | - :param w: the width of the rectangle |
2042 | - :param y: the height of the rectangle |
2043 | - :param emblems: a list of pixbufs |
2044 | - """ |
2045 | - # w = max(self.width, w) |
2046 | - corners = [[x, y], |
2047 | - [x+w, y], |
2048 | - [x, y+h], |
2049 | - [x+w-4, y+h-4]] |
2050 | - context = window.cairo_create() |
2051 | - for i in xrange(len(emblems)): |
2052 | - i = i % len(emblems) |
2053 | - pixbuf = emblems[i] |
2054 | - if pixbuf: |
2055 | - pbw, pbh = pixbuf.get_width()/2, pixbuf.get_height()/2 |
2056 | - context.set_source_pixbuf(pixbuf, corners[i][0]-pbw, corners[i][1]-pbh) |
2057 | - context.rectangle(corners[i][0]-pbw, corners[i][1]-pbh, pbw*2, pbh*2) |
2058 | - context.fill() |
2059 | - |
2060 | -## |
2061 | -## Color functions |
2062 | - |
2063 | -def shade_gdk_color(color, shade): |
2064 | - """ |
2065 | - Shades a color by a fraction |
2066 | - |
2067 | - Arguments: |
2068 | - :param color: a gdk color |
2069 | - :param shade: fraction by which to shade the color |
2070 | - |
2071 | - :returns: a :class:`Color <gtk.gdk.Color>` |
2072 | - """ |
2073 | - f = lambda num: min((num * shade, 65535.0)) |
2074 | - if gtk.pygtk_version >= (2, 16, 0): |
2075 | - color.red = f(color.red) |
2076 | - color.green = f(color.green) |
2077 | - color.blue = f(color.blue) |
2078 | - else: |
2079 | - red = int(f(color.red)) |
2080 | - green = int(f(color.green)) |
2081 | - blue = int(f(color.blue)) |
2082 | - color = gtk.gdk.Color(red=red, green=green, blue=blue) |
2083 | - return color |
2084 | - |
2085 | -def combine_gdk_color(color, fcolor): |
2086 | - """ |
2087 | - Combines a color with another color |
2088 | - |
2089 | - Arguments: |
2090 | - :param color: a gdk color |
2091 | - :param fcolor: a gdk color to combine with color |
2092 | - |
2093 | - :returns: a :class:`Color <gtk.gdk.Color>` |
2094 | - """ |
2095 | - if gtk.pygtk_version >= (2, 16, 0): |
2096 | - color.red = (2*color.red + fcolor.red)/3 |
2097 | - color.green = (2*color.green + fcolor.green)/3 |
2098 | - color.blue = (2*color.blue + fcolor.blue)/3 |
2099 | - else: |
2100 | - red = int(((2*color.red + fcolor.red)/3)) |
2101 | - green = int(((2*color.green + fcolor.green)/3)) |
2102 | - blue = int(((2*color.blue + fcolor.blue)/3)) |
2103 | - color = gtk.gdk.Color(red=red, green=green, blue=blue) |
2104 | - return color |
2105 | - |
2106 | -def get_gtk_rgba(style, palette, i, shade = 1, alpha = 1): |
2107 | - """Takes a gtk style and returns a RGB tuple |
2108 | - |
2109 | - Arguments: |
2110 | - :param style: a gtk_style object |
2111 | - :param palette: a string representing the palette you want to pull a color from |
2112 | - Example: "bg", "fg" |
2113 | - :param shade: how much you want to shade the color |
2114 | - |
2115 | - :returns: a rgba tuple |
2116 | - """ |
2117 | - f = lambda num: (num/65535.0) * shade |
2118 | - color = getattr(style, palette)[i] |
2119 | - if isinstance(color, gtk.gdk.Color): |
2120 | - red = f(color.red) |
2121 | - green = f(color.green) |
2122 | - blue = f(color.blue) |
2123 | - return (min(red, 1), min(green, 1), min(blue, 1), alpha) |
2124 | - else: raise TypeError("Not a valid gtk.gdk.Color") |
2125 | - |
2126 | - |
2127 | -## |
2128 | -## Pixbuff work |
2129 | -## |
2130 | - |
2131 | -def new_grayscale_pixbuf(pixbuf): |
2132 | - """ |
2133 | - Makes a pixbuf grayscale |
2134 | - |
2135 | - :param pixbuf: a :class:`Pixbuf <gtk.gdk.Pixbuf>` |
2136 | - |
2137 | - :returns: a :class:`Pixbuf <gtk.gdk.Pixbuf>` |
2138 | - |
2139 | - """ |
2140 | - pixbuf2 = pixbuf.copy() |
2141 | - pixbuf.saturate_and_pixelate(pixbuf2, 0.0, False) |
2142 | - return pixbuf2 |
2143 | - |
2144 | -def crop_pixbuf(pixbuf, x, y, width, height): |
2145 | - """ |
2146 | - Crop a pixbuf |
2147 | - |
2148 | - Arguments: |
2149 | - :param pixbuf: a :class:`Pixbuf <gtk.gdk.Pixbuf>` |
2150 | - :param x: the x position to crop from in the source |
2151 | - :param y: the y position to crop from in the source |
2152 | - :param width: crop width |
2153 | - :param height: crop height |
2154 | - |
2155 | - :returns: a :class:`Pixbuf <gtk.gdk.Pixbuf>` |
2156 | - """ |
2157 | - dest_pixbuf = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, True, 8, width, height) |
2158 | - pixbuf.copy_area(x, y, width, height, dest_pixbuf, 0, 0) |
2159 | - return dest_pixbuf |
2160 | - |
2161 | -def scale_to_fill(pixbuf, neww, newh): |
2162 | - """ |
2163 | - Scales/crops a new pixbuf to a width and height at best fit and returns it |
2164 | - |
2165 | - Arguments: |
2166 | - :param pixbuf: a :class:`Pixbuf <gtk.gdk.Pixbuf>` |
2167 | - :param neww: new width of the new pixbuf |
2168 | - :param newh: a new height of the new pixbuf |
2169 | - |
2170 | - :returns: a :class:`Pixbuf <gtk.gdk.Pixbuf>` |
2171 | - """ |
2172 | - imagew, imageh = pixbuf.get_width(), pixbuf.get_height() |
2173 | - if (imagew, imageh) != (neww, newh): |
2174 | - imageratio = float(imagew) / float(imageh) |
2175 | - newratio = float(neww) / float(newh) |
2176 | - if imageratio > newratio: |
2177 | - transformw = int(round(newh * imageratio)) |
2178 | - pixbuf = pixbuf.scale_simple(transformw, newh, gtk.gdk.INTERP_BILINEAR) |
2179 | - pixbuf = crop_pixbuf(pixbuf, 0, 0, neww, newh) |
2180 | - elif imageratio < newratio: |
2181 | - transformh = int(round(neww / imageratio)) |
2182 | - pixbuf = pixbuf.scale_simple(neww, transformh, gtk.gdk.INTERP_BILINEAR) |
2183 | - pixbuf = crop_pixbuf(pixbuf, 0, 0, neww, newh) |
2184 | - else: |
2185 | - pixbuf = pixbuf.scale_simple(neww, newh, gtk.gdk.INTERP_BILINEAR) |
2186 | - return pixbuf |
2187 | - |
2188 | - |
2189 | -class PixbufCache(dict): |
2190 | - """ |
2191 | - A pixbuf cache dict which stores, loads, and saves pixbufs to a cache and to |
2192 | - the users filesystem. The naming scheme for thumb files are use hash |
2193 | - |
2194 | - There are huge flaws with this object. It does not have a ceiling, and it |
2195 | - does not remove thumbnails from the file system. Essentially meaning the |
2196 | - cache directory can grow forever. |
2197 | - """ |
2198 | - def __init__(self, *args, **kwargs): |
2199 | - super(PixbufCache, self).__init__() |
2200 | - |
2201 | - def check_cache(self, uri): |
2202 | - return self[uri] |
2203 | - |
2204 | - def get_buff(self, key): |
2205 | - thumbpath = os.path.expanduser("~/.cache/GAJ/1_" + str(hash(key))) |
2206 | - if os.path.exists(thumbpath): |
2207 | - self[key] = (gtk.gdk.pixbuf_new_from_file(thumbpath), True) |
2208 | - return self[key] |
2209 | - return None |
2210 | - |
2211 | - def __getitem__(self, key): |
2212 | - if self.has_key(key): |
2213 | - return super(PixbufCache, self).__getitem__(key) |
2214 | - return self.get_buff(key) |
2215 | - |
2216 | - def __setitem__(self, key, (pb, isthumb)): |
2217 | - dir_ = os.path.expanduser("~/.cache/GAJ/") |
2218 | - if not os.path.exists(os.path.expanduser("~/.cache/GAJ/")): |
2219 | - os.makedirs(dir_) |
2220 | - path = dir_ + str(hash(isthumb)) + "_" + str(hash(key)) |
2221 | - if not os.path.exists(path): |
2222 | - open(path, 'w').close() |
2223 | - pb.save(path, "png") |
2224 | - return super(PixbufCache, self).__setitem__(key, (pb, isthumb)) |
2225 | - |
2226 | - def get_pixbuf_from_uri(self, uri, size=SIZE_LARGE, iconscale=1, w=0, h=0): |
2227 | - """ |
2228 | - Returns a pixbuf and True if a thumbnail was found, else False. Uses the |
2229 | - Pixbuf Cache for thumbnail compatible files. If the pixbuf is a thumb |
2230 | - it is cached. |
2231 | - |
2232 | - Arguments: |
2233 | - :param uri: a uri on the disk |
2234 | - :param size: a size tuple from thumbfactory |
2235 | - :param iconscale: a factor to reduce icons by (not thumbs) |
2236 | - :param w: resulting width |
2237 | - :param h: resulting height |
2238 | - |
2239 | - Warning! This function is in need of a serious clean up. |
2240 | - |
2241 | - :returns: a tuple containing a :class:`Pixbuf <gtk.gdk.Pixbuf>` and bool |
2242 | - which is True if a thumbnail was found |
2243 | - """ |
2244 | - try: |
2245 | - cached = self.check_cache(uri) |
2246 | - except gobject.GError: |
2247 | - cached = None |
2248 | - if cached: |
2249 | - return cached |
2250 | - gfile = GioFile.create(uri) |
2251 | - thumb = True |
2252 | - if gfile: |
2253 | - if gfile.has_preview(): |
2254 | - pb = gfile.get_thumbnail(size=size) |
2255 | - else: |
2256 | - iconsize = int(size[0]*iconscale) |
2257 | - pb = gfile.get_icon(size=iconsize) |
2258 | - thumb = False |
2259 | - else: pb = None |
2260 | - if not pb: |
2261 | - pb = ICON_THEME.lookup_icon(gtk.STOCK_MISSING_IMAGE, int(size[0]*iconscale), gtk.ICON_LOOKUP_FORCE_SVG).load_icon() |
2262 | - thumb = False |
2263 | - if thumb: |
2264 | - pb = scale_to_fill(pb, w, h) |
2265 | - self[uri] = (pb, thumb) |
2266 | - return pb, thumb |
2267 | - |
2268 | -PIXBUFCACHE = PixbufCache() |
2269 | - |
2270 | -def get_icon_for_name(name, size): |
2271 | - """ |
2272 | - return a icon for a name |
2273 | - """ |
2274 | - size = int(size) |
2275 | - ICONS[(size, size)] |
2276 | - if ICONS[(size, size)].has_key(name): |
2277 | - return ICONS[(size, size)][name] |
2278 | - info = ICON_THEME.lookup_icon(name, size, gtk.ICON_LOOKUP_USE_BUILTIN) |
2279 | - if not info: |
2280 | - return None |
2281 | - location = info.get_filename() |
2282 | - return get_icon_for_uri(location, size) |
2283 | - |
2284 | -def get_icon_for_uri(uri, size): |
2285 | - pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(uri, size, size) |
2286 | - ICONS[(size, size)][uri] = pixbuf |
2287 | - return pixbuf |
2288 | - |
2289 | -def get_icon_from_object_at_uri(uri, size): |
2290 | - """ |
2291 | - Returns a icon from a event at size |
2292 | - |
2293 | - :param uri: a uri string |
2294 | - :param size: a int representing the size in pixels of the icon |
2295 | - |
2296 | - :returns: a :class:`Pixbuf <gtk.gdk.Pixbuf>` |
2297 | - """ |
2298 | - gfile = GioFile.create(uri) |
2299 | - if gfile: |
2300 | - pb = gfile.get_icon(size=size) |
2301 | - if pb: |
2302 | - return pb |
2303 | - return False |
2304 | - |
2305 | - |
2306 | - |
2307 | -## |
2308 | -## Other useful methods |
2309 | -## |
2310 | - |
2311 | -def is_command_available(command): |
2312 | - """ |
2313 | - Checks whether the given command is available, by looking for it in |
2314 | - the PATH. |
2315 | - |
2316 | - This is useful for ensuring that optional dependencies on external |
2317 | - applications are fulfilled. |
2318 | - """ |
2319 | - assert len(" a".split()) == 1, "No arguments are accepted in command" |
2320 | - for directory in os.environ["PATH"].split(os.pathsep): |
2321 | - if os.path.exists(os.path.join(directory, command)): |
2322 | - return True |
2323 | - return False |
2324 | - |
2325 | -def launch_command(command, arguments=None): |
2326 | - """ |
2327 | - Launches a program as an independent process. |
2328 | - """ |
2329 | - if not arguments: |
2330 | - arguments = [] |
2331 | - null = os.open(os.devnull, os.O_RDWR) |
2332 | - subprocess.Popen([command] + arguments, stdout=null, stderr=null, |
2333 | - close_fds=True) |
2334 | - |
2335 | -def launch_string_command(command): |
2336 | - """ |
2337 | - Launches a program as an independent from a string |
2338 | - """ |
2339 | - command = command.split(" ") |
2340 | - null = os.open(os.devnull, os.O_RDWR) |
2341 | - subprocess.Popen(command, stdout=null, stderr=null, |
2342 | - close_fds=True) |
2343 | - |
2344 | |
2345 | === removed file 'src/daywidgets.py' |
2346 | --- src/daywidgets.py 2010-04-22 14:05:34 +0000 |
2347 | +++ src/daywidgets.py 1970-01-01 00:00:00 +0000 |
2348 | @@ -1,791 +0,0 @@ |
2349 | -# -.- coding: utf-8 -.- |
2350 | -# |
2351 | -# GNOME Activity Journal |
2352 | -# |
2353 | -# Copyright © 2009-2010 Seif Lotfy <seif@lotfy.com> |
2354 | -# Copyright © 2010 Randal Barlow <email.tehk@gmail.com> |
2355 | -# Copyright © 2010 Siegfried Gevatter <siegfried@gevatter.com> |
2356 | -# |
2357 | -# This program is free software: you can redistribute it and/or modify |
2358 | -# it under the terms of the GNU General Public License as published by |
2359 | -# the Free Software Foundation, either version 3 of the License, or |
2360 | -# (at your option) any later version. |
2361 | -# |
2362 | -# This program is distributed in the hope that it will be useful, |
2363 | -# but WITHOUT ANY WARRANTY; without even the implied warranty of |
2364 | -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
2365 | -# GNU General Public License for more details. |
2366 | -# |
2367 | -# You should have received a copy of the GNU General Public License |
2368 | -# along with this program. If not, see <http://www.gnu.org/licenses/>. |
2369 | - |
2370 | -import gtk |
2371 | -import time |
2372 | -import gobject |
2373 | -import gettext |
2374 | -import cairo |
2375 | -import pango |
2376 | -import math |
2377 | -import os |
2378 | -import urllib |
2379 | -from datetime import date |
2380 | - |
2381 | -from zeitgeist.client import ZeitgeistClient |
2382 | -from zeitgeist.datamodel import Event, Subject, Interpretation, Manifestation, \ |
2383 | - ResultType, TimeRange |
2384 | - |
2385 | -from common import shade_gdk_color, combine_gdk_color, get_gtk_rgba |
2386 | -from widgets import * |
2387 | -from thumb import ThumbBox |
2388 | -from timeline import TimelineView, TimelineHeader |
2389 | -from eventgatherer import get_dayevents, get_file_events |
2390 | - |
2391 | -CLIENT = ZeitgeistClient() |
2392 | - |
2393 | - |
2394 | -class GenericViewWidget(gtk.VBox): |
2395 | - __gsignals__ = { |
2396 | - "unfocus-day" : (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ()), |
2397 | - # Sends a list zeitgeist events |
2398 | - } |
2399 | - |
2400 | - def __init__(self): |
2401 | - gtk.VBox.__init__(self) |
2402 | - self.daylabel = None |
2403 | - self.connect("style-set", self.change_style) |
2404 | - |
2405 | - def _set_date_strings(self): |
2406 | - self.date_string = date.fromtimestamp(self.day_start).strftime("%d %B") |
2407 | - self.year_string = date.fromtimestamp(self.day_start).strftime("%Y") |
2408 | - if time.time() < self.day_end and time.time() > self.day_start: |
2409 | - self.week_day_string = _("Today") |
2410 | - elif time.time() - 86400 < self.day_end and time.time() - 86400> self.day_start: |
2411 | - self.week_day_string = _("Yesterday") |
2412 | - else: |
2413 | - self.week_day_string = date.fromtimestamp(self.day_start).strftime("%A") |
2414 | - self.emit("style-set", None) |
2415 | - |
2416 | - def click(self, widget, event): |
2417 | - if event.button in (1, 3): |
2418 | - self.emit("unfocus-day") |
2419 | - |
2420 | - def change_style(self, widget, style): |
2421 | - rc_style = self.style |
2422 | - color = rc_style.bg[gtk.STATE_NORMAL] |
2423 | - color = shade_gdk_color(color, 102/100.0) |
2424 | - self.view.modify_bg(gtk.STATE_NORMAL, color) |
2425 | - self.view.modify_base(gtk.STATE_NORMAL, color) |
2426 | - |
2427 | - |
2428 | -class ThumbnailDayWidget(GenericViewWidget): |
2429 | - |
2430 | - def __init__(self): |
2431 | - GenericViewWidget.__init__(self) |
2432 | - self.monitors = [] |
2433 | - self.scrolledwindow = gtk.ScrolledWindow() |
2434 | - self.scrolledwindow.set_shadow_type(gtk.SHADOW_NONE) |
2435 | - self.scrolledwindow.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) |
2436 | - self.view = ThumbBox() |
2437 | - self.scrolledwindow.add_with_viewport(self.view) |
2438 | - self.scrolledwindow.get_children()[0].set_shadow_type(gtk.SHADOW_NONE) |
2439 | - self.pack_end(self.scrolledwindow) |
2440 | - |
2441 | - def set_day(self, start, end): |
2442 | - |
2443 | - self.day_start = start |
2444 | - self.day_end = end |
2445 | - for widget in self: |
2446 | - if self.scrolledwindow != widget: |
2447 | - self.remove(widget) |
2448 | - self._set_date_strings() |
2449 | - today = int(time.time() ) - 7*86400 |
2450 | - if self.daylabel: |
2451 | - #Disconnect here |
2452 | - pass |
2453 | - if self.day_start < today: |
2454 | - self.daylabel = DayLabel(self.date_string, self.week_day_string+", "+ self.year_string) |
2455 | - else: |
2456 | - self.daylabel = DayLabel(self.week_day_string, self.date_string+", "+ self.year_string) |
2457 | - self.daylabel.set_size_request(100, 60) |
2458 | - self.daylabel.connect("button-press-event", self.click) |
2459 | - self.daylabel.set_tooltip_text(_("Click to return to multiday view")) |
2460 | - self.pack_start(self.daylabel, False, False) |
2461 | - self.show_all() |
2462 | - self.view.hide_all() |
2463 | - self.daylabel.show_all() |
2464 | - self.view.show() |
2465 | - |
2466 | - hour = 60*60 |
2467 | - get_file_events(start*1000, (start + 12*hour -1) * 1000, self.set_morning_events) |
2468 | - get_file_events((start + 12*hour)*1000, (start + 18*hour - 1)*1000, self.set_afternoon_events) |
2469 | - get_file_events((start + 18*hour)*1000, end*1000, self.set_evening_events) |
2470 | - |
2471 | - def set_morning_events(self, events): |
2472 | - if len(events) > 0: |
2473 | - timestamp = int(events[0].timestamp) |
2474 | - if self.day_start*1000 <= timestamp and timestamp < (self.day_start + 12*60*60)*1000: |
2475 | - self.view.set_morning_events(events) |
2476 | - self.view.views[0].show_all() |
2477 | - self.view.labels[0].show_all() |
2478 | - else: |
2479 | - self.view.set_morning_events(events) |
2480 | - self.view.views[0].hide_all() |
2481 | - self.view.labels[0].hide_all() |
2482 | - |
2483 | - def set_afternoon_events(self, events): |
2484 | - if len(events) > 0: |
2485 | - timestamp = int(events[0].timestamp) |
2486 | - if (self.day_start + 12*60*60)*1000 <= timestamp and timestamp < (self.day_start + 18*60*60)*1000: |
2487 | - self.view.set_afternoon_events(events) |
2488 | - self.view.views[1].show_all() |
2489 | - self.view.labels[1].show_all() |
2490 | - else: |
2491 | - self.view.set_afternoon_events(events) |
2492 | - self.view.views[1].hide_all() |
2493 | - self.view.labels[1].hide_all() |
2494 | - |
2495 | - def set_evening_events(self, events): |
2496 | - if len(events) > 0: |
2497 | - timestamp = int(events[0].timestamp) |
2498 | - if (self.day_start + 18*60*60)*1000 <= timestamp and timestamp < self.day_end*1000: |
2499 | - self.view.set_evening_events(events) |
2500 | - self.view.views[2].show_all() |
2501 | - self.view.labels[2].show_all() |
2502 | - else: |
2503 | - self.view.set_evening_events(events) |
2504 | - self.view.views[2].hide_all() |
2505 | - self.view.labels[2].hide_all() |
2506 | - |
2507 | - |
2508 | -class SingleDayWidget(GenericViewWidget): |
2509 | - |
2510 | - def __init__(self): |
2511 | - GenericViewWidget.__init__(self) |
2512 | - self.ruler = TimelineHeader() |
2513 | - self.scrolledwindow = gtk.ScrolledWindow() |
2514 | - self.scrolledwindow.set_shadow_type(gtk.SHADOW_NONE) |
2515 | - self.scrolledwindow.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_ALWAYS) |
2516 | - self.view = TimelineView() |
2517 | - self.scrolledwindow.add(self.view) |
2518 | - self.pack_end(self.scrolledwindow) |
2519 | - self.pack_end(self.ruler, False, False) |
2520 | - self.view.set_events(gtk.gdk.POINTER_MOTION_MASK | gtk.gdk.POINTER_MOTION_HINT_MASK) |
2521 | - |
2522 | - def set_day(self, start, end): |
2523 | - self.day_start = start |
2524 | - self.day_end = end |
2525 | - for widget in self: |
2526 | - if widget not in (self.ruler, self.scrolledwindow): |
2527 | - self.remove(widget) |
2528 | - self._set_date_strings() |
2529 | - today = int(time.time() ) - 7*86400 |
2530 | - if self.daylabel: |
2531 | - #Disconnect here |
2532 | - pass |
2533 | - if self.day_start < today: |
2534 | - self.daylabel = DayLabel(self.date_string, self.week_day_string+", "+ self.year_string) |
2535 | - else: |
2536 | - self.daylabel = DayLabel(self.week_day_string, self.date_string+", "+ self.year_string) |
2537 | - self.daylabel.set_size_request(100, 60) |
2538 | - self.daylabel.connect("button-press-event", self.click) |
2539 | - self.daylabel.set_tooltip_text(_("Click to return to multiday view")) |
2540 | - |
2541 | - self.pack_start(self.daylabel, False, False) |
2542 | - get_dayevents(start*1000, end*1000, 1, self.view.set_model_from_list) |
2543 | - self.show_all() |
2544 | - |
2545 | - def change_style(self, widget, style): |
2546 | - GenericViewWidget.change_style(self, widget, style) |
2547 | - rc_style = self.style |
2548 | - color = rc_style.bg[gtk.STATE_NORMAL] |
2549 | - color = shade_gdk_color(color, 102/100.0) |
2550 | - self.ruler.modify_bg(gtk.STATE_NORMAL, color) |
2551 | - |
2552 | - |
2553 | -class DayWidget(gtk.VBox): |
2554 | - |
2555 | - __gsignals__ = { |
2556 | - "focus-day" : (gobject.SIGNAL_RUN_FIRST, |
2557 | - gobject.TYPE_NONE, |
2558 | - (gobject.TYPE_INT,)) |
2559 | - } |
2560 | - |
2561 | - def __init__(self, start, end): |
2562 | - super(DayWidget, self).__init__() |
2563 | - hour = 60*60 |
2564 | - self.day_start = start |
2565 | - self.day_end = end |
2566 | - |
2567 | - self._set_date_strings() |
2568 | - self._periods = [ |
2569 | - (_("Morning"), start, start + 12*hour - 1), |
2570 | - (_("Afternoon"), start + 12*hour, start + 18*hour - 1), |
2571 | - (_("Evening"), start + 18*hour, end), |
2572 | - ] |
2573 | - |
2574 | - self._init_widgets() |
2575 | - self._init_pinbox() |
2576 | - gobject.timeout_add_seconds( |
2577 | - 86400 - (int(time.time() - time.timezone) % 86400), self._refresh) |
2578 | - |
2579 | - |
2580 | - self.show_all() |
2581 | - self._init_events() |
2582 | - |
2583 | - def refresh(self): |
2584 | - pass |
2585 | - |
2586 | - def _set_date_strings(self): |
2587 | - self.date_string = date.fromtimestamp(self.day_start).strftime("%d %B") |
2588 | - self.year_string = date.fromtimestamp(self.day_start).strftime("%Y") |
2589 | - if time.time() < self.day_end and time.time() > self.day_start: |
2590 | - self.week_day_string = _("Today") |
2591 | - elif time.time() - 86400 < self.day_end and time.time() - 86400> self.day_start: |
2592 | - self.week_day_string = _("Yesterday") |
2593 | - else: |
2594 | - self.week_day_string = date.fromtimestamp(self.day_start).strftime("%A") |
2595 | - self.emit("style-set", None) |
2596 | - |
2597 | - def _refresh(self): |
2598 | - self._init_date_label() |
2599 | - self._init_pinbox() |
2600 | - pinbox.show_all() |
2601 | - |
2602 | - def _init_pinbox(self): |
2603 | - if self.day_start <= time.time() < self.day_end: |
2604 | - self.view.pack_start(pinbox, False, False) |
2605 | - |
2606 | - def _init_widgets(self): |
2607 | - self.vbox = gtk.VBox() |
2608 | - self.pack_start(self.vbox) |
2609 | - |
2610 | - self.daylabel = None |
2611 | - |
2612 | - self._init_date_label() |
2613 | - |
2614 | - #label.modify_bg(gtk.STATE_SELECTED, style.bg[gtk.STATE_SELECTED]) |
2615 | - |
2616 | - self.view = gtk.VBox() |
2617 | - scroll = gtk.ScrolledWindow() |
2618 | - scroll.set_shadow_type(gtk.SHADOW_NONE) |
2619 | - |
2620 | - evbox2 = gtk.EventBox() |
2621 | - evbox2.add(self.view) |
2622 | - self.view.set_border_width(6) |
2623 | - |
2624 | - scroll.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) |
2625 | - scroll.add_with_viewport(evbox2) |
2626 | - for w in scroll.get_children(): |
2627 | - w.set_shadow_type(gtk.SHADOW_NONE) |
2628 | - self.vbox.pack_start(scroll) |
2629 | - self.show_all() |
2630 | - |
2631 | - def change_style(widget, style): |
2632 | - rc_style = self.style |
2633 | - color = rc_style.bg[gtk.STATE_NORMAL] |
2634 | - color = shade_gdk_color(color, 102/100.0) |
2635 | - evbox2.modify_bg(gtk.STATE_NORMAL, color) |
2636 | - |
2637 | - self.connect("style-set", change_style) |
2638 | - |
2639 | - def _init_date_label(self): |
2640 | - self._set_date_strings() |
2641 | - |
2642 | - today = int(time.time() ) - 7*86400 |
2643 | - if self.daylabel: |
2644 | - # Disconnect HERE |
2645 | - pass |
2646 | - if self.day_start < today: |
2647 | - self.daylabel = DayLabel(self.date_string, self.week_day_string+", "+ self.year_string) |
2648 | - else: |
2649 | - self.daylabel = DayLabel(self.week_day_string, self.date_string+", "+ self.year_string) |
2650 | - self.daylabel.connect("button-press-event", self.click) |
2651 | - self.daylabel.set_tooltip_text( |
2652 | - _("Left click for a detailed timeline view") |
2653 | - + u"\n" + |
2654 | - _("Right click for a thumbnail view")) |
2655 | - self.daylabel.set_size_request(100, 60) |
2656 | - evbox = gtk.EventBox() |
2657 | - evbox.add(self.daylabel) |
2658 | - evbox.set_size_request(100, 60) |
2659 | - self.vbox.pack_start(evbox, False, False) |
2660 | - def cursor_func(x, y): |
2661 | - if evbox.window: |
2662 | - evbox.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.HAND2)) |
2663 | - self.connect("motion-notify-event", cursor_func) |
2664 | - |
2665 | - def change_style(widget, style): |
2666 | - rc_style = self.style |
2667 | - color = rc_style.bg[gtk.STATE_NORMAL] |
2668 | - evbox.modify_bg(gtk.STATE_NORMAL, color) |
2669 | - self.daylabel.modify_bg(gtk.STATE_NORMAL, color) |
2670 | - |
2671 | - self.connect("style-set", change_style) |
2672 | - #self.connect("leave-notify-event", lambda x, y: evbox.window.set_cursor(None)) |
2673 | - |
2674 | - self.vbox.reorder_child(self.daylabel, 0) |
2675 | - |
2676 | - def click(self, widget, event): |
2677 | - if event.button == 1: |
2678 | - self.emit("focus-day", 1) |
2679 | - elif event.button == 3: |
2680 | - self.emit("focus-day", 2) |
2681 | - |
2682 | - def _init_events(self): |
2683 | - for w in self.view: |
2684 | - if not w == pinbox: |
2685 | - self.view.remove(w) |
2686 | - for period in self._periods: |
2687 | - part = DayPartWidget(period[0], period[1], period[2]) |
2688 | - self.view.pack_start(part, False, False) |
2689 | - |
2690 | - |
2691 | -class CategoryBox(gtk.HBox): |
2692 | - |
2693 | - def __init__(self, category, events, pinnable = False): |
2694 | - super(CategoryBox, self).__init__() |
2695 | - self.view = gtk.VBox(True) |
2696 | - self.vbox = gtk.VBox() |
2697 | - for event in events: |
2698 | - item = Item(event, pinnable) |
2699 | - hbox = gtk.HBox () |
2700 | - #label = gtk.Label("") |
2701 | - #hbox.pack_start(label, False, False, 7) |
2702 | - hbox.pack_start(item, True, True, 0) |
2703 | - self.view.pack_start(hbox, False, False, 0) |
2704 | - hbox.show() |
2705 | - #label.show() |
2706 | - |
2707 | - # If this isn't a set of ungrouped events, give it a label |
2708 | - if category: |
2709 | - # Place the items into a box and simulate left padding |
2710 | - self.box = gtk.HBox() |
2711 | - #label = gtk.Label("") |
2712 | - self.box.pack_start(self.view) |
2713 | - |
2714 | - hbox = gtk.HBox() |
2715 | - # Add the title button |
2716 | - if category in SUPPORTED_SOURCES: |
2717 | - text = SUPPORTED_SOURCES[category].group_label(len(events)) |
2718 | - else: |
2719 | - text = "Unknown" |
2720 | - |
2721 | - label = gtk.Label() |
2722 | - label.set_markup("<span>%s</span>" % text) |
2723 | - #label.set_ellipsize(pango.ELLIPSIZE_END) |
2724 | - |
2725 | - hbox.pack_start(label, True, True, 0) |
2726 | - |
2727 | - label = gtk.Label() |
2728 | - label.set_markup("<span>(%d)</span>" % len(events)) |
2729 | - label.set_alignment(1.0,0.5) |
2730 | - label.set_alignment(1.0,0.5) |
2731 | - hbox.pack_end(label, False, False, 2) |
2732 | - |
2733 | - hbox.set_border_width(3) |
2734 | - |
2735 | - self.expander = gtk.Expander() |
2736 | - self.expander.set_label_widget(hbox) |
2737 | - |
2738 | - self.vbox.pack_start(self.expander, False, False) |
2739 | - self.expander.add(self.box)# |
2740 | - |
2741 | - self.pack_start(self.vbox, True, True, 24) |
2742 | - |
2743 | - self.expander.show_all() |
2744 | - self.show() |
2745 | - hbox.show_all() |
2746 | - label.show_all() |
2747 | - self.view.show() |
2748 | - |
2749 | - else: |
2750 | - self.box = self.view |
2751 | - self.vbox.pack_end(self.box) |
2752 | - self.box.show() |
2753 | - self.show() |
2754 | - |
2755 | - self.pack_start(self.vbox, True, True, 16) |
2756 | - |
2757 | - self.show_all() |
2758 | - |
2759 | - def on_toggle(self, view, bool): |
2760 | - if bool: |
2761 | - self.box.show() |
2762 | - else: |
2763 | - self.box.hide() |
2764 | - pinbox.show_all() |
2765 | - |
2766 | - |
2767 | -class DayLabel(gtk.DrawingArea): |
2768 | - |
2769 | - _events = ( |
2770 | - gtk.gdk.ENTER_NOTIFY_MASK | gtk.gdk.LEAVE_NOTIFY_MASK | |
2771 | - gtk.gdk.KEY_PRESS_MASK | gtk.gdk.BUTTON_MOTION_MASK | |
2772 | - gtk.gdk.POINTER_MOTION_HINT_MASK | gtk.gdk.BUTTON_RELEASE_MASK | |
2773 | - gtk.gdk.BUTTON_PRESS_MASK |
2774 | - ) |
2775 | - |
2776 | - def __init__(self, day, date): |
2777 | - if day == _("Today"): |
2778 | - self.leading = True |
2779 | - else: |
2780 | - self.leading = False |
2781 | - super(DayLabel, self).__init__() |
2782 | - self.date = date |
2783 | - self.day = day |
2784 | - self.set_events(self._events) |
2785 | - self.connect("expose_event", self.expose) |
2786 | - self.connect("enter-notify-event", self._on_enter) |
2787 | - self.connect("leave-notify-event", self._on_leave) |
2788 | - |
2789 | - def _on_enter(self, widget, event): |
2790 | - widget.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.HAND2)) |
2791 | - |
2792 | - def _on_leave(self, widget, event): |
2793 | - widget.window.set_cursor(None) |
2794 | - |
2795 | - def expose(self, widget, event): |
2796 | - context = widget.window.cairo_create() |
2797 | - self.context = context |
2798 | - |
2799 | - bg = self.style.bg[0] |
2800 | - red, green, blue = bg.red/65535.0, bg.green/65535.0, bg.blue/65535.0 |
2801 | - self.font_name = self.style.font_desc.get_family() |
2802 | - |
2803 | - widget.style.set_background(widget.window, gtk.STATE_NORMAL) |
2804 | - |
2805 | - # set a clip region for the expose event |
2806 | - context.rectangle(event.area.x, event.area.y, event.area.width, event.area.height) |
2807 | - context.clip() |
2808 | - self.draw(widget, event, context) |
2809 | - self.day_text(widget, event, context) |
2810 | - return False |
2811 | - |
2812 | - def day_text(self, widget, event, context): |
2813 | - actual_y = self.get_size_request()[1] |
2814 | - if actual_y > event.area.height: |
2815 | - y = actual_y |
2816 | - else: |
2817 | - y = event.area.height |
2818 | - x = event.area.width |
2819 | - gc = self.style.fg_gc[gtk.STATE_SELECTED if self.leading else gtk.STATE_NORMAL] |
2820 | - layout = widget.create_pango_layout(self.day) |
2821 | - layout.set_font_description(pango.FontDescription(self.font_name + " Bold 15")) |
2822 | - w, h = layout.get_pixel_size() |
2823 | - widget.window.draw_layout(gc, (x-w)/2, (y)/2 - h + 5, layout) |
2824 | - self.date_text(widget, event, context, (y)/2 + 5) |
2825 | - |
2826 | - def date_text(self, widget, event, context, lastfontheight): |
2827 | - gc = self.style.fg_gc[gtk.STATE_SELECTED if self.leading else gtk.STATE_INSENSITIVE] |
2828 | - layout = widget.create_pango_layout(self.date) |
2829 | - layout.set_font_description(pango.FontDescription(self.font_name + " 10")) |
2830 | - w, h = layout.get_pixel_size() |
2831 | - widget.window.draw_layout(gc, (event.area.width-w)/2, lastfontheight, layout) |
2832 | - |
2833 | - def draw(self, widget, event, context): |
2834 | - if self.leading: |
2835 | - bg = self.style.bg[gtk.STATE_SELECTED] |
2836 | - red, green, blue = bg.red/65535.0, bg.green/65535.0, bg.blue/65535.0 |
2837 | - else: |
2838 | - bg = self.style.bg[gtk.STATE_NORMAL] |
2839 | - red = (bg.red * 125 / 100)/65535.0 |
2840 | - green = (bg.green * 125 / 100)/65535.0 |
2841 | - blue = (bg.blue * 125 / 100)/65535.0 |
2842 | - x = 0; y = 0 |
2843 | - r = 5 |
2844 | - w, h = event.area.width, event.area.height |
2845 | - context.set_source_rgba(red, green, blue, 1) |
2846 | - context.new_sub_path() |
2847 | - context.arc(r+x, r+y, r, math.pi, 3 * math.pi /2) |
2848 | - context.arc(w-r, r+y, r, 3 * math.pi / 2, 0) |
2849 | - context.close_path() |
2850 | - context.rectangle(0, r, w, h) |
2851 | - context.fill_preserve() |
2852 | - |
2853 | - |
2854 | -class DayButton(gtk.DrawingArea): |
2855 | - leading = False |
2856 | - pressed = False |
2857 | - sensitive = True |
2858 | - hover = False |
2859 | - header_size = 60 |
2860 | - bg_color = (0, 0, 0, 0) |
2861 | - header_color = (1, 1, 1, 1) |
2862 | - leading_header_color = (1, 1, 1, 1) |
2863 | - internal_color = (0, 1, 0, 1) |
2864 | - arrow_color = (1,1,1,1) |
2865 | - arrow_color_selected = (1, 1, 1, 1) |
2866 | - |
2867 | - __gsignals__ = { |
2868 | - "clicked": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,()), |
2869 | - } |
2870 | - _events = ( |
2871 | - gtk.gdk.ENTER_NOTIFY_MASK | gtk.gdk.LEAVE_NOTIFY_MASK | |
2872 | - gtk.gdk.KEY_PRESS_MASK | gtk.gdk.BUTTON_RELEASE_MASK | gtk.gdk.BUTTON_PRESS_MASK | |
2873 | - gtk.gdk.MOTION_NOTIFY | gtk.gdk.POINTER_MOTION_MASK |
2874 | - ) |
2875 | - def __init__(self, side = 0, leading = False): |
2876 | - super(DayButton, self).__init__() |
2877 | - self.set_events(self._events) |
2878 | - self.set_flags(gtk.CAN_FOCUS) |
2879 | - self.leading = leading |
2880 | - self.side = side |
2881 | - self.connect("button_press_event", self.on_press) |
2882 | - self.connect("button_release_event", self.clicked_sender) |
2883 | - self.connect("key_press_event", self.keyboard_clicked_sender) |
2884 | - self.connect("motion_notify_event", self.on_hover) |
2885 | - self.connect("leave_notify_event", self._enter_leave_notify, False) |
2886 | - self.connect("expose_event", self.expose) |
2887 | - self.connect("style-set", self.change_style) |
2888 | - self.set_size_request(20, -1) |
2889 | - |
2890 | - def set_sensitive(self, case): |
2891 | - self.sensitive = case |
2892 | - self.queue_draw() |
2893 | - |
2894 | - def _enter_leave_notify(self, widget, event, bol): |
2895 | - self.hover = bol |
2896 | - self.queue_draw() |
2897 | - |
2898 | - def on_hover(self, widget, event): |
2899 | - if event.y > self.header_size: |
2900 | - if not self.hover: |
2901 | - self.hover = True |
2902 | - self.queue_draw() |
2903 | - else: |
2904 | - if self.hover: |
2905 | - self.hover = False |
2906 | - self.queue_draw() |
2907 | - return False |
2908 | - |
2909 | - def on_press(self, widget, event): |
2910 | - if event.y > self.header_size: |
2911 | - self.pressed = True |
2912 | - self.queue_draw() |
2913 | - |
2914 | - def keyboard_clicked_sender(self, widget, event): |
2915 | - if event.keyval in (gtk.keysyms.Return, gtk.keysyms.space): |
2916 | - if self.sensitive: |
2917 | - self.emit("clicked") |
2918 | - self.pressed = False |
2919 | - self.queue_draw() |
2920 | - return True |
2921 | - return False |
2922 | - |
2923 | - def clicked_sender(self, widget, event): |
2924 | - if event.y > self.header_size: |
2925 | - if self.sensitive: |
2926 | - self.emit("clicked") |
2927 | - self.pressed = False |
2928 | - self.queue_draw() |
2929 | - return True |
2930 | - |
2931 | - def change_style(self, *args, **kwargs): |
2932 | - self.bg_color = get_gtk_rgba(self.style, "bg", 0) |
2933 | - self.header_color = get_gtk_rgba(self.style, "bg", 0, 1.25) |
2934 | - self.leading_header_color = get_gtk_rgba(self.style, "bg", 3) |
2935 | - self.internal_color = get_gtk_rgba(self.style, "bg", 0, 1.02) |
2936 | - self.arrow_color = get_gtk_rgba(self.style, "text", 0, 0.6) |
2937 | - self.arrow_color_selected = get_gtk_rgba(self.style, "bg", 3) |
2938 | - self.arrow_color_insensitive = get_gtk_rgba(self.style, "text", 4) |
2939 | - |
2940 | - def expose(self, widget, event): |
2941 | - context = widget.window.cairo_create() |
2942 | - |
2943 | - context.set_source_rgba(*self.bg_color) |
2944 | - context.set_operator(cairo.OPERATOR_SOURCE) |
2945 | - context.paint() |
2946 | - context.rectangle(event.area.x, event.area.y, event.area.width, event.area.height) |
2947 | - context.clip() |
2948 | - |
2949 | - x = 0; y = 0 |
2950 | - r = 5 |
2951 | - w, h = event.area.width, event.area.height |
2952 | - size = 20 |
2953 | - if self.sensitive: |
2954 | - context.set_source_rgba(*(self.leading_header_color if self.leading else self.header_color)) |
2955 | - context.new_sub_path() |
2956 | - context.move_to(x+r,y) |
2957 | - context.line_to(x+w-r,y) |
2958 | - context.curve_to(x+w,y,x+w,y,x+w,y+r) |
2959 | - context.line_to(x+w,y+h-r) |
2960 | - context.curve_to(x+w,y+h,x+w,y+h,x+w-r,y+h) |
2961 | - context.line_to(x+r,y+h) |
2962 | - context.curve_to(x,y+h,x,y+h,x,y+h-r) |
2963 | - context.line_to(x,y+r) |
2964 | - context.curve_to(x,y,x,y,x+r,y) |
2965 | - context.set_source_rgba(*(self.leading_header_color if self.leading else self.header_color)) |
2966 | - context.close_path() |
2967 | - context.rectangle(0, r, w, self.header_size) |
2968 | - context.fill() |
2969 | - context.set_source_rgba(*self.internal_color) |
2970 | - context.rectangle(0, self.header_size, w, h) |
2971 | - context.fill() |
2972 | - if self.hover: |
2973 | - widget.style.paint_box(widget.window, gtk.STATE_PRELIGHT, gtk.SHADOW_OUT, |
2974 | - event.area, widget, "button", |
2975 | - event.area.x, self.header_size, |
2976 | - w, h-self.header_size) |
2977 | - size = 10 |
2978 | - if not self.sensitive: |
2979 | - state = gtk.STATE_INSENSITIVE |
2980 | - elif self.is_focus() or self.pressed: |
2981 | - widget.style.paint_focus(widget.window, gtk.STATE_ACTIVE, event.area, |
2982 | - widget, None, event.area.x, self.header_size, |
2983 | - w, h-self.header_size) |
2984 | - state = gtk.STATE_SELECTED |
2985 | - else: |
2986 | - state = gtk.STATE_NORMAL |
2987 | - arrow = gtk.ARROW_RIGHT if self.side else gtk.ARROW_LEFT |
2988 | - self.style.paint_arrow(widget.window, state, gtk.SHADOW_NONE, None, |
2989 | - self, "arrow", arrow, True, |
2990 | - w/2-size/2, h/2 + size/2, size, size) |
2991 | - |
2992 | - |
2993 | -class EventGroup(gtk.VBox): |
2994 | - |
2995 | - def __init__(self, title): |
2996 | - super(EventGroup, self).__init__() |
2997 | - |
2998 | - # Create the title label |
2999 | - self.label = gtk.Label(title) |
3000 | - self.label.set_alignment(0.03, 0.5) |
3001 | - self.pack_start(self.label, False, False, 6) |
3002 | - self.events = [] |
3003 | - # Create the main container |
3004 | - self.view = gtk.VBox() |
3005 | - self.pack_start(self.view) |
3006 | - |
3007 | - # Connect to relevant signals |
3008 | - self.connect("style-set", self.on_style_change) |
3009 | - |
3010 | - # Populate the widget with content |
3011 | - self.get_events() |
3012 | - |
3013 | - def on_style_change(self, widget, style): |
3014 | - """ Update used colors according to the system theme. """ |
3015 | - color = self.style.bg[gtk.STATE_NORMAL] |
3016 | - fcolor = self.style.fg[gtk.STATE_NORMAL] |
3017 | - color = combine_gdk_color(color, fcolor) |
3018 | - self.label.modify_fg(gtk.STATE_NORMAL, color) |
3019 | - |
3020 | - @staticmethod |
3021 | - def event_exists(uri): |
3022 | - # TODO: Move this into Zeitgeist's datamodel.py |
3023 | - return not uri.startswith("file://") or os.path.exists( |
3024 | - urllib.unquote(str(uri[7:]))) |
3025 | - |
3026 | - def set_events(self, events): |
3027 | - self.events = [] |
3028 | - for widget in self.view: |
3029 | - self.view.remove(widget) |
3030 | - |
3031 | - if self == pinbox: |
3032 | - box = CategoryBox(None, events, True) |
3033 | - self.view.pack_start(box) |
3034 | - else: |
3035 | - categories = {} |
3036 | - for event in events: |
3037 | - subject = event.subjects[0] |
3038 | - if self.event_exists(subject.uri): |
3039 | - if not categories.has_key(subject.interpretation): |
3040 | - categories[subject.interpretation] = [] |
3041 | - categories[subject.interpretation].append(event) |
3042 | - self.events.append(event) |
3043 | - |
3044 | - if not categories: |
3045 | - self.hide_all() |
3046 | - else: |
3047 | - # Make the group title, etc. visible |
3048 | - self.show_all() |
3049 | - |
3050 | - ungrouped_events = [] |
3051 | - for key in sorted(categories.iterkeys()): |
3052 | - events = categories[key] |
3053 | - if len(events) > 3: |
3054 | - box = CategoryBox(key, list(reversed(events))) |
3055 | - self.view.pack_start(box) |
3056 | - else: |
3057 | - ungrouped_events += events |
3058 | - |
3059 | - ungrouped_events.sort(key=lambda x: x.timestamp) |
3060 | - box = CategoryBox(None, ungrouped_events) |
3061 | - self.view.pack_start(box) |
3062 | - |
3063 | - # Make the group's contents visible |
3064 | - self.view.show() |
3065 | - pinbox.show_all() |
3066 | - |
3067 | - if len(self.events) == 0: |
3068 | - self.hide() |
3069 | - else: |
3070 | - self.show() |
3071 | - |
3072 | - def get_events(self, *discard): |
3073 | - if self.event_templates and len(self.event_templates) > 0: |
3074 | - CLIENT.find_events_for_templates(self.event_templates, |
3075 | - self.set_events, self.event_timerange, num_events=50000, |
3076 | - result_type=ResultType.MostRecentSubjects) |
3077 | - else: |
3078 | - self.view.hide() |
3079 | - |
3080 | - |
3081 | -class DayPartWidget(EventGroup): |
3082 | - |
3083 | - def __init__(self, title, start, end): |
3084 | - # Setup event criteria for querying |
3085 | - self.event_timerange = [start * 1000, end * 1000] |
3086 | - self.event_templates = ( |
3087 | - Event.new_for_values(interpretation=Interpretation.VISIT_EVENT.uri), |
3088 | - Event.new_for_values(interpretation=Interpretation.MODIFY_EVENT.uri), |
3089 | - Event.new_for_values(interpretation=Interpretation.CREATE_EVENT.uri), |
3090 | - Event.new_for_values(interpretation=Interpretation.OPEN_EVENT.uri), |
3091 | - ) |
3092 | - |
3093 | - # Initialize the widget |
3094 | - super(DayPartWidget, self).__init__(title) |
3095 | - |
3096 | - # FIXME: Move this into EventGroup |
3097 | - CLIENT.install_monitor(self.event_timerange, self.event_templates, |
3098 | - self.notify_insert_handler, self.notify_delete_handler) |
3099 | - |
3100 | - def notify_insert_handler(self, time_range, events): |
3101 | - # FIXME: Don't regenerate everything, we already get the |
3102 | - # information we need |
3103 | - self.get_events() |
3104 | - |
3105 | - def notify_delete_handler(self, time_range, event_ids): |
3106 | - # FIXME: Same as above |
3107 | - self.get_events() |
3108 | - |
3109 | -class PinBox(EventGroup): |
3110 | - |
3111 | - def __init__(self): |
3112 | - # Setup event criteria for querying |
3113 | - self.event_timerange = TimeRange.until_now() |
3114 | - |
3115 | - # Initialize the widget |
3116 | - super(PinBox, self).__init__(_("Pinned items")) |
3117 | - |
3118 | - # Connect to relevant signals |
3119 | - bookmarker.connect("reload", self.get_events) |
3120 | - |
3121 | - @property |
3122 | - def event_templates(self): |
3123 | - if not bookmarker.bookmarks: |
3124 | - # Abort, or we will query with no templates and get lots of |
3125 | - # irrelevant events. |
3126 | - return None |
3127 | - |
3128 | - templates = [] |
3129 | - for bookmark in bookmarker.bookmarks: |
3130 | - templates.append(Event.new_for_values(subject_uri=bookmark)) |
3131 | - return templates |
3132 | - |
3133 | - def set_events(self, *args, **kwargs): |
3134 | - super(PinBox, self).set_events(*args, **kwargs) |
3135 | - # Make the pin icons visible |
3136 | - self.view.show_all() |
3137 | - self.show_all() |
3138 | - |
3139 | -pinbox = PinBox() |
3140 | |
3141 | === removed file 'src/eventgatherer.py' |
3142 | --- src/eventgatherer.py 2010-04-07 03:17:49 +0000 |
3143 | +++ src/eventgatherer.py 1970-01-01 00:00:00 +0000 |
3144 | @@ -1,224 +0,0 @@ |
3145 | -# -.- coding: utf-8 -.- |
3146 | -# |
3147 | -# GNOME Activity Journal |
3148 | -# |
3149 | -# Copyright © 2010 Seif Lotfy <seif@lotfy.com> |
3150 | -# Copyright © 2010 Siegfried Gevatter <siegfried@gevatter.com> |
3151 | -# |
3152 | -# This program is free software: you can redistribute it and/or modify |
3153 | -# it under the terms of the GNU General Public License as published by |
3154 | -# the Free Software Foundation, either version 3 of the License, or |
3155 | -# (at your option) any later version. |
3156 | -# |
3157 | -# This program is distributed in the hope that it will be useful, |
3158 | -# but WITHOUT ANY WARRANTY; without even the implied warranty of |
3159 | -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
3160 | -# GNU General Public License for more details. |
3161 | -# |
3162 | -# You should have received a copy of the GNU General Public License |
3163 | -# along with this program. If not, see <http://www.gnu.org/licenses/>. |
3164 | - |
3165 | -import time |
3166 | -from datetime import timedelta, datetime |
3167 | -import gtk |
3168 | -import random |
3169 | -import os |
3170 | -import urllib |
3171 | -from zeitgeist.client import ZeitgeistClient |
3172 | -from zeitgeist.datamodel import Event, Subject, Interpretation, Manifestation, \ |
3173 | - ResultType, TimeRange, StorageState |
3174 | - |
3175 | -CLIENT = ZeitgeistClient() |
3176 | - |
3177 | -event_templates = ( |
3178 | - Event.new_for_values(interpretation=Interpretation.VISIT_EVENT.uri), |
3179 | - Event.new_for_values(interpretation=Interpretation.MODIFY_EVENT.uri), |
3180 | - Event.new_for_values(interpretation=Interpretation.CREATE_EVENT.uri), |
3181 | - Event.new_for_values(interpretation=Interpretation.OPEN_EVENT.uri), |
3182 | - Event.new_for_values(interpretation=Interpretation.CLOSE_EVENT.uri), |
3183 | -) |
3184 | - |
3185 | -EVENTS = {} |
3186 | - |
3187 | -def event_exists(uri): |
3188 | - # TODO: Move this into Zeitgeist's datamodel.py |
3189 | - if uri.startswith("trash://"): |
3190 | - return False |
3191 | - return not uri.startswith("file://") or os.path.exists( |
3192 | - urllib.unquote(str(uri[7:]))) |
3193 | - |
3194 | -def get_dayevents(start, end, result_type, callback, force = False): |
3195 | - """ |
3196 | - :param start: a int time from which to start gathering events in milliseconds |
3197 | - :param end: a int time from which to stop gathering events in milliseconds |
3198 | - :callback: a callable to be called when the query is done |
3199 | - """ |
3200 | - def handle_find_events(events): |
3201 | - results = {} |
3202 | - for event in events: |
3203 | - uri = event.subjects[0].uri |
3204 | - if not event.subjects[0].uri in results: |
3205 | - results[uri] = [] |
3206 | - if not event.interpretation == Interpretation.CLOSE_EVENT.uri: |
3207 | - results[uri].append([event, 0]) |
3208 | - else: |
3209 | - if not len(results[uri]) == 0: |
3210 | - #print "***", results[uri] |
3211 | - results[uri][len(results[uri])-1][1] = (int(event.timestamp)) - int(results[uri][-1][0].timestamp) |
3212 | - else: |
3213 | - tend = int(event.timestamp) |
3214 | - event.timestamp = str(start) |
3215 | - results[uri].append([event, tend - start]) |
3216 | - events = list(sorted(results.itervalues(), key=lambda r: \ |
3217 | - r[0][0].timestamp)) |
3218 | - EVENTS[start+end] = events |
3219 | - callback(events) |
3220 | - |
3221 | - def notify_insert_handler_morning(timerange, events): |
3222 | - find_events() |
3223 | - |
3224 | - def find_events(): |
3225 | - event_templates = [] |
3226 | - CLIENT.find_events_for_templates(event_templates, handle_find_events, |
3227 | - [start, end], num_events=50000, |
3228 | - result_type=result_type) |
3229 | - |
3230 | - if not EVENTS.has_key(start+end) or force: |
3231 | - find_events() |
3232 | - event_timerange = [start, end] |
3233 | - event_templates = ( |
3234 | - Event.new_for_values(interpretation=Interpretation.VISIT_EVENT.uri), |
3235 | - Event.new_for_values(interpretation=Interpretation.MODIFY_EVENT.uri), |
3236 | - Event.new_for_values(interpretation=Interpretation.CREATE_EVENT.uri), |
3237 | - Event.new_for_values(interpretation=Interpretation.OPEN_EVENT.uri), |
3238 | - ) |
3239 | - # FIXME: Move this into EventGroup |
3240 | - |
3241 | - CLIENT.install_monitor([start, end], event_templates, |
3242 | - notify_insert_handler_morning, notify_insert_handler_morning) |
3243 | - |
3244 | - |
3245 | - |
3246 | - else: |
3247 | - callback(EVENTS[start+end]) |
3248 | - |
3249 | - |
3250 | - |
3251 | -def get_file_events(start, end, callback, force = False): |
3252 | - """ |
3253 | - :param start: a int time from which to start gathering events in milliseconds |
3254 | - :param end: a int time from which to stop gathering events in milliseconds |
3255 | - :callback: a callable to be called when the query is done |
3256 | - """ |
3257 | - |
3258 | - def handle_find_events(events): |
3259 | - results = {} |
3260 | - for event in events: |
3261 | - uri = event.subjects[0].uri |
3262 | - if not event.subjects[0].uri in results: |
3263 | - results[uri] = [] |
3264 | - results[uri].append(event) |
3265 | - events = [result[0] for result in results.values()] |
3266 | - EVENTS[start+end] = events |
3267 | - callback(events) |
3268 | - |
3269 | - def notify_insert_handler_morning(timerange, events): |
3270 | - find_events() |
3271 | - |
3272 | - event_templates = ( |
3273 | - #Event.new_for_values(interpretation=Interpretation.VISIT_EVENT.uri), |
3274 | - #Event.new_for_values(interpretation=Interpretation.MODIFY_EVENT.uri), |
3275 | - #Event.new_for_values(interpretation=Interpretation.CREATE_EVENT.uri), |
3276 | - #Event.new_for_values(interpretation=Interpretation.OPEN_EVENT.uri), |
3277 | - ) |
3278 | - |
3279 | - def find_events(): |
3280 | - CLIENT.find_events_for_templates(event_templates, handle_find_events, |
3281 | - [start, end], num_events=50000, |
3282 | - result_type=ResultType.LeastRecentEvents) |
3283 | - |
3284 | - if not EVENTS.has_key(start+end) or force: |
3285 | - find_events() |
3286 | - event_timerange = [start, end] |
3287 | - |
3288 | - # FIXME: Move this into EventGroup |
3289 | - |
3290 | - CLIENT.install_monitor([start, end], event_templates, |
3291 | - notify_insert_handler_morning, notify_insert_handler_morning) |
3292 | - |
3293 | - |
3294 | - |
3295 | - else: |
3296 | - callback(EVENTS[start+end]) |
3297 | - |
3298 | - |
3299 | -def datelist(n, callback): |
3300 | - """ |
3301 | - :param n: number of days to query |
3302 | - :callback: a callable to be called when the query is done |
3303 | - """ |
3304 | - event_templates = ( |
3305 | - Event.new_for_values(interpretation=Interpretation.VISIT_EVENT.uri, |
3306 | - storage_state=StorageState.Available), |
3307 | - Event.new_for_values(interpretation=Interpretation.MODIFY_EVENT.uri, |
3308 | - storage_state=StorageState.Available), |
3309 | - Event.new_for_values(interpretation=Interpretation.CREATE_EVENT.uri, |
3310 | - storage_state=StorageState.Available), |
3311 | - Event.new_for_values(interpretation=Interpretation.OPEN_EVENT.uri, |
3312 | - storage_state=StorageState.Available), |
3313 | - Event.new_for_values(interpretation=Interpretation.CLOSE_EVENT.uri, |
3314 | - storage_state=StorageState.Available), |
3315 | - ) |
3316 | - if n == -1: |
3317 | - n = int(time.time()/86400) |
3318 | - today = int(time.mktime(time.strptime(time.strftime("%d %B %Y"), |
3319 | - "%d %B %Y"))) |
3320 | - today = today - n*86400 |
3321 | - |
3322 | - x = [] |
3323 | - |
3324 | - def _handle_find_events(ids): |
3325 | - x.append((today+len(x)*86400, len(ids))) |
3326 | - if len(x) == n+1: |
3327 | - callback(x) |
3328 | - |
3329 | - def get_ids(start, end): |
3330 | - CLIENT.find_event_ids_for_templates(event_templates, |
3331 | - _handle_find_events, [start * 1000, end * 1000], |
3332 | - num_events=50000, |
3333 | - result_type=0,) |
3334 | - |
3335 | - for i in xrange(n+1): |
3336 | - get_ids(today+i*86400, today+i*86400+86399) |
3337 | - |
3338 | - |
3339 | -def get_related_events_for_uri(uri, callback): |
3340 | - """ |
3341 | - :param uri: A uri for which to request related uris using zetigeist |
3342 | - :param callback: this callback is called once the events are retrieved for |
3343 | - the uris. It is called with a list of events. |
3344 | - """ |
3345 | - def _event_request_handler(uris): |
3346 | - """ |
3347 | - :param uris: a list of uris which are related to the windows current uri |
3348 | - Seif look here |
3349 | - """ |
3350 | - templates = [] |
3351 | - if len(uris) > 0: |
3352 | - for i, uri in enumerate(uris): |
3353 | - templates += [ |
3354 | - Event.new_for_values(interpretation=Interpretation.VISIT_EVENT.uri, subject_uri=uri), |
3355 | - Event.new_for_values(interpretation=Interpretation.MODIFY_EVENT.uri, subject_uri=uri), |
3356 | - Event.new_for_values(interpretation=Interpretation.CREATE_EVENT.uri, subject_uri=uri), |
3357 | - Event.new_for_values(interpretation=Interpretation.OPEN_EVENT.uri, subject_uri=uri) |
3358 | - ] |
3359 | - CLIENT.find_events_for_templates(templates, callback, |
3360 | - [0, time.time()*1000], num_events=50000, |
3361 | - result_type=ResultType.MostRecentSubjects) |
3362 | - |
3363 | - end = time.time() * 1000 |
3364 | - start = end - (86400*30*1000) |
3365 | - CLIENT.find_related_uris_for_uris([uri], _event_request_handler) |
3366 | - |
3367 | - |
3368 | - |
3369 | |
3370 | === added file 'src/histogram.py' |
3371 | --- src/histogram.py 1970-01-01 00:00:00 +0000 |
3372 | +++ src/histogram.py 2010-05-04 05:28:16 +0000 |
3373 | @@ -0,0 +1,532 @@ |
3374 | +# -.- coding: utf-8 -.- |
3375 | +# |
3376 | +# GNOME Activity Journal |
3377 | +# |
3378 | +# Copyright © 2010 Randal Barlow <email.tehk@gmail.com> |
3379 | +# Copyright © 2010 Markus Korn |
3380 | +# Copyright © 2010 Siegfried Gevatter <siegfried@gevatter.com> |
3381 | +# |
3382 | +# This program is free software: you can redistribute it and/or modify |
3383 | +# it under the terms of the GNU General Public License as published by |
3384 | +# the Free Software Foundation, either version 3 of the License, or |
3385 | +# (at your option) any later version. |
3386 | +# |
3387 | +# This program is distributed in the hope that it will be useful, |
3388 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
3389 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
3390 | +# GNU General Public License for more details. |
3391 | +# |
3392 | +# You should have received a copy of the GNU General Public License |
3393 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
3394 | + |
3395 | +import datetime |
3396 | +import cairo |
3397 | +import calendar |
3398 | +import gettext |
3399 | +import gobject |
3400 | +import gtk |
3401 | +from math import pi as PI |
3402 | +import pango |
3403 | +from common import * |
3404 | + |
3405 | + |
3406 | +def get_gc_from_colormap(widget, shade): |
3407 | + """ |
3408 | + Gets a gtk.gdk.GC and modifies the color by shade |
3409 | + """ |
3410 | + gc = widget.style.text_gc[gtk.STATE_INSENSITIVE] |
3411 | + if gc: |
3412 | + color = widget.style.text[4] |
3413 | + color = shade_gdk_color(color, shade) |
3414 | + gc.set_rgb_fg_color(color) |
3415 | + return gc |
3416 | + |
3417 | + |
3418 | +class CairoHistogram(gtk.DrawingArea): |
3419 | + """ |
3420 | + A histogram which is represented by a list of dates, and nitems. |
3421 | + |
3422 | + There are a few maintenance issues due to the movement abilities. The widget |
3423 | + currently is able to capture motion events when the mouse is outside |
3424 | + the widget and the button is pressed if it was initially pressed inside |
3425 | + the widget. This event mask magic leaves a few flaws open. |
3426 | + """ |
3427 | + _selected = (0,) |
3428 | + padding = 2 |
3429 | + bottom_padding = 23 |
3430 | + top_padding = 2 |
3431 | + wcolumn = 12 |
3432 | + xincrement = wcolumn + padding |
3433 | + start_x_padding = 2 |
3434 | + max_width = xincrement |
3435 | + column_radius = 0 |
3436 | + stroke_width = 1 |
3437 | + stroke_offset = 0 |
3438 | + min_column_height = 4 |
3439 | + max_column_height = 101 |
3440 | + gc = None |
3441 | + pangofont = None |
3442 | + _disable_mouse_motion = False |
3443 | + selected_range = 0 |
3444 | + _highlighted = tuple() |
3445 | + _last_location = -1 |
3446 | + _single_day_only = False |
3447 | + colors = { |
3448 | + "bg" : (1, 1, 1, 1), |
3449 | + "base" : (1, 1, 1, 1), |
3450 | + "column_normal" : (1, 1, 1, 1), |
3451 | + "column_selected" : (1, 1, 1, 1), |
3452 | + "column_alternative" : (1, 1, 1, 1), |
3453 | + "column_selected_alternative" : (1, 1, 1, 1), |
3454 | + "font_color" : "#ffffff", |
3455 | + "stroke" : (1, 1, 1, 0), |
3456 | + "shadow" : (1, 1, 1, 0), |
3457 | + } |
3458 | + |
3459 | + _store = None |
3460 | + |
3461 | + __gsignals__ = { |
3462 | + "selection-set" : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, |
3463 | + (gobject.TYPE_PYOBJECT,)), |
3464 | + "data-updated" : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,()), |
3465 | + "column_clicked" : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, |
3466 | + (gobject.TYPE_PYOBJECT,)) |
3467 | + } |
3468 | + _connections = {"style-set": "change_style", |
3469 | + "expose_event": "_expose", |
3470 | + "button_press_event": "mouse_press_interaction", |
3471 | + "motion_notify_event": "mouse_motion_interaction", |
3472 | + "key_press_event": "keyboard_interaction", |
3473 | + "scroll-event" : "mouse_scroll_interaction", |
3474 | + } |
3475 | + _events = (gtk.gdk.KEY_PRESS_MASK | gtk.gdk.BUTTON_MOTION_MASK | |
3476 | + gtk.gdk.POINTER_MOTION_HINT_MASK | gtk.gdk.BUTTON_RELEASE_MASK | |
3477 | + gtk.gdk.BUTTON_PRESS_MASK) |
3478 | + |
3479 | + def __init__(self): |
3480 | + """ |
3481 | + :param datastore: The.CairoHistograms two dimensional list of dates and nitems |
3482 | + :param selected_range: the number of days displayed at once |
3483 | + """ |
3484 | + super(CairoHistogram, self).__init__() |
3485 | + self._selected = [] |
3486 | + self.set_events(self._events) |
3487 | + self.set_flags(gtk.CAN_FOCUS) |
3488 | + for key, val in self._connections.iteritems(): |
3489 | + self.connect(key, getattr(self, val)) |
3490 | + self.font_name = self.style.font_desc.get_family() |
3491 | + |
3492 | + def change_style(self, widget, old_style): |
3493 | + """ |
3494 | + Sets the widgets style and coloring |
3495 | + """ |
3496 | + self.colors = self.colors.copy() |
3497 | + self.colors["bg"] = get_gtk_rgba(self.style, "bg", 0) |
3498 | + self.colors["base"] = get_gtk_rgba(self.style, "base", 0) |
3499 | + self.colors["column_normal"] = get_gtk_rgba(self.style, "text", 4, 1.17) |
3500 | + self.colors["column_selected"] = get_gtk_rgba(self.style, "bg", 3) |
3501 | + color = self.style.bg[gtk.STATE_NORMAL] |
3502 | + fcolor = self.style.fg[gtk.STATE_NORMAL] |
3503 | + self.colors["font_color"] = combine_gdk_color(color, fcolor).to_string() |
3504 | + |
3505 | + pal = get_gtk_rgba(self.style, "bg", 3, 1.2) |
3506 | + self.colors["column_alternative"] = (pal[2], pal[1], pal[0], 1) |
3507 | + self.colors["column_selected_alternative"] = get_gtk_rgba(self.style, "bg", 3, 0.6) |
3508 | + self.colors["stroke"] = get_gtk_rgba(self.style, "text", 4) |
3509 | + self.colors["shadow"] = get_gtk_rgba(self.style, "text", 4) |
3510 | + self.font_size = self.style.font_desc.get_size()/1024 |
3511 | + self.pangofont = pango.FontDescription(self.font_name + " %d" % self.font_size) |
3512 | + self.pangofont.set_weight(pango.WEIGHT_BOLD) |
3513 | + self.bottom_padding = self.font_size + 9 + widget.style.ythickness |
3514 | + self.gc = get_gc_from_colormap(widget, 0.6) |
3515 | + |
3516 | + def set_store(self, store): |
3517 | + if self._store: |
3518 | + self._store.disconnect(self._store_connection) |
3519 | + self._store = store |
3520 | + self.largest = min(max(max(map(lambda x: len(x), store.days)), 1), 100) |
3521 | + if not self.get_selected(): |
3522 | + self.set_selected([datetime.date.today()]) |
3523 | + self._store_connection = store.connect("update", self.set_store) |
3524 | + |
3525 | + def get_store(self): |
3526 | + return self._store |
3527 | + |
3528 | + def _expose(self, widget, event): |
3529 | + """ |
3530 | + The major drawing method that the expose event calls directly |
3531 | + """ |
3532 | + widget.style.set_background(widget.window, gtk.STATE_NORMAL) |
3533 | + context = widget.window.cairo_create() |
3534 | + self.expose(widget, event, context) |
3535 | + |
3536 | + def expose(self, widget, event, context): |
3537 | + """ |
3538 | + The minor drawing method |
3539 | + |
3540 | + :param event: a gtk event with x and y values |
3541 | + :param context: This drawingarea's cairo context from the expose event |
3542 | + """ |
3543 | + if not self.pangofont: |
3544 | + self.pangofont = pango.FontDescription(self.font_name + " %d" % self.font_size) |
3545 | + self.pangofont.set_weight(pango.WEIGHT_BOLD) |
3546 | + if not self.gc: |
3547 | + self.gc = get_gc_from_colormap(widget, 0.6) |
3548 | + context.set_source_rgba(*self.colors["base"]) |
3549 | + context.set_operator(cairo.OPERATOR_SOURCE) |
3550 | + #context.paint() |
3551 | + context.rectangle(event.area.x, event.area.y, event.area.width, event.area.height) |
3552 | + context.clip() |
3553 | + #context.set_source_rgba(*self.colors["bg"]) |
3554 | + context.rectangle(event.area.x, event.area.y, event.area.width, event.area.height - self.bottom_padding) |
3555 | + context.fill() |
3556 | + self.draw_columns_from_store(context, event, self.get_selected()) |
3557 | + context.set_line_width(1) |
3558 | + if type(self) == CairoHistogram: |
3559 | + widget.style.paint_shadow(widget.window, gtk.STATE_NORMAL, gtk.SHADOW_IN, |
3560 | + event.area, widget, "treeview", event.area.x, event.area.y, |
3561 | + event.area.width, event.area.height - self.bottom_padding) |
3562 | + if self.is_focus(): |
3563 | + widget.style.paint_focus(widget.window, gtk.STATE_NORMAL, event.area, widget, None, event.area.x, event.area.y, |
3564 | + event.area.width, event.area.height - self.bottom_padding) |
3565 | + |
3566 | + def draw_columns_from_store(self, context, event, selected): |
3567 | + """ |
3568 | + Draws columns from a datastore |
3569 | + |
3570 | + :param context: This drawingarea's cairo context from the expose event |
3571 | + :param event: a gtk event with x and y values |
3572 | + :param selected: a list of the selected dates |
3573 | + """ |
3574 | + x = self.start_x_padding |
3575 | + months_positions = [] |
3576 | + for day in self.get_store().days: |
3577 | + if day.date.day == 1: |
3578 | + months_positions += [(day.date, x)] |
3579 | + if day.date in self._highlighted: |
3580 | + color = self.colors["column_selected_alternative"] if day.date in selected else self.colors["column_alternative"] |
3581 | + elif not selected: |
3582 | + color = self.colors["column_normal"] |
3583 | + if day.date in selected: |
3584 | + color = self.colors["column_selected"] |
3585 | + else: |
3586 | + color = self.colors["column_normal"] |
3587 | + self.draw_column(context, x, event.area.height, len(day), color) |
3588 | + x += self.xincrement |
3589 | + if x > event.area.width: # Check for resize |
3590 | + self.set_size_request(x+self.xincrement, event.area.height) |
3591 | + for date, xpos in months_positions: |
3592 | + edge = 0 |
3593 | + if (date, xpos) == months_positions[-1]: |
3594 | + edge = len(self._store)*self.xincrement |
3595 | + self.draw_month(context, xpos - self.padding, event.area.height, date, edge) |
3596 | + self.max_width = x # remove me |
3597 | + |
3598 | + def draw_column(self, context, x, maxheight, nitems, color): |
3599 | + """ |
3600 | + Draws a columns at x with height based on nitems, and maxheight |
3601 | + |
3602 | + :param context: The drawingarea's cairo context from the expose event |
3603 | + :param x: The current position in the image |
3604 | + :param maxheight: The event areas height |
3605 | + :param nitems: The number of items in the column to be drawn |
3606 | + :param color: A RGBA tuple Example: (0.3, 0.4, 0.8, 1) |
3607 | + """ |
3608 | + if nitems < 2: |
3609 | + nitems = 2 |
3610 | + elif nitems > self.max_column_height: |
3611 | + nitems = self.max_column_height |
3612 | + maxheight = maxheight - self.bottom_padding - 2 |
3613 | + #height = int((maxheight-self.top_padding-2) * (self.largest*math.log(nitems)/math.log(self.largest))/100) |
3614 | + height = int(((float(nitems)/self.largest)*(maxheight-2))) - self.top_padding |
3615 | + #height = min(int((maxheight*self.largest/100) * (1 - math.e**(-0.025*nitems))), maxheight) |
3616 | + if height < self.min_column_height: |
3617 | + height = self.min_column_height |
3618 | + y = maxheight - height |
3619 | + context.set_source_rgba(*color) |
3620 | + context.move_to(x + self.column_radius, y) |
3621 | + context.new_sub_path() |
3622 | + if nitems > 4: |
3623 | + context.arc(self.column_radius + x, self.column_radius + y, self.column_radius, PI, 3 * PI /2) |
3624 | + context.arc(x + self.wcolumn - self.column_radius, self.column_radius + y, self.column_radius, 3 * PI / 2, 0) |
3625 | + context.rectangle(x, y + self.column_radius, self.wcolumn, height - self.column_radius) |
3626 | + else: |
3627 | + context.rectangle(x, y, self.wcolumn, height) |
3628 | + context.close_path() |
3629 | + context.fill() |
3630 | + |
3631 | + def draw_month(self, context, x, height, date, edge=0): |
3632 | + """ |
3633 | + Draws a line signifying the start of a month |
3634 | + """ |
3635 | + context.set_source_rgba(*self.colors["stroke"]) |
3636 | + context.set_line_width(self.stroke_width) |
3637 | + context.move_to(x+self.stroke_offset, 0) |
3638 | + context.line_to(x+self.stroke_offset, height - self.bottom_padding) |
3639 | + context.stroke() |
3640 | + month = calendar.month_name[date.month] |
3641 | + date = "<span color='%s'>%s %d</span>" % (self.colors["font_color"], month, date.year) |
3642 | + layout = self.create_pango_layout(date) |
3643 | + layout.set_markup(date) |
3644 | + layout.set_font_description(self.pangofont) |
3645 | + w, h = layout.get_pixel_size() |
3646 | + if edge: |
3647 | + if x + w > edge: x = edge - w - 5 |
3648 | + self.window.draw_layout(self.gc, int(x + 3), int(height - self.bottom_padding/2 - h/2), layout) |
3649 | + |
3650 | + def set_selected(self, dates): |
3651 | + if dates == self._selected: |
3652 | + return False |
3653 | + self._selected = dates |
3654 | + if dates: |
3655 | + date = dates[-1] |
3656 | + self.emit("selection-set", dates) |
3657 | + self.queue_draw() |
3658 | + return True |
3659 | + |
3660 | + def get_selected(self): |
3661 | + """ |
3662 | + returns a list of selected indices |
3663 | + """ |
3664 | + return self._selected |
3665 | + |
3666 | + def clear_selection(self): |
3667 | + """ |
3668 | + clears the selected items |
3669 | + """ |
3670 | + self._selected = [] |
3671 | + self.queue_draw() |
3672 | + |
3673 | + def set_highlighted(self, highlighted): |
3674 | + """ |
3675 | + Sets the widgets which should be highlighted with an alternative color |
3676 | + |
3677 | + :param highlighted: a list of indexes to be highlighted |
3678 | + """ |
3679 | + if isinstance(highlighted, list): |
3680 | + self._highlighted = highlighted |
3681 | + else: raise TypeError("highlighted is not a list") |
3682 | + self.queue_draw() |
3683 | + |
3684 | + def clear_highlighted(self): |
3685 | + """Clears the highlighted color""" |
3686 | + self._highlighted = [] |
3687 | + self.queue_draw() |
3688 | + |
3689 | + def set_single_day(self, choice): |
3690 | + """ |
3691 | + Allows the cal to enter a mode where the trailing days are not selected but still kept |
3692 | + """ |
3693 | + self._single_day_only = choice |
3694 | + self.queue_draw() |
3695 | + |
3696 | + def get_store_index_from_cartesian(self, x, y): |
3697 | + """ |
3698 | + Gets the datastore index from a x, y value |
3699 | + """ |
3700 | + return int((x - self.start_x_padding) / self.xincrement) |
3701 | + |
3702 | + def keyboard_interaction(self, widget, event): |
3703 | + if event.keyval in (gtk.keysyms.space, gtk.keysyms.Right, gtk.keysyms.Left, gtk.keysyms.BackSpace): |
3704 | + i = self.get_selected() |
3705 | + if isinstance(i, list) and len(i) > 0: i = i[-1] |
3706 | + if event.keyval in (gtk.keysyms.space, gtk.keysyms.Right): |
3707 | + i = i + datetime.timedelta(days=1) |
3708 | + elif event.keyval in (gtk.keysyms.Left, gtk.keysyms.BackSpace): |
3709 | + i = i + datetime.timedelta(days=-1) |
3710 | + if i < datetime.date.today() + datetime.timedelta(days=1): |
3711 | + self.change_location(i) |
3712 | + |
3713 | + def mouse_motion_interaction(self, widget, event, *args, **kwargs): |
3714 | + """ |
3715 | + Reacts to mouse moving (while pressed), and clicks |
3716 | + """ |
3717 | + #if (event.state == gtk.gdk.BUTTON1_MASK and not self._disable_mouse_motion): |
3718 | + location = min((self.get_store_index_from_cartesian(event.x, event.y), len(self._store.days) - 1)) |
3719 | + if location != self._last_location: |
3720 | + self.change_location(location) |
3721 | + self._last_location = location |
3722 | + #return True |
3723 | + return False |
3724 | + |
3725 | + def mouse_press_interaction(self, widget, event, *args, **kwargs): |
3726 | + if (event.y > self.get_size_request()[1] - self.bottom_padding and |
3727 | + event.y < self.get_size_request()[1]): |
3728 | + return False |
3729 | + location = min((self.get_store_index_from_cartesian(event.x, event.y), len(self._store.days) - 1)) |
3730 | + if location != self._last_location: |
3731 | + self.change_location(location) |
3732 | + self._last_location = location |
3733 | + return True |
3734 | + |
3735 | + def mouse_scroll_interaction(self, widget, event): |
3736 | + date = self.get_selected()[-1] |
3737 | + i = self.get_store().dates.index(date) |
3738 | + if (event.direction in (gtk.gdk.SCROLL_UP, gtk.gdk.SCROLL_RIGHT)): |
3739 | + if i+1< len(self.get_store().days): |
3740 | + self.change_location(i+1) |
3741 | + elif (event.direction in (gtk.gdk.SCROLL_DOWN, gtk.gdk.SCROLL_LEFT)): |
3742 | + if 0 <= i-1: |
3743 | + self.change_location(i-1) |
3744 | + |
3745 | + def change_location(self, location): |
3746 | + """ |
3747 | + Handles click events |
3748 | + """ |
3749 | + if isinstance(location, int): |
3750 | + if location < 0: |
3751 | + return False |
3752 | + store = self.get_store() |
3753 | + date = store.days[location].date |
3754 | + else: date = location |
3755 | + self.emit("column_clicked", date) |
3756 | + return True |
3757 | + |
3758 | + |
3759 | +def _in_area(coord_x, coord_y, area): |
3760 | + """check if some given X,Y coordinates are within an area. |
3761 | + area is either None or a (top_left_x, top_left_y, width, height)-tuple""" |
3762 | + if area is None: |
3763 | + return False |
3764 | + area_x, area_y, area_width, area_height = area |
3765 | + return (area_x <= coord_x <= area_x + area_width) and \ |
3766 | + (area_y <= coord_y <= area_y + area_height) |
3767 | + |
3768 | + |
3769 | +def _in_area(coord_x, coord_y, area): |
3770 | + """check if some given X,Y coordinates are within an area. |
3771 | + area is either None or a (top_left_x, top_left_y, width, height)-tuple""" |
3772 | + if area is None: |
3773 | + return False |
3774 | + area_x, area_y, area_width, area_height = area |
3775 | + return (area_x <= coord_x <= area_x + area_width) and \ |
3776 | + (area_y <= coord_y <= area_y + area_height) |
3777 | + |
3778 | + |
3779 | +class TooltipEventBox(gtk.EventBox): |
3780 | + """ |
3781 | + A event box housing the tool tip logic that can be used for a CairoHistogram. |
3782 | + Otherwise it interferes with the scrubbing mask code |
3783 | + """ |
3784 | + _saved_tooltip_location = None |
3785 | + def __init__(self, histogram, container): |
3786 | + super(TooltipEventBox, self).__init__() |
3787 | + self.add(histogram) |
3788 | + self.histogram = histogram |
3789 | + self.container = container |
3790 | + self.set_property("has-tooltip", True) |
3791 | + #self.connect("query-tooltip", self.query_tooltip) |
3792 | + |
3793 | + def query_tooltip(self, widget, x, y, keyboard_mode, tooltip): |
3794 | + if y < self.histogram.get_size_request()[1] - self.histogram.bottom_padding: |
3795 | + location = self.histogram.get_store_index_from_cartesian(x, y) |
3796 | + if location != self._saved_tooltip_location: |
3797 | + # don't show the previous tooltip if we moved to another |
3798 | + # location |
3799 | + self._saved_tooltip_location = location |
3800 | + return False |
3801 | + try: |
3802 | + timestamp, count = self.histogram.get_store()[location] |
3803 | + except IndexError: |
3804 | + # there is no bar for at this location |
3805 | + # don't show a tooltip |
3806 | + return False |
3807 | + date = datetime.date.fromtimestamp(timestamp).strftime("%A, %d %B, %Y") |
3808 | + tooltip.set_text("%s\n%i %s" % (date, count, |
3809 | + gettext.ngettext("item", "items", count))) |
3810 | + else: |
3811 | + return False |
3812 | + return True |
3813 | + |
3814 | + |
3815 | +class JournalHistogram(CairoHistogram): |
3816 | + """ |
3817 | + A subclass of CairoHistogram with theming to fit into Journal |
3818 | + """ |
3819 | + padding = 2 |
3820 | + column_radius = 1.3 |
3821 | + top_padding = 6 |
3822 | + bottom_padding = 29 |
3823 | + wcolumn = 10 |
3824 | + xincrement = wcolumn + padding |
3825 | + column_radius = 2 |
3826 | + stroke_width = 2 |
3827 | + stroke_offset = 1 |
3828 | + font_size = 12 |
3829 | + min_column_height = 2 |
3830 | + |
3831 | + def change_style(self, widget, *args, **kwargs): |
3832 | + self.colors = self.colors.copy() |
3833 | + self.colors["bg"] = get_gtk_rgba(self.style, "bg", 0) |
3834 | + self.colors["color"] = get_gtk_rgba(self.style, "base", 0) |
3835 | + self.colors["column_normal"] = get_gtk_rgba(self.style, "bg", 1) |
3836 | + self.colors["column_selected"] = get_gtk_rgba(self.style, "bg", 3) |
3837 | + self.colors["column_selected_alternative"] = get_gtk_rgba(self.style, "bg", 3, 0.7) |
3838 | + self.colors["column_alternative"] = get_gtk_rgba(self.style, "text", 2) |
3839 | + self.colors["stroke"] = get_gtk_rgba(self.style, "bg", 0) |
3840 | + self.colors["shadow"] = get_gtk_rgba(self.style, "bg", 0, 0.98) |
3841 | + self.font_size = self.style.font_desc.get_size()/1024 |
3842 | + self.bottom_padding = self.font_size + 9 + widget.style.ythickness |
3843 | + self.gc = self.style.text_gc[gtk.STATE_NORMAL] |
3844 | + self.pangofont = pango.FontDescription(self.font_name + " %d" % self.font_size) |
3845 | + self.pangofont.set_weight(pango.WEIGHT_BOLD) |
3846 | + |
3847 | + |
3848 | +class HistogramWidget(gtk.Viewport): |
3849 | + """ |
3850 | + A container for a CairoHistogram which allows you to scroll |
3851 | + """ |
3852 | + __gsignals__ = { |
3853 | + # the index of the first selected item in the datastore. |
3854 | + "date-changed" : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, |
3855 | + (gobject.TYPE_PYOBJECT,)), |
3856 | + } |
3857 | + |
3858 | + def __init__(self, histo_type=CairoHistogram, size = (600, 75)): |
3859 | + """ |
3860 | + :param histo_type: a :class:`CairoHistogram <CairoHistogram>` or a derivative |
3861 | + """ |
3862 | + super(HistogramWidget, self).__init__() |
3863 | + self.set_shadow_type(gtk.SHADOW_NONE) |
3864 | + self.histogram = histo_type() |
3865 | + self.eventbox = TooltipEventBox(self.histogram, self) |
3866 | + self.set_size_request(*size) |
3867 | + self.add(self.eventbox) |
3868 | + self.histogram.connect("column_clicked", self.date_changed) |
3869 | + self.histogram.connect("selection-set", self.scrubbing_fix) |
3870 | + self.histogram.queue_draw() |
3871 | + self.queue_draw() |
3872 | + |
3873 | + def date_changed(self, widget, date): |
3874 | + self.emit("date-changed", date) |
3875 | + |
3876 | + def set_store(self, store): |
3877 | + self.histogram.set_store(store) |
3878 | + self.scroll_to_end() |
3879 | + |
3880 | + def set_dates(self, dates): |
3881 | + self.histogram.set_selected(dates) |
3882 | + |
3883 | + def scroll_to_end(self, *args, **kwargs): |
3884 | + """ |
3885 | + Scroll to the end of the drawing area's viewport |
3886 | + """ |
3887 | + hadjustment = self.get_hadjustment() |
3888 | + hadjustment.set_value(1) |
3889 | + hadjustment.set_value(self.histogram.max_width - hadjustment.page_size) |
3890 | + |
3891 | + def scrubbing_fix(self, widget, dates): |
3892 | + """ |
3893 | + Allows scrubbing to scroll the scroll window |
3894 | + """ |
3895 | + if not len(dates): |
3896 | + return |
3897 | + store = widget.get_store() |
3898 | + i = store.dates.index(dates[0]) |
3899 | + hadjustment = self.get_hadjustment() |
3900 | + proposed_xa = ((i) * self.histogram.xincrement) + self.histogram.start_x_padding |
3901 | + proposed_xb = ((i + len(dates)) * self.histogram.xincrement) + self.histogram.start_x_padding |
3902 | + if proposed_xa < hadjustment.value: |
3903 | + hadjustment.set_value(proposed_xa) |
3904 | + elif proposed_xb > hadjustment.value + hadjustment.page_size: |
3905 | + hadjustment.set_value(proposed_xb - hadjustment.page_size) |
3906 | |
3907 | === removed file 'src/histogram.py' |
3908 | --- src/histogram.py 2010-04-15 14:34:37 +0000 |
3909 | +++ src/histogram.py 1970-01-01 00:00:00 +0000 |
3910 | @@ -1,625 +0,0 @@ |
3911 | -# -.- coding: utf-8 -.- |
3912 | -# |
3913 | -# GNOME Activity Journal |
3914 | -# |
3915 | -# Copyright © 2010 Randal Barlow <email.tehk@gmail.com> |
3916 | -# Copyright © 2010 Markus Korn |
3917 | -# Copyright © 2010 Siegfried Gevatter <siegfried@gevatter.com> |
3918 | -# |
3919 | -# This program is free software: you can redistribute it and/or modify |
3920 | -# it under the terms of the GNU General Public License as published by |
3921 | -# the Free Software Foundation, either version 3 of the License, or |
3922 | -# (at your option) any later version. |
3923 | -# |
3924 | -# This program is distributed in the hope that it will be useful, |
3925 | -# but WITHOUT ANY WARRANTY; without even the implied warranty of |
3926 | -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
3927 | -# GNU General Public License for more details. |
3928 | -# |
3929 | -# You should have received a copy of the GNU General Public License |
3930 | -# along with this program. If not, see <http://www.gnu.org/licenses/>. |
3931 | - |
3932 | -""" |
3933 | -Takes a two dementional list of ints and turns it into a graph based on |
3934 | -the first value a int date, and the second value the number of items on that date |
3935 | -where items are |
3936 | -datastore = [] |
3937 | -datastore.append(time, nitems) |
3938 | -CairoHistogram.set_datastore(datastore) |
3939 | -""" |
3940 | -import datetime |
3941 | -import cairo |
3942 | -import calendar |
3943 | -import gettext |
3944 | -import gobject |
3945 | -import gtk |
3946 | -from math import pi as PI |
3947 | -import pango |
3948 | -from common import * |
3949 | - |
3950 | - |
3951 | -def get_gc_from_colormap(widget, shade): |
3952 | - """ |
3953 | - Gets a gtk.gdk.GC and modifies the color by shade |
3954 | - """ |
3955 | - gc = widget.style.text_gc[gtk.STATE_INSENSITIVE] |
3956 | - if gc: |
3957 | - color = widget.style.text[4] |
3958 | - color = shade_gdk_color(color, shade) |
3959 | - gc.set_rgb_fg_color(color) |
3960 | - return gc |
3961 | - |
3962 | - |
3963 | -class CairoHistogram(gtk.DrawingArea): |
3964 | - """ |
3965 | - A histogram which is represented by a list of dates, and nitems. |
3966 | - |
3967 | - There are a few maintenance issues due to the movement abilities. The widget |
3968 | - currently is able to capture motion events when the mouse is outside |
3969 | - the widget and the button is pressed if it was initially pressed inside |
3970 | - the widget. This event mask magic leaves a few flaws open. |
3971 | - """ |
3972 | - _selected = (0,) |
3973 | - padding = 2 |
3974 | - bottom_padding = 23 |
3975 | - top_padding = 2 |
3976 | - wcolumn = 12 |
3977 | - xincrement = wcolumn + padding |
3978 | - start_x_padding = 2 |
3979 | - max_width = xincrement |
3980 | - column_radius = 0 |
3981 | - stroke_width = 1 |
3982 | - stroke_offset = 0 |
3983 | - min_column_height = 4 |
3984 | - max_column_height = 101 |
3985 | - gc = None |
3986 | - pangofont = None |
3987 | - _disable_mouse_motion = False |
3988 | - selected_range = 0 |
3989 | - _highlighted = tuple() |
3990 | - _last_location = -1 |
3991 | - _single_day_only = False |
3992 | - colors = { |
3993 | - "bg" : (1, 1, 1, 1), |
3994 | - "base" : (1, 1, 1, 1), |
3995 | - "column_normal" : (1, 1, 1, 1), |
3996 | - "column_selected" : (1, 1, 1, 1), |
3997 | - "column_alternative" : (1, 1, 1, 1), |
3998 | - "column_selected_alternative" : (1, 1, 1, 1), |
3999 | - "font_color" : "#ffffff", |
4000 | - "stroke" : (1, 1, 1, 0), |
4001 | - "shadow" : (1, 1, 1, 0), |
4002 | - } |
4003 | - |
4004 | - # Today button stuff |
4005 | - _today_width = 0 |
4006 | - _today_text = "" |
4007 | - _today_area = None |
4008 | - _today_hover = False |
4009 | - |
4010 | - _datastore = None |
4011 | - __gsignals__ = { |
4012 | - # the index of the first selected item in the datastore. |
4013 | - "selection-set" : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, |
4014 | - (gobject.TYPE_INT,gobject.TYPE_INT)), |
4015 | - "data-updated" : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,()), |
4016 | - "column_clicked" : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, |
4017 | - (gobject.TYPE_INT,)) |
4018 | - } |
4019 | - _connections = {"style-set": "change_style", |
4020 | - "expose_event": "_expose", |
4021 | - "button_press_event": "mouse_press_interaction", |
4022 | - "motion_notify_event": "mouse_motion_interaction", |
4023 | - "key_press_event": "keyboard_interaction", |
4024 | - "scroll-event" : "mouse_scroll_interaction", |
4025 | - "selection-set": "check_for_today", |
4026 | - } |
4027 | - _events = (gtk.gdk.KEY_PRESS_MASK | gtk.gdk.BUTTON_MOTION_MASK | |
4028 | - gtk.gdk.POINTER_MOTION_HINT_MASK | gtk.gdk.BUTTON_RELEASE_MASK | |
4029 | - gtk.gdk.BUTTON_PRESS_MASK) |
4030 | - |
4031 | - def __init__(self, datastore = None, selected_range = 0): |
4032 | - """ |
4033 | - :param datastore: The.CairoHistograms two dimensional list of dates and nitems |
4034 | - :param selected_range: the number of days displayed at once |
4035 | - """ |
4036 | - super(CairoHistogram, self).__init__() |
4037 | - self.set_events(self._events) |
4038 | - self.set_flags(gtk.CAN_FOCUS) |
4039 | - for key, val in self._connections.iteritems(): |
4040 | - self.connect(key, getattr(self, val)) |
4041 | - self.font_name = self.style.font_desc.get_family() |
4042 | - self.set_datastore(datastore if datastore else [], draw = False) |
4043 | - self.selected_range = selected_range |
4044 | - |
4045 | - def change_style(self, widget, old_style): |
4046 | - """ |
4047 | - Sets the widgets style and coloring |
4048 | - """ |
4049 | - self.colors = self.colors.copy() |
4050 | - self.colors["bg"] = get_gtk_rgba(self.style, "bg", 0) |
4051 | - self.colors["base"] = get_gtk_rgba(self.style, "base", 0) |
4052 | - self.colors["column_normal"] = get_gtk_rgba(self.style, "text", 4, 1.17) |
4053 | - self.colors["column_selected"] = get_gtk_rgba(self.style, "bg", 3) |
4054 | - color = self.style.bg[gtk.STATE_NORMAL] |
4055 | - fcolor = self.style.fg[gtk.STATE_NORMAL] |
4056 | - self.colors["font_color"] = combine_gdk_color(color, fcolor).to_string() |
4057 | - |
4058 | - pal = get_gtk_rgba(self.style, "bg", 3, 1.2) |
4059 | - self.colors["column_alternative"] = (pal[2], pal[1], pal[0], 1) |
4060 | - self.colors["column_selected_alternative"] = get_gtk_rgba(self.style, "bg", 3, 0.6) |
4061 | - self.colors["stroke"] = get_gtk_rgba(self.style, "text", 4) |
4062 | - self.colors["shadow"] = get_gtk_rgba(self.style, "text", 4) |
4063 | - self.font_size = self.style.font_desc.get_size()/1024 |
4064 | - self.pangofont = pango.FontDescription(self.font_name + " %d" % self.font_size) |
4065 | - self.pangofont.set_weight(pango.WEIGHT_BOLD) |
4066 | - self.bottom_padding = self.font_size + 9 + widget.style.ythickness |
4067 | - self.gc = get_gc_from_colormap(widget, 0.6) |
4068 | - |
4069 | - def set_selected_range(self, selected_range): |
4070 | - """ |
4071 | - Set the number of days to be colored as selected |
4072 | - |
4073 | - :param selected_range: the range to be used when setting selected coloring |
4074 | - """ |
4075 | - self.selected_range = selected_range |
4076 | - return True |
4077 | - |
4078 | - def set_datastore(self, datastore, draw = True): |
4079 | - """ |
4080 | - Sets the objects datastore attribute using a list |
4081 | - |
4082 | - :param datastore: A list that is comprised of rows containing |
4083 | - a int time and a int nitems |
4084 | - """ |
4085 | - if isinstance(datastore, list): |
4086 | - self._datastore = datastore |
4087 | - self.largest = 1 |
4088 | - for date, nitems in self._datastore: |
4089 | - if nitems > self.largest: self.largest = nitems |
4090 | - if self.largest > self.max_column_height: self.largest = self.max_column_height |
4091 | - self.max_width = self.xincrement + (self.xincrement *len(datastore)) |
4092 | - else: |
4093 | - raise TypeError("Datastore is not a <list>") |
4094 | - self.emit("data-updated") |
4095 | - self.set_selected(len(datastore) - self.selected_range) |
4096 | - |
4097 | - def get_datastore(self): |
4098 | - return self._datastore |
4099 | - |
4100 | - def prepend_data(self, newdatastore): |
4101 | - """ |
4102 | - Adds the items of a new list before the items of the current datastore |
4103 | - |
4104 | - :param newdatastore: the new list to be prepended |
4105 | - |
4106 | - ## WARNING SELECTION WILL CHANGE WHEN DOING THIS TO BE FIXED ## |
4107 | - """ |
4108 | - selected = self.get_selected()[-1] |
4109 | - self._datastore = newdatastore + self._datastore |
4110 | - self.queue_draw() |
4111 | - self.set_selected(len(newdatastore) + selected) |
4112 | - |
4113 | - def _expose(self, widget, event): |
4114 | - """ |
4115 | - The major drawing method that the expose event calls directly |
4116 | - """ |
4117 | - widget.style.set_background(widget.window, gtk.STATE_NORMAL) |
4118 | - context = widget.window.cairo_create() |
4119 | - self.expose(widget, event, context) |
4120 | - if len(self._today_text): |
4121 | - self.draw_today(widget, event, context) |
4122 | - |
4123 | - def expose(self, widget, event, context): |
4124 | - """ |
4125 | - The minor drawing method |
4126 | - |
4127 | - :param event: a gtk event with x and y values |
4128 | - :param context: This drawingarea's cairo context from the expose event |
4129 | - """ |
4130 | - if not self.pangofont: |
4131 | - self.pangofont = pango.FontDescription(self.font_name + " %d" % self.font_size) |
4132 | - self.pangofont.set_weight(pango.WEIGHT_BOLD) |
4133 | - if not self.gc: |
4134 | - self.gc = get_gc_from_colormap(widget, 0.6) |
4135 | - context.set_source_rgba(*self.colors["base"]) |
4136 | - context.set_operator(cairo.OPERATOR_SOURCE) |
4137 | - #context.paint() |
4138 | - context.rectangle(event.area.x, event.area.y, event.area.width, event.area.height) |
4139 | - context.clip() |
4140 | - #context.set_source_rgba(*self.colors["bg"]) |
4141 | - context.rectangle(event.area.x, event.area.y, event.area.width, event.area.height - self.bottom_padding) |
4142 | - context.fill() |
4143 | - self.draw_columns_from_datastore(context, event, self.get_selected()) |
4144 | - context.set_line_width(1) |
4145 | - if type(self) == CairoHistogram: |
4146 | - widget.style.paint_shadow(widget.window, gtk.STATE_NORMAL, gtk.SHADOW_IN, |
4147 | - event.area, widget, "treeview", event.area.x, event.area.y, |
4148 | - event.area.width, event.area.height - self.bottom_padding) |
4149 | - if self.is_focus(): |
4150 | - widget.style.paint_focus(widget.window, gtk.STATE_NORMAL, event.area, widget, None, event.area.x, event.area.y, |
4151 | - event.area.width, event.area.height - self.bottom_padding) |
4152 | - |
4153 | - def draw_today(self, widget, event, context): |
4154 | - """ |
4155 | - """ |
4156 | - layout = widget.create_pango_layout(self._today_text) |
4157 | - pangofont = pango.FontDescription(widget.font_name + " %d" % (widget.font_size - 1)) |
4158 | - if not widget.gc: |
4159 | - widget.gc = get_gc_from_colormap(widget, 0.6) |
4160 | - layout.set_font_description(pangofont) |
4161 | - w, h = layout.get_pixel_size() |
4162 | - self._today_width = w + 10 |
4163 | - self._today_area = ( |
4164 | - int(event.area.x + event.area.width - self._today_width), |
4165 | - int(event.area.height - widget.bottom_padding + 2), |
4166 | - self._today_width, |
4167 | - widget.bottom_padding - 2) |
4168 | - state = gtk.STATE_PRELIGHT |
4169 | - shadow = gtk.SHADOW_OUT |
4170 | - widget.style.paint_box( |
4171 | - widget.window, state, shadow, event.area, widget, "button", *self._today_area) |
4172 | - widget.window.draw_layout( |
4173 | - widget.gc, int(event.area.x + event.area.width - w -5), |
4174 | - int(event.area.height - widget.bottom_padding/2 - h/2), layout) |
4175 | - |
4176 | - def draw_columns_from_datastore(self, context, event, selected): |
4177 | - """ |
4178 | - Draws columns from a datastore |
4179 | - |
4180 | - :param context: This drawingarea's cairo context from the expose event |
4181 | - :param event: a gtk event with x and y values |
4182 | - :param selected: a list of the selected columns |
4183 | - """ |
4184 | - x = self.start_x_padding |
4185 | - months_positions = [] |
4186 | - for i, (date, nitems) in enumerate(self.get_datastore()): |
4187 | - if datetime.date.fromtimestamp(date).day == 1: |
4188 | - months_positions += [(date, x)] |
4189 | - if len(self._highlighted) > 0 and i >= self._highlighted[0] and i <= self._highlighted[-1] and i in self._highlighted: |
4190 | - color = self.colors["column_selected_alternative"] if i in selected else self.colors["column_alternative"] |
4191 | - elif not selected: |
4192 | - color = self.colors["column_normal"] |
4193 | - elif self._single_day_only and i != selected[-1]: |
4194 | - color = self.colors["column_normal"] |
4195 | - elif i >= selected[0] and i <= selected[-1] and i in selected: |
4196 | - color = self.colors["column_selected"] |
4197 | - else: |
4198 | - color = self.colors["column_normal"] |
4199 | - self.draw_column(context, x, event.area.height, nitems, color) |
4200 | - x += self.xincrement |
4201 | - if x > event.area.width: # Check for resize |
4202 | - self.set_size_request(x+self.xincrement, event.area.height) |
4203 | - for date, xpos in months_positions: |
4204 | - edge = 0 |
4205 | - if (date, xpos) == months_positions[-1]: |
4206 | - edge = len(self._datastore)*self.xincrement |
4207 | - self.draw_month(context, xpos - self.padding, event.area.height, date, edge) |
4208 | - self.max_width = x # remove me |
4209 | - |
4210 | - def draw_column(self, context, x, maxheight, nitems, color): |
4211 | - """ |
4212 | - Draws a columns at x with height based on nitems, and maxheight |
4213 | - |
4214 | - :param context: The drawingarea's cairo context from the expose event |
4215 | - :param x: The current position in the image |
4216 | - :param maxheight: The event areas height |
4217 | - :param nitems: The number of items in the column to be drawn |
4218 | - :param color: A RGBA tuple Example: (0.3, 0.4, 0.8, 1) |
4219 | - """ |
4220 | - if nitems < 2: |
4221 | - nitems = 2 |
4222 | - elif nitems > self.max_column_height: |
4223 | - nitems = self.max_column_height |
4224 | - maxheight = maxheight - self.bottom_padding - 2 |
4225 | - #height = int((maxheight-self.top_padding-2) * (self.largest*math.log(nitems)/math.log(self.largest))/100) |
4226 | - height = int(((float(nitems)/self.largest)*(maxheight-2))) - self.top_padding |
4227 | - #height = min(int((maxheight*self.largest/100) * (1 - math.e**(-0.025*nitems))), maxheight) |
4228 | - if height < self.min_column_height: |
4229 | - height = self.min_column_height |
4230 | - y = maxheight - height |
4231 | - context.set_source_rgba(*color) |
4232 | - context.move_to(x + self.column_radius, y) |
4233 | - context.new_sub_path() |
4234 | - if nitems > 4: |
4235 | - context.arc(self.column_radius + x, self.column_radius + y, self.column_radius, PI, 3 * PI /2) |
4236 | - context.arc(x + self.wcolumn - self.column_radius, self.column_radius + y, self.column_radius, 3 * PI / 2, 0) |
4237 | - context.rectangle(x, y + self.column_radius, self.wcolumn, height - self.column_radius) |
4238 | - else: |
4239 | - context.rectangle(x, y, self.wcolumn, height) |
4240 | - context.close_path() |
4241 | - context.fill() |
4242 | - |
4243 | - def draw_month(self, context, x, height, date, edge=0): |
4244 | - """ |
4245 | - Draws a line signifying the start of a month |
4246 | - """ |
4247 | - context.set_source_rgba(*self.colors["stroke"]) |
4248 | - context.set_line_width(self.stroke_width) |
4249 | - context.move_to(x+self.stroke_offset, 0) |
4250 | - context.line_to(x+self.stroke_offset, height - self.bottom_padding) |
4251 | - context.stroke() |
4252 | - date = datetime.date.fromtimestamp(date) |
4253 | - month = calendar.month_name[date.month] |
4254 | - date = "<span color='%s'>%s %d</span>" % (self.colors["font_color"], month, date.year) |
4255 | - layout = self.create_pango_layout(date) |
4256 | - layout.set_markup(date) |
4257 | - layout.set_font_description(self.pangofont) |
4258 | - w, h = layout.get_pixel_size() |
4259 | - if edge: |
4260 | - if x + w > edge: x = edge - w - 5 |
4261 | - self.window.draw_layout(self.gc, int(x + 3), int(height - self.bottom_padding/2 - h/2), layout) |
4262 | - |
4263 | - def set_selected(self, i): |
4264 | - """ |
4265 | - Set the selected items using a int or a list of the selections |
4266 | - If you pass this method a int it will select the index + selected_range |
4267 | - |
4268 | - Emits: |
4269 | - self._selected[0] and self._selected[-1] |
4270 | - |
4271 | - :param i: a list or a int where the int will select i + selected_range |
4272 | - """ |
4273 | - if len(self._selected): |
4274 | - if i == self._selected[0]: |
4275 | - return False |
4276 | - if isinstance(i, int): |
4277 | - self._selected = range(i, i + self.selected_range) |
4278 | - self.emit("selection-set", max(i, 0), max(i + self.selected_range - 1, 0)) |
4279 | - else: self._selected = (-1,) |
4280 | - self.queue_draw() |
4281 | - return True |
4282 | - |
4283 | - def get_selected(self): |
4284 | - """ |
4285 | - returns a list of selected indices |
4286 | - """ |
4287 | - return self._selected |
4288 | - |
4289 | - def clear_selection(self): |
4290 | - """ |
4291 | - clears the selected items |
4292 | - """ |
4293 | - self._selected = range(len(self._datastore))[-self.selected_range:] |
4294 | - self.queue_draw() |
4295 | - |
4296 | - def set_highlighted(self, highlighted): |
4297 | - """ |
4298 | - Sets the widgets which should be highlighted with an alternative color |
4299 | - |
4300 | - :param highlighted: a list of indexes to be highlighted |
4301 | - """ |
4302 | - if isinstance(highlighted, list): |
4303 | - self._highlighted = highlighted |
4304 | - else: raise TypeError("highlighted is not a list") |
4305 | - self.queue_draw() |
4306 | - |
4307 | - def clear_highlighted(self): |
4308 | - """Clears the highlighted color""" |
4309 | - self._highlighted = [] |
4310 | - self.queue_draw() |
4311 | - |
4312 | - def set_single_day(self, choice): |
4313 | - """ |
4314 | - Allows the cal to enter a mode where the trailing days are not selected but still kept |
4315 | - """ |
4316 | - self._single_day_only = choice |
4317 | - self.queue_draw() |
4318 | - |
4319 | - def get_datastore_index_from_cartesian(self, x, y): |
4320 | - """ |
4321 | - Gets the datastore index from a x, y value |
4322 | - """ |
4323 | - return int((x - self.start_x_padding) / self.xincrement) |
4324 | - |
4325 | - def keyboard_interaction(self, widget, event): |
4326 | - if event.keyval in (gtk.keysyms.space, gtk.keysyms.Right, gtk.keysyms.Left, gtk.keysyms.BackSpace): |
4327 | - i = self.get_selected() |
4328 | - if isinstance(i, list) and len(i) > 0: i = i[-1] |
4329 | - if event.keyval in (gtk.keysyms.space, gtk.keysyms.Right): |
4330 | - i += 1 |
4331 | - elif event.keyval in (gtk.keysyms.Left, gtk.keysyms.BackSpace): |
4332 | - i -= 1 |
4333 | - if i < len(self.get_datastore()): |
4334 | - self.change_location(i) |
4335 | - |
4336 | - def mouse_motion_interaction(self, widget, event, *args, **kwargs): |
4337 | - """ |
4338 | - Reacts to mouse moving (while pressed), and clicks |
4339 | - """ |
4340 | - #if (event.state == gtk.gdk.BUTTON1_MASK and not self._disable_mouse_motion): |
4341 | - location = min((self.get_datastore_index_from_cartesian(event.x, event.y), len(self._datastore) - 1)) |
4342 | - if location != self._last_location: |
4343 | - self.change_location(location) |
4344 | - self._last_location = location |
4345 | - #return True |
4346 | - return False |
4347 | - |
4348 | - def mouse_press_interaction(self, widget, event, *args, **kwargs): |
4349 | - if (event.y > self.get_size_request()[1] - self.bottom_padding and |
4350 | - event.y < self.get_size_request()[1]): |
4351 | - return False |
4352 | - location = min((self.get_datastore_index_from_cartesian(event.x, event.y), len(self._datastore) - 1)) |
4353 | - if location != self._last_location: |
4354 | - self.change_location(location) |
4355 | - self._last_location = location |
4356 | - return True |
4357 | - |
4358 | - def mouse_scroll_interaction(self, widget, event): |
4359 | - i = self.get_selected()[-1] |
4360 | - if (event.direction in (gtk.gdk.SCROLL_UP, gtk.gdk.SCROLL_RIGHT)): |
4361 | - if i+1< len(self.get_datastore()): |
4362 | - self.change_location(i+1) |
4363 | - elif (event.direction in (gtk.gdk.SCROLL_DOWN, gtk.gdk.SCROLL_LEFT)): |
4364 | - if 0 <= i-1: |
4365 | - self.change_location(i-1) |
4366 | - |
4367 | - def change_location(self, location): |
4368 | - """ |
4369 | - Handles click events |
4370 | - """ |
4371 | - if location < 0: |
4372 | - return False |
4373 | - self.set_selected(max(location - self.selected_range + 1, 0)) |
4374 | - self.emit("column_clicked", location) |
4375 | - return True |
4376 | - |
4377 | - # Today stuff |
4378 | - def check_for_today(self, widget, i, ii): |
4379 | - """ |
4380 | - Changes today to a empty string if the selected item is not today |
4381 | - """ |
4382 | - if ii == len(self.get_datastore())-1: |
4383 | - self._today_text = "" |
4384 | - self._today_area = None |
4385 | - elif len(self._today_text) == 0: |
4386 | - self._today_text = _("Today") + " »" |
4387 | - self.queue_draw() |
4388 | - return True |
4389 | - |
4390 | - |
4391 | -def _in_area(coord_x, coord_y, area): |
4392 | - """check if some given X,Y coordinates are within an area. |
4393 | - area is either None or a (top_left_x, top_left_y, width, height)-tuple""" |
4394 | - if area is None: |
4395 | - return False |
4396 | - area_x, area_y, area_width, area_height = area |
4397 | - return (area_x <= coord_x <= area_x + area_width) and \ |
4398 | - (area_y <= coord_y <= area_y + area_height) |
4399 | - |
4400 | - |
4401 | -def _in_area(coord_x, coord_y, area): |
4402 | - """check if some given X,Y coordinates are within an area. |
4403 | - area is either None or a (top_left_x, top_left_y, width, height)-tuple""" |
4404 | - if area is None: |
4405 | - return False |
4406 | - area_x, area_y, area_width, area_height = area |
4407 | - return (area_x <= coord_x <= area_x + area_width) and \ |
4408 | - (area_y <= coord_y <= area_y + area_height) |
4409 | - |
4410 | - |
4411 | -class TooltipEventBox(gtk.EventBox): |
4412 | - """ |
4413 | - A event box housing the tool tip logic that can be used for a CairoHistogram. |
4414 | - Otherwise it interferes with the scrubbing mask code |
4415 | - """ |
4416 | - _saved_tooltip_location = None |
4417 | - def __init__(self, histogram, container): |
4418 | - super(TooltipEventBox, self).__init__() |
4419 | - self.add(histogram) |
4420 | - self.histogram = histogram |
4421 | - self.container = container |
4422 | - self.set_property("has-tooltip", True) |
4423 | - self.connect("query-tooltip", self.query_tooltip) |
4424 | - |
4425 | - def query_tooltip(self, widget, x, y, keyboard_mode, tooltip): |
4426 | - if y < self.histogram.get_size_request()[1] - self.histogram.bottom_padding: |
4427 | - location = self.histogram.get_datastore_index_from_cartesian(x, y) |
4428 | - if location != self._saved_tooltip_location: |
4429 | - # don't show the previous tooltip if we moved to another |
4430 | - # location |
4431 | - self._saved_tooltip_location = location |
4432 | - return False |
4433 | - try: |
4434 | - timestamp, count = self.histogram.get_datastore()[location] |
4435 | - except IndexError: |
4436 | - # there is no bar for at this location |
4437 | - # don't show a tooltip |
4438 | - return False |
4439 | - date = datetime.date.fromtimestamp(timestamp).strftime("%A, %d %B, %Y") |
4440 | - tooltip.set_text("%s\n%i %s" % (date, count, |
4441 | - gettext.ngettext("item", "items", count))) |
4442 | - elif self.container.histogram._today_text and _in_area(x, y, self.container.histogram._today_area): |
4443 | - tooltip.set_text(_("Click today to return to today")) |
4444 | - else: |
4445 | - return False |
4446 | - return True |
4447 | - |
4448 | - |
4449 | -class JournalHistogram(CairoHistogram): |
4450 | - """ |
4451 | - A subclass of CairoHistogram with theming to fit into Journal |
4452 | - """ |
4453 | - padding = 2 |
4454 | - column_radius = 1.3 |
4455 | - top_padding = 6 |
4456 | - bottom_padding = 29 |
4457 | - wcolumn = 10 |
4458 | - xincrement = wcolumn + padding |
4459 | - column_radius = 2 |
4460 | - stroke_width = 2 |
4461 | - stroke_offset = 1 |
4462 | - font_size = 12 |
4463 | - min_column_height = 2 |
4464 | - |
4465 | - def change_style(self, widget, *args, **kwargs): |
4466 | - self.colors = self.colors.copy() |
4467 | - self.colors["bg"] = get_gtk_rgba(self.style, "bg", 0) |
4468 | - self.colors["color"] = get_gtk_rgba(self.style, "base", 0) |
4469 | - self.colors["column_normal"] = get_gtk_rgba(self.style, "bg", 1) |
4470 | - self.colors["column_selected"] = get_gtk_rgba(self.style, "bg", 3) |
4471 | - self.colors["column_selected_alternative"] = get_gtk_rgba(self.style, "bg", 3, 0.7) |
4472 | - self.colors["column_alternative"] = get_gtk_rgba(self.style, "text", 2) |
4473 | - self.colors["stroke"] = get_gtk_rgba(self.style, "bg", 0) |
4474 | - self.colors["shadow"] = get_gtk_rgba(self.style, "bg", 0, 0.98) |
4475 | - self.font_size = self.style.font_desc.get_size()/1024 |
4476 | - self.bottom_padding = self.font_size + 9 + widget.style.ythickness |
4477 | - self.gc = self.style.text_gc[gtk.STATE_NORMAL] |
4478 | - self.pangofont = pango.FontDescription(self.font_name + " %d" % self.font_size) |
4479 | - self.pangofont.set_weight(pango.WEIGHT_BOLD) |
4480 | - |
4481 | - |
4482 | -class HistogramWidget(gtk.Viewport): |
4483 | - """ |
4484 | - A container for a CairoHistogram which allows you to scroll |
4485 | - """ |
4486 | - |
4487 | - |
4488 | - def __init__(self, histo_type, size = (600, 75)): |
4489 | - """ |
4490 | - :param histo_type: a :class:`CairoHistogram <CairoHistogram>` or a derivative |
4491 | - """ |
4492 | - super(HistogramWidget, self).__init__() |
4493 | - self.set_shadow_type(gtk.SHADOW_NONE) |
4494 | - self.histogram = histo_type() |
4495 | - self.eventbox = TooltipEventBox(self.histogram, self) |
4496 | - self.set_size_request(*size) |
4497 | - self.add(self.eventbox) |
4498 | - self.histogram.connect("button_press_event", self.footer_clicked) |
4499 | - self.histogram.connect("selection-set", self.scrubbing_fix) |
4500 | - self.histogram.queue_draw() |
4501 | - self.queue_draw() |
4502 | - |
4503 | - def footer_clicked(self, widget, event): |
4504 | - """ |
4505 | - Handles all rejected clicks from bellow the histogram internal view and |
4506 | - checks to see if they were inside of the today text |
4507 | - """ |
4508 | - hadjustment = self.get_hadjustment() |
4509 | - # Check for today button click |
4510 | - if (widget._today_text and event.x > hadjustment.value + hadjustment.page_size - widget._today_width): |
4511 | - self.histogram.change_location(len(self.histogram.get_datastore()) - 1) |
4512 | - return True |
4513 | - else: |
4514 | - pass # Drag here |
4515 | - return False |
4516 | - |
4517 | - def scroll_to_end(self, *args, **kwargs): |
4518 | - """ |
4519 | - Scroll to the end of the drawing area's viewport |
4520 | - """ |
4521 | - hadjustment = self.get_hadjustment() |
4522 | - hadjustment.set_value(1) |
4523 | - hadjustment.set_value(self.histogram.max_width - hadjustment.page_size) |
4524 | - |
4525 | - def scrubbing_fix(self, widget, i, ii): |
4526 | - """ |
4527 | - Allows scrubbing to scroll the scroll window |
4528 | - """ |
4529 | - hadjustment = self.get_hadjustment() |
4530 | - proposed_xa = ((i) * self.histogram.xincrement) + self.histogram.start_x_padding |
4531 | - proposed_xb = ((i + self.histogram.selected_range) * self.histogram.xincrement) + self.histogram.start_x_padding |
4532 | - if proposed_xa < hadjustment.value: |
4533 | - hadjustment.set_value(proposed_xa) |
4534 | - elif proposed_xb > hadjustment.value + hadjustment.page_size: |
4535 | - hadjustment.set_value(proposed_xb - hadjustment.page_size) |
4536 | |
4537 | === added file 'src/infopane.py' |
4538 | --- src/infopane.py 1970-01-01 00:00:00 +0000 |
4539 | +++ src/infopane.py 2010-05-04 05:28:16 +0000 |
4540 | @@ -0,0 +1,577 @@ |
4541 | +# -.- coding: utf-8 -.- |
4542 | +# |
4543 | +# Filename |
4544 | +# |
4545 | +# Copyright © 2010 Randal Barlow |
4546 | +# Copyright © 2010 Markus Korn <thekorn@gmx.de> |
4547 | +# |
4548 | +# This program is free software: you can redistribute it and/or modify |
4549 | +# it under the terms of the GNU General Public License as published by |
4550 | +# the Free Software Foundation, either version 3 of the License, or |
4551 | +# (at your option) any later version. |
4552 | +# |
4553 | +# This program is distributed in the hope that it will be useful, |
4554 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
4555 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
4556 | +# GNU General Public License for more details. |
4557 | +# |
4558 | +# You should have received a copy of the GNU General Public License |
4559 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
4560 | + |
4561 | +# Purpose: |
4562 | + |
4563 | +import gobject |
4564 | +import gtk |
4565 | +import mimetypes |
4566 | +import os |
4567 | +import pango |
4568 | +try: import gst |
4569 | +except ImportError: |
4570 | + gst = None |
4571 | +try: import gtksourceview2 |
4572 | +except ImportError: gtksourceview2 = None |
4573 | +import threading |
4574 | + |
4575 | +from zeitgeist.client import ZeitgeistClient |
4576 | +from zeitgeist.datamodel import Event, Subject, Interpretation, Manifestation, \ |
4577 | + ResultType, TimeRange |
4578 | + |
4579 | +import content_objects |
4580 | +from common import * |
4581 | +from gio_file import GioFile |
4582 | +import supporting_widgets |
4583 | + |
4584 | + |
4585 | +GENERIC_DISPLAY_NAME = "other" |
4586 | + |
4587 | +MIMETYPEMAP = { |
4588 | + GENERIC_DISPLAY_NAME : ("image", None), |
4589 | + #"multimedia" : ("video", "audio"), |
4590 | + #"text" : ("text",), |
4591 | +} |
4592 | + |
4593 | +CLIENT = ZeitgeistClient() |
4594 | + |
4595 | +def get_related_events_for_uri(uri, callback): |
4596 | + """ |
4597 | + :param uri: A uri for which to request related uris using zetigeist |
4598 | + :param callback: this callback is called once the events are retrieved for |
4599 | + the uris. It is called with a list of events. |
4600 | + """ |
4601 | + def _event_request_handler(uris): |
4602 | + """ |
4603 | + :param uris: a list of uris which are related to the windows current uri |
4604 | + Seif look here |
4605 | + """ |
4606 | + templates = [] |
4607 | + if len(uris) > 0: |
4608 | + for i, uri in enumerate(uris): |
4609 | + templates += [ |
4610 | + Event.new_for_values(interpretation=Interpretation.VISIT_EVENT.uri, subject_uri=uri), |
4611 | + Event.new_for_values(interpretation=Interpretation.MODIFY_EVENT.uri, subject_uri=uri), |
4612 | + Event.new_for_values(interpretation=Interpretation.CREATE_EVENT.uri, subject_uri=uri), |
4613 | + Event.new_for_values(interpretation=Interpretation.OPEN_EVENT.uri, subject_uri=uri) |
4614 | + ] |
4615 | + CLIENT.find_events_for_templates(templates, callback, |
4616 | + [0, time.time()*1000], num_events=50000, |
4617 | + result_type=ResultType.MostRecentSubjects) |
4618 | + |
4619 | + end = time.time() * 1000 |
4620 | + start = end - (86400*30*1000) |
4621 | + CLIENT.find_related_uris_for_uris([uri], _event_request_handler) |
4622 | + |
4623 | + |
4624 | +def get_media_type(gfile): |
4625 | + uri = gfile.uri |
4626 | + if not uri.startswith("file://") or not gfile: |
4627 | + return GENERIC_DISPLAY_NAME |
4628 | + majortype = gfile.mime_type.split("/")[0] |
4629 | + for key, mimes in MIMETYPEMAP.iteritems(): |
4630 | + if majortype in mimes: |
4631 | + return key |
4632 | + #if isinstance(gfile, GioFile): |
4633 | + # if "text-x-generic" in gfile.icon_names or "text-x-script" in gfile.icon_names: |
4634 | + # return "text" |
4635 | + return GENERIC_DISPLAY_NAME |
4636 | + |
4637 | + |
4638 | +class ContentDisplay(object): |
4639 | + """ |
4640 | + The abstract base class for content displays |
4641 | + """ |
4642 | + def set_content_object(self, obj): |
4643 | + """ |
4644 | + :param obj a content object which the Content Display displays |
4645 | + """ |
4646 | + pass |
4647 | + |
4648 | + def set_inactive(self): |
4649 | + """ |
4650 | + This method performs clean when the displays are swapped |
4651 | + """ |
4652 | + pass |
4653 | + |
4654 | + |
4655 | +class ScrolledDisplay(gtk.ScrolledWindow): |
4656 | + """ |
4657 | + A scrolled window container that acts as a proxy for a child |
4658 | + use type to make wrapers for your type |
4659 | + """ |
4660 | + child_type = gtk.Widget |
4661 | + def __init__(self): |
4662 | + super(ScrolledDisplay, self).__init__() |
4663 | + self._child_obj = self.child_type() |
4664 | + self.add(self._child_obj) |
4665 | + self.set_shadow_type(gtk.SHADOW_IN) |
4666 | + self.set_size_request(-1, 200) |
4667 | + |
4668 | + def set_content_object(self, obj): self._child_obj.set_content_object(obj) |
4669 | + def set_inactive(self): self._child_obj.set_inactive() |
4670 | + |
4671 | + |
4672 | +class TextDisplay(gtksourceview2.View if gtksourceview2 |
4673 | + else gtk.TextView, ContentDisplay): |
4674 | + """ |
4675 | + A text preview display which uses a sourceview or a textview if sourceview |
4676 | + modules are not found |
4677 | + """ |
4678 | + def __init__(self): |
4679 | + """""" |
4680 | + super(TextDisplay, self).__init__() |
4681 | + self.textbuffer = (gtksourceview2.Buffer() if gtksourceview2 |
4682 | + else gtk.TextBuffer()) |
4683 | + self.set_buffer(self.textbuffer) |
4684 | + self.set_editable(False) |
4685 | + font = pango.FontDescription() |
4686 | + font.set_family("Monospace") |
4687 | + self.modify_font(font) |
4688 | + if gtksourceview2: |
4689 | + self.manager = gtksourceview2.LanguageManager() |
4690 | + self.textbuffer.set_highlight_syntax(True) |
4691 | + |
4692 | + def get_language_from_mime_type(self, mime): |
4693 | + for id_ in self.manager.get_language_ids(): |
4694 | + temp_language = self.manager.get_language(id_) |
4695 | + if mime in temp_language.get_mime_types(): |
4696 | + return temp_language |
4697 | + return None |
4698 | + |
4699 | + def set_content_object(self, obj): |
4700 | + if obj: |
4701 | + content = obj.get_content() |
4702 | + self.textbuffer.set_text(content) |
4703 | + if gtksourceview2: |
4704 | + lang = self.get_language_from_mime_type(obj.mime_type) |
4705 | + self.textbuffer.set_language(lang) |
4706 | + |
4707 | + |
4708 | +class ImageDisplay(gtk.Image, ContentDisplay): |
4709 | + """ |
4710 | + A display based on GtkImage to display a uri's thumb or icon using GioFile |
4711 | + """ |
4712 | + def set_content_object(self, obj): |
4713 | + if obj: |
4714 | + if isinstance(obj, GioFile) and obj.has_preview(): |
4715 | + pixbuf = obj.get_thumbnail(size=SIZE_NORMAL, border=3) |
4716 | + else: |
4717 | + pixbuf = obj.get_icon(size=128) |
4718 | + self.set_from_pixbuf(pixbuf) |
4719 | + |
4720 | + |
4721 | +class MultimediaDisplay(gtk.VBox, ContentDisplay): |
4722 | + """ |
4723 | + a display which words for video and audio using gstreamer |
4724 | + """ |
4725 | + def __init__(self): |
4726 | + super(MultimediaDisplay, self).__init__() |
4727 | + self.playing = False |
4728 | + self.mediascreen = gtk.DrawingArea() |
4729 | + self.player = gst.element_factory_make("playbin", "player") |
4730 | + bus = self.player.get_bus() |
4731 | + bus.add_signal_watch() |
4732 | + bus.enable_sync_message_emission() |
4733 | + bus.connect("message", self.on_message) |
4734 | + bus.connect("sync-message::element", self.on_sync_message) |
4735 | + buttonbox = gtk.HBox() |
4736 | + self.playbutton = gtk.Button() |
4737 | + buttonbox.pack_start(self.playbutton, True, False) |
4738 | + self.playbutton.gtkimage = gtk.Image() |
4739 | + self.playbutton.add(self.playbutton.gtkimage) |
4740 | + self.playbutton.gtkimage.set_from_stock(gtk.STOCK_MEDIA_PAUSE, 2) |
4741 | + self.pack_start(self.mediascreen, True, True, 10) |
4742 | + self.pack_end(buttonbox, False, False) |
4743 | + self.playbutton.connect("clicked", self.on_play_click) |
4744 | + self.playbutton.set_relief(gtk.RELIEF_NONE) |
4745 | + self.connect("hide", self._handle_hide) |
4746 | + |
4747 | + def _handle_hide(self, widget): |
4748 | + self.player.set_state(gst.STATE_NULL) |
4749 | + |
4750 | + def set_playing(self): |
4751 | + """ |
4752 | + Set MultimediaDisplay.player's state to playing |
4753 | + """ |
4754 | + self.player.set_state(gst.STATE_PLAYING) |
4755 | + self.playbutton.gtkimage.set_from_stock(gtk.STOCK_MEDIA_PAUSE, 2) |
4756 | + self.playing = True |
4757 | + |
4758 | + def set_paused(self): |
4759 | + """ |
4760 | + Set MultimediaDisplay.player's state to paused |
4761 | + """ |
4762 | + self.player.set_state(gst.STATE_PAUSED) |
4763 | + self.playbutton.gtkimage.set_from_stock(gtk.STOCK_MEDIA_PLAY, 2) |
4764 | + self.playing = False |
4765 | + |
4766 | + |
4767 | + def set_content_object(self, obj): |
4768 | + if isinstance(obj, GioFile): |
4769 | + self.player.set_state(gst.STATE_NULL) |
4770 | + self.player.set_property("uri", obj.uri) |
4771 | + self.set_playing() |
4772 | + |
4773 | + def set_inactive(self): |
4774 | + self.player.set_state(gst.STATE_NULL) |
4775 | + self.playing = False |
4776 | + |
4777 | + def on_play_click(self, widget): |
4778 | + if self.playing: |
4779 | + return self.set_paused() |
4780 | + return self.set_playing() |
4781 | + |
4782 | + def on_sync_message(self, bus, message): |
4783 | + if message.structure is None: |
4784 | + return |
4785 | + message_name = message.structure.get_name() |
4786 | + if message_name == "prepare-xwindow-id": |
4787 | + imagesink = message.src |
4788 | + imagesink.set_property("force-aspect-ratio", True) |
4789 | + gtk.gdk.threads_enter() |
4790 | + try: |
4791 | + self.show_all() |
4792 | + imagesink.set_xwindow_id(self.mediascreen.window.xid) |
4793 | + finally: |
4794 | + gtk.gdk.threads_leave() |
4795 | + |
4796 | + def on_message(self, bus, message): |
4797 | + t = message.type |
4798 | + if t == gst.MESSAGE_EOS: |
4799 | + self.player.set_state(gst.STATE_NULL) |
4800 | + elif t == gst.MESSAGE_ERROR: |
4801 | + self.player.set_state(gst.STATE_NULL) |
4802 | + err, debug = message.parse_error() |
4803 | + print "Error: %s" % err, debug |
4804 | + |
4805 | + |
4806 | +class EventDataPane(gtk.Table): |
4807 | + x = 2 |
4808 | + y = 8 |
4809 | + |
4810 | + column_names = ( |
4811 | + _("Actor"), # 0 |
4812 | + _("Time"), # 1 |
4813 | + _(""), # 2 |
4814 | + _("Interpretation"), # 3 |
4815 | + _("Subject Interpretation"), # 4 |
4816 | + _("Manifestation"), |
4817 | + ) |
4818 | + |
4819 | + |
4820 | + def __init__(self): |
4821 | + super(EventDataPane, self).__init__(self.x, self.y) |
4822 | + self.set_col_spacings(4) |
4823 | + self.set_row_spacings(4) |
4824 | + self.labels = [] |
4825 | + for i, name in enumerate(self.column_names): |
4826 | + #for i in xrange(len(self.column_names)): |
4827 | + namelabel = gtk.Label() |
4828 | + if name: |
4829 | + namelabel.set_markup("<b>" + name + ":</b>") |
4830 | + namelabel.set_alignment(0, 0) |
4831 | + self.attach(namelabel, 0, 1, i, i+1) |
4832 | + label = gtk.Label() |
4833 | + label.set_alignment(0, 0) |
4834 | + self.attach(label, 1, 2, i, i+1) |
4835 | + self.labels.append(label) |
4836 | + |
4837 | + def set_content_object(self, obj): |
4838 | + event = obj.event |
4839 | + # Actor |
4840 | + desktop_file = obj.get_actor_desktop_file() |
4841 | + if desktop_file: |
4842 | + actor = desktop_file.getName() |
4843 | + else: actor = event.actor |
4844 | + self.labels[0].set_text(actor) |
4845 | + # Time |
4846 | + local_t = time.localtime(int(event.timestamp)/1000) |
4847 | + time_str = time.strftime("%b %d %Y %H:%M:%S", local_t) |
4848 | + self.labels[1].set_text(time_str) |
4849 | + #self.labels[2].set_text(event.subjects[0].uri) |
4850 | + # Interpetation |
4851 | + try: interpretation_name = Interpretation[event.interpretation].display_name |
4852 | + except KeyError: interpretation_name = "" |
4853 | + self.labels[3].set_text(interpretation_name) |
4854 | + # Subject Interpetation |
4855 | + try: subject_interpretation_name = Interpretation[event.subjects[0].interpretation].display_name |
4856 | + except KeyError: subject_interpretation_name = "" |
4857 | + self.labels[4].set_text(subject_interpretation_name) |
4858 | + # Manifestation |
4859 | + try: manifestation_name = Manifestation[event.manifestation].display_name |
4860 | + except KeyError: manifestation_name = "" |
4861 | + self.labels[5].set_text(manifestation_name) |
4862 | + |
4863 | + |
4864 | +class InformationPane(gtk.VBox): |
4865 | + """ |
4866 | + . . . . . . . . |
4867 | + . . |
4868 | + . Info . |
4869 | + . . |
4870 | + . . |
4871 | + . . . . . . . . |
4872 | + |
4873 | + Holds widgets which display information about a uri |
4874 | + """ |
4875 | + displays = { |
4876 | + GENERIC_DISPLAY_NAME : ImageDisplay, |
4877 | + "multimedia" : MultimediaDisplay if gst else ImageDisplay, |
4878 | + "text" : type("TextScrolledWindow", (ScrolledDisplay,), |
4879 | + {"child_type" : TextDisplay}), |
4880 | + } |
4881 | + |
4882 | + obj = None |
4883 | + |
4884 | + def __init__(self): |
4885 | + super(InformationPane, self).__init__() |
4886 | + vbox = gtk.VBox() |
4887 | + buttonhbox = gtk.HBox() |
4888 | + self.box = gtk.Frame() |
4889 | + self.label = gtk.Label() |
4890 | + self.pathlabel = gtk.Label() |
4891 | + labelvbox = gtk.VBox() |
4892 | + labelvbox.pack_start(self.label) |
4893 | + labelvbox.pack_end(self.pathlabel) |
4894 | + self.openbutton = gtk.Button(stock=gtk.STOCK_OPEN) |
4895 | + self.displays = self.displays.copy() |
4896 | + #self.set_shadow_type(gtk.SHADOW_NONE) |
4897 | + #self.set_label_widget(labelvbox) |
4898 | + self.pack_start(labelvbox) |
4899 | + self.box.set_shadow_type(gtk.SHADOW_NONE) |
4900 | + buttonhbox.pack_end(self.openbutton, False, False, 5) |
4901 | + buttonhbox.set_border_width(5) |
4902 | + vbox.pack_start(self.box, True, True) |
4903 | + vbox.pack_end(buttonhbox, False, False) |
4904 | + #self.set_label_align(0.5, 0.5) |
4905 | + #self.label.set_ellipsize(pango.ELLIPSIZE_MIDDLE) |
4906 | + #self.pathlabel.set_size_request(100, -1) |
4907 | + #self.pathlabel.set_size_request(300, -1) |
4908 | + self.pathlabel.set_ellipsize(pango.ELLIPSIZE_MIDDLE) |
4909 | + def _launch(w): |
4910 | + self.obj.launch() |
4911 | + self.openbutton.connect("clicked", _launch) |
4912 | + |
4913 | + #self.datapane = EventDataPane() |
4914 | + #vbox.pack_end(self.datapane, False, False) |
4915 | + self.add(vbox) |
4916 | + self.show_all() |
4917 | + |
4918 | + def set_displaytype(self, obj): |
4919 | + """ |
4920 | + Determines the ContentDisplay to use for a given uri |
4921 | + """ |
4922 | + media_type = get_media_type(obj) |
4923 | + display_widget = self.displays[media_type] |
4924 | + if isinstance(display_widget, type): |
4925 | + display_widget = self.displays[media_type] = display_widget() |
4926 | + if display_widget.parent != self.box: |
4927 | + child = self.box.get_child() |
4928 | + if child: |
4929 | + self.box.remove(child) |
4930 | + child.set_inactive() |
4931 | + self.box.add(display_widget) |
4932 | + display_widget.set_content_object(obj) |
4933 | + self.show_all() |
4934 | + |
4935 | + def set_content_object(self, obj): |
4936 | + self.obj = obj |
4937 | + self.set_displaytype(obj) |
4938 | + self.label.set_markup("<span size='12336'>" + obj.text.replace("&", "&") + "</span>") |
4939 | + self.pathlabel.set_markup("<span color='#979797'>" + obj.uri + "</span>") |
4940 | + #self.datapane.set_content_object(obj) |
4941 | + |
4942 | + def set_inactive(self): |
4943 | + display = self.box.get_child() |
4944 | + if display: display.set_inactive() |
4945 | + |
4946 | + |
4947 | +class RelatedPane(gtk.TreeView): |
4948 | + """ |
4949 | + . . . |
4950 | + . . |
4951 | + . . <--- Related files |
4952 | + . . |
4953 | + . . |
4954 | + . . . |
4955 | + |
4956 | + Displays related events using a widget based on gtk.TreeView |
4957 | + """ |
4958 | + def __init__(self): |
4959 | + super(RelatedPane, self).__init__() |
4960 | + self.popupmenu = supporting_widgets.ContextMenu |
4961 | + self.connect("button-press-event", self.on_button_press) |
4962 | + self.connect("row-activated", self.row_activated) |
4963 | + pcolumn = gtk.TreeViewColumn(_("Related Items")) |
4964 | + pixbuf_render = gtk.CellRendererPixbuf() |
4965 | + pcolumn.pack_start(pixbuf_render, False) |
4966 | + pcolumn.set_cell_data_func(pixbuf_render, self.celldatamethod, "pixbuf") |
4967 | + text_render = gtk.CellRendererText() |
4968 | + text_render.set_property("ellipsize", pango.ELLIPSIZE_MIDDLE) |
4969 | + pcolumn.pack_end(text_render, True) |
4970 | + pcolumn.set_cell_data_func(text_render, self.celldatamethod, "text") |
4971 | + self.append_column(pcolumn) |
4972 | + #self.set_headers_visible(False) |
4973 | + |
4974 | + def celldatamethod(self, column, cell, model, iter_, user_data): |
4975 | + if model: |
4976 | + obj = model.get_value(iter_, 0) |
4977 | + if user_data == "text": |
4978 | + cell.set_property("text", obj.text.replace("&", "&")) |
4979 | + elif user_data == "pixbuf": |
4980 | + cell.set_property("pixbuf", obj.icon) |
4981 | + |
4982 | + def _set_model_in_thread(self, events): |
4983 | + """ |
4984 | + A threaded which generates pixbufs and emblems for a list of events. |
4985 | + It takes those properties and appends them to the view's model |
4986 | + """ |
4987 | + lock = threading.Lock() |
4988 | + self.active_list = [] |
4989 | + liststore = gtk.ListStore(gobject.TYPE_PYOBJECT) |
4990 | + gtk.gdk.threads_enter() |
4991 | + self.set_model(liststore) |
4992 | + gtk.gdk.threads_leave() |
4993 | + for event in events: |
4994 | + obj = content_objects.choose_content_object(event) |
4995 | + if not obj: continue |
4996 | + gtk.gdk.threads_enter() |
4997 | + lock.acquire() |
4998 | + self.active_list.append(False) |
4999 | + liststore.append((obj,)) |
5000 | + lock.release() |
The diff has been truncated for viewing.