Merge lp:~gnome-zeitgeist/gnome-activity-journal/new-core into lp:gnome-activity-journal

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
Reviewer Review Type Date Requested Status
Randal Barlow Pending
Review via email: mp+24652@code.launchpad.net
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
=== added file 'data/multiview_icon.png'
0Binary files data/multiview_icon.png 1970-01-01 00:00:00 +0000 and data/multiview_icon.png 2010-05-04 05:28:16 +0000 differ0Binary 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
=== added file 'data/thumbview_icon.png'
1Binary files data/thumbview_icon.png 1970-01-01 00:00:00 +0000 and data/thumbview_icon.png 2010-05-04 05:28:16 +0000 differ1Binary 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
=== added file 'data/timelineview_icon.png'
2Binary files data/timelineview_icon.png 1970-01-01 00:00:00 +0000 and data/timelineview_icon.png 2010-05-04 05:28:16 +0000 differ2Binary 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
=== added file 'src/activityviews.py'
--- src/activityviews.py 1970-01-01 00:00:00 +0000
+++ src/activityviews.py 2010-05-04 05:28:16 +0000
@@ -0,0 +1,1159 @@
1# -.- coding: utf-8 -.-
2#
3# GNOME Activity Journal
4#
5# Copyright © 2009-2010 Seif Lotfy <seif@lotfy.com>
6# Copyright © 2010 Randal Barlow <email.tehk@gmail.com>
7# Copyright © 2010 Siegfried Gevatter <siegfried@gevatter.com>
8# Copyright © 2010 Markus Korn <thekorn@gmx.de>
9#
10# This program is free software: you can redistribute it and/or modify
11# it under the terms of the GNU General Public License as published by
12# the Free Software Foundation, either version 3 of the License, or
13# (at your option) any later version.
14#
15# This program is distributed in the hope that it will be useful,
16# but WITHOUT ANY WARRANTY; without even the implied warranty of
17# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18# GNU General Public License for more details.
19#
20# You should have received a copy of the GNU General Public License
21# along with this program. If not, see <http://www.gnu.org/licenses/>.
22
23import datetime
24import gobject
25import gst
26import gtk
27import math
28import pango
29import threading
30
31from bookmarker import bookmarker
32from common import *
33import content_objects
34from config import settings
35from sources import SUPPORTED_SOURCES
36from store import ContentStruct, CLIENT
37from supporting_widgets import DayLabel, ContextMenu, StaticPreviewTooltip, VideoPreviewTooltip, Pane
38
39from zeitgeist.datamodel import ResultType, StorageState, TimeRange
40
41
42class _GenericViewWidget(gtk.VBox):
43 day = None
44
45 def __init__(self):
46 gtk.VBox.__init__(self)
47 self.daylabel = DayLabel()
48 self.pack_start(self.daylabel, False, False)
49 self.connect("style-set", self.change_style)
50
51 def set_day(self, day, store):
52 self.store = store
53 if self.day:
54 self.day.disconnect(self.day_signal_id)
55 self.day = day
56 self.day_signal_id = self.day.connect("update", self.update_day)
57 self.update_day(day)
58
59 def update_day(self, day):
60 self.daylabel.set_date(day.date)
61 self.view.set_day(self.day)
62
63 def click(self, widget, event):
64 if event.button in (1, 3):
65 self.emit("unfocus-day")
66
67 def change_style(self, widget, style):
68 rc_style = self.style
69 color = rc_style.bg[gtk.STATE_NORMAL]
70 color = shade_gdk_color(color, 102/100.0)
71 self.view.modify_bg(gtk.STATE_NORMAL, color)
72 self.view.modify_base(gtk.STATE_NORMAL, color)
73
74
75#####################
76## MultiView code
77## This doesnt work, No idea why
78#####################
79
80class MultiViewContainer(gtk.HBox):
81
82 days = []
83 num_pages = 3
84 day_signal_id = [None] * num_pages
85
86 def __init__(self):
87 super(MultiViewContainer, self).__init__()
88 self.pages = []
89 for i in range(self.num_pages):
90 group = DayViewContainer()
91 evbox = gtk.EventBox()
92 evbox.add(group)
93 self.pages.append(group)
94 padding = 6 if i != self.num_pages-1 and i != 0 else 0
95 self.pack_start(evbox, True, True, padding)
96 self.connect("style-set", self.change_style)
97
98 def set_day(self, day, store):
99 if self.days:
100 for i, _day in enumerate(self.__days(self.days[0], store)):
101 signal = self.day_signal_id[i]
102 if signal:
103 _day.disconnect(signal)
104 self.days = self.__days(day, store)
105 for i, day in enumerate(self.days):
106 self.day_signal_id[i] = day.connect("update", self.update_day)
107 self.update_day()
108
109 def __days(self, day, store):
110 days = []
111 for i in range(self.num_pages):
112 days += [day]
113 day = day.previous(store)
114 return days
115
116 def update_day(self, *args):
117 #print "UPDATED", i, dayobj
118 #if i != None and dayobj:
119 # print "DO IT"
120 # return self.pages[i].set_day(dayobj)
121 for page, day in map(None, reversed(self.pages), self.days):
122 page.set_day(day)
123
124 def change_style(self, this, old_style):
125 style = this.style
126 for widget in self:
127 color = style.bg[gtk.STATE_NORMAL]
128 bgcolor = shade_gdk_color(color, 102/100.0)
129 widget.modify_bg(gtk.STATE_NORMAL, bgcolor)
130
131
132
133class DayViewContainer(gtk.VBox):
134 event_templates = (
135 Event.new_for_values(interpretation=Interpretation.VISIT_EVENT.uri),
136 Event.new_for_values(interpretation=Interpretation.MODIFY_EVENT.uri),
137 Event.new_for_values(interpretation=Interpretation.CREATE_EVENT.uri),
138 Event.new_for_values(interpretation=Interpretation.OPEN_EVENT.uri),
139 )
140 # Do day label stuff here please
141 def __init__(self):
142 super(DayViewContainer, self).__init__()
143 self.daylabel = DayLabel()
144 self.pack_start(self.daylabel, False, False)
145 self.dayviews = (DayView(_("Morning")), DayView(_("Afternoon")), DayView(_("Evening")))
146 self.scrolled_window = gtk.ScrolledWindow()
147 self.scrolled_window.set_shadow_type(gtk.SHADOW_NONE)
148 viewport = gtk.Viewport()
149 viewport.set_shadow_type(gtk.SHADOW_NONE)
150 box = gtk.VBox()
151 for dayview in self.dayviews:
152 box.pack_start(dayview, False, False)
153 viewport.add(box)
154 self.scrolled_window.add(viewport)
155
156 self.pack_end(self.scrolled_window, True, True)
157 self.scrolled_window.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
158 self.show_all()
159
160 def set_day(self, day):
161 self.daylabel.set_date(day.date)
162 morning = []
163 afternoon = []
164 evening = []
165 for item in day.filter(self.event_templates, result_type=ResultType.MostRecentSubjects):
166 if not item.content_object:continue
167 t = time.localtime(int(item.event.timestamp)/1000)
168 if t.tm_hour < 11:
169 morning.append(item)
170 elif t.tm_hour < 17:
171 afternoon.append(item)
172 else:
173 evening.append(item)
174 self.dayviews[0].set_items(morning)
175 self.dayviews[1].set_items(afternoon)
176 self.dayviews[2].set_items(evening)
177
178
179class DayView(gtk.VBox):
180
181 def __init__(self, title=""):
182 super(DayView, self).__init__()
183 #self.add(gtk.Button("LOL"))
184 # Create the title label
185 self.label = gtk.Label(title)
186 self.label.set_alignment(0.03, 0.5)
187 self.pack_start(self.label, False, False, 6)
188 # Create the main container
189 self.view = None
190
191 # Connect to relevant signals
192 self.connect("style-set", self.on_style_change)
193 self.show_all()
194 # Populate the widget with content
195
196 def on_style_change(self, widget, style):
197 """ Update used colors according to the system theme. """
198 color = self.style.bg[gtk.STATE_NORMAL]
199 fcolor = self.style.fg[gtk.STATE_NORMAL]
200 color = combine_gdk_color(color, fcolor)
201 self.label.modify_fg(gtk.STATE_NORMAL, color)
202
203 def clear(self):
204 if self.view:
205 self.remove(self.view)
206 self.view.destroy()
207 del self.view
208 self.view = gtk.VBox()
209 self.pack_start(self.view)
210
211 def set_items(self, items):
212 self.clear()
213 categories = {}
214 for struct in items:
215 if not struct.content_object: continue
216 subject = struct.event.subjects[0]
217 if not categories.has_key(subject.interpretation):
218 categories[subject.interpretation] = []
219 categories[subject.interpretation].append(struct)
220 if not categories:
221 self.hide_all()
222 else:
223 ungrouped_events = []
224 for key in sorted(categories.iterkeys()):
225 events = categories[key]
226 if len(events) > 3:
227 box = CategoryBox(key, list(reversed(events)))
228 self.view.pack_start(box)
229 else:
230 ungrouped_events += events
231 box = CategoryBox(None, ungrouped_events)
232 self.view.pack_start(box)
233 self.show_all()
234
235
236class CategoryBox(gtk.HBox):
237
238 def __init__(self, category, event_structs, pinnable = False):
239 super(CategoryBox, self).__init__()
240 self.view = gtk.VBox(True)
241 self.vbox = gtk.VBox()
242 for struct in event_structs:
243 if not struct.content_object:continue
244 item = Item(struct, pinnable)
245 hbox = gtk.HBox ()
246 #label = gtk.Label("")
247 #hbox.pack_start(label, False, False, 7)
248 hbox.pack_start(item, True, True, 0)
249 self.view.pack_start(hbox, False, False, 0)
250 hbox.show_all()
251 #label.show()
252 self.pack_end(hbox)
253
254 # If this isn't a set of ungrouped events, give it a label
255 if category:
256 # Place the items into a box and simulate left padding
257 self.box = gtk.HBox()
258 #label = gtk.Label("")
259 self.box.pack_start(self.view)
260
261 hbox = gtk.HBox()
262 # Add the title button
263 if category in SUPPORTED_SOURCES:
264 text = SUPPORTED_SOURCES[category].group_label(len(event_structs))
265 else:
266 text = "Unknown"
267
268 label = gtk.Label()
269 label.set_markup("<span>%s</span>" % text)
270 #label.set_ellipsize(pango.ELLIPSIZE_END)
271
272 hbox.pack_start(label, True, True, 0)
273
274 label = gtk.Label()
275 label.set_markup("<span>(%d)</span>" % len(event_structs))
276 label.set_alignment(1.0,0.5)
277 label.set_alignment(1.0,0.5)
278 hbox.pack_end(label, False, False, 2)
279
280 hbox.set_border_width(3)
281
282 self.expander = gtk.Expander()
283 self.expander.set_label_widget(hbox)
284
285 self.vbox.pack_start(self.expander, False, False)
286 self.expander.add(self.box)#
287
288 self.pack_start(self.vbox, True, True, 24)
289
290 self.expander.show_all()
291 self.show()
292 hbox.show_all()
293 label.show_all()
294 self.view.show()
295
296 else:
297 self.box = self.view
298 self.vbox.pack_end(self.box)
299 self.box.show()
300 self.show()
301
302 self.pack_start(self.vbox, True, True, 16)
303
304 self.show_all()
305
306 def on_toggle(self, view, bool):
307 if bool:
308 self.box.show()
309 else:
310 self.box.hide()
311 pinbox.show_all()
312
313
314class Item(gtk.HBox):
315
316 def __init__(self, content_struct, allow_pin = False):
317 event = content_struct.event
318 gtk.HBox.__init__(self)
319 self.set_border_width(2)
320 self.allow_pin = allow_pin
321 self.btn = gtk.Button()
322 self.search_results = []
323 self.in_search = False
324 self.subject = event.subjects[0]
325 self.content_obj = content_struct.content_object
326 # self.content_obj = GioFile.create(self.subject.uri)
327 self.time = float(event.timestamp) / 1000
328 self.time = time.strftime("%H:%M", time.localtime(self.time))
329
330 if self.content_obj is not None:
331 self.icon = self.content_obj.get_icon(
332 can_thumb=settings.get('small_thumbnails', False), border=0)
333 else:
334 self.icon = None
335 self.btn.set_relief(gtk.RELIEF_NONE)
336 self.btn.set_focus_on_click(False)
337 self.__init_widget()
338 self.show_all()
339 self.markup = None
340
341 def __init_widget(self):
342 self.label = gtk.Label()
343 text = self.content_obj.text.replace("&", "&amp;")
344 self.label.set_markup(text)
345 self.label.set_ellipsize(pango.ELLIPSIZE_MIDDLE)
346 self.label.set_alignment(0.0, 0.5)
347
348 if self.icon: img = gtk.image_new_from_pixbuf(self.icon)
349 else: img = None
350 hbox = gtk.HBox()
351 if img: hbox.pack_start(img, False, False, 1)
352 hbox.pack_start(self.label, True, True, 4)
353
354 if self.allow_pin:
355 # TODO: get the name "pin" from theme when icons are properly installed
356 img = gtk.image_new_from_file(get_icon_path("hicolor/24x24/status/pin.png"))
357 self.pin = gtk.Button()
358 self.pin.add(img)
359 self.pin.set_tooltip_text(_("Remove Pin"))
360 self.pin.set_focus_on_click(False)
361 self.pin.set_relief(gtk.RELIEF_NONE)
362 self.pack_end(self.pin, False, False)
363 self.pin.connect("clicked", lambda x: self.set_bookmarked(False))
364 #hbox.pack_end(img, False, False)
365 evbox = gtk.EventBox()
366 self.btn.add(hbox)
367 evbox.add(self.btn)
368 self.pack_start(evbox)
369
370 self.btn.connect("clicked", self.launch)
371 self.btn.connect("button_press_event", self._show_item_popup)
372
373 def realize_cb(widget):
374 evbox.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.HAND2))
375
376 self.btn.connect("realize", realize_cb)
377
378 self.init_multimedia_tooltip()
379
380 def init_multimedia_tooltip(self):
381 """add multimedia tooltip to multimedia files
382 multimedia tooltip is shown for all images, all videos and pdfs
383
384 TODO: make loading of multimedia thumbs async
385 """
386 if isinstance(self.content_obj, GioFile) and self.content_obj.has_preview():
387 icon_names = self.content_obj.icon_names
388 self.set_property("has-tooltip", True)
389 self.connect("query-tooltip", self._handle_tooltip)
390 if "video-x-generic" in icon_names and gst is not None:
391 self.set_tooltip_window(VideoPreviewTooltip)
392 else:
393 self.set_tooltip_window(StaticPreviewTooltip)
394
395 def _handle_tooltip(self, widget, x, y, keyboard_mode, tooltip):
396 # nothing to do here, we always show the multimedia tooltip
397 # if we like video/sound preview later on we can start them here
398 tooltip_window = self.get_tooltip_window()
399 return tooltip_window.preview(self.content_obj)
400
401 def _show_item_popup(self, widget, ev):
402 if ev.button == 3:
403 items = [self.content_obj]
404 ContextMenu.do_popup(ev.time, items)
405
406 def set_bookmarked(self, bool_):
407 uri = unicode(self.subject.uri)
408 if bool_:
409 bookmarker.bookmark(uri)
410 else:
411 bookmarker.unbookmark(uri)
412
413
414 def launch(self, *discard):
415 if self.content_obj is not None:
416 self.content_obj.launch()
417
418
419#####################
420## ThumbView code
421#####################
422class ThumbViewContainer(_GenericViewWidget):
423 day_signal_id = None
424
425 def __init__(self):
426 _GenericViewWidget.__init__(self)
427 self.scrolledwindow = gtk.ScrolledWindow()
428 self.scrolledwindow.set_shadow_type(gtk.SHADOW_NONE)
429 self.scrolledwindow.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
430 self.view = ThumbView()
431 self.scrolledwindow.add_with_viewport(self.view)
432 self.scrolledwindow.get_children()[0].set_shadow_type(gtk.SHADOW_NONE)
433 self.pack_end(self.scrolledwindow)
434 self.show_all()
435
436
437class _ThumbViewRenderer(gtk.GenericCellRenderer):
438 """
439 A IconView renderer to be added to a cellayout. It displays a pixbuf and
440 data based on the event property
441 """
442
443 __gtype_name__ = "_ThumbViewRenderer"
444 __gproperties__ = {
445 "content_obj" :
446 (gobject.TYPE_PYOBJECT,
447 "event to be displayed",
448 "event to be displayed",
449 gobject.PARAM_READWRITE,
450 ),
451 }
452
453 width = 96
454 height = 72
455 properties = {}
456
457 @property
458 def content_obj(self):
459 return self.get_property("content_obj")
460
461 @property
462 def emblems(self):
463 return self.content_obj.emblems
464
465 @property
466 def pixbuf(self):
467 return self.content_obj.thumbview_pixbuf
468
469 @property
470 def event(self):
471 return self.content_obj.event
472
473 def __init__(self):
474 super(_ThumbViewRenderer, self).__init__()
475 self.properties = {}
476 self.set_fixed_size(self.width, self.height)
477 self.set_property("mode", gtk.CELL_RENDERER_MODE_ACTIVATABLE)
478
479 def do_set_property(self, pspec, value):
480 self.properties[pspec.name] = value
481
482 def do_get_property(self, pspec):
483 return self.properties[pspec.name]
484
485 def on_get_size(self, widget, area):
486 if area:
487 #return (area.x, area.y, area.width, area.height)
488 return (0, 0, area.width, area.height)
489 return (0,0,0,0)
490
491 def on_render(self, window, widget, background_area, cell_area, expose_area, flags):
492 """
493 The primary rendering function. It calls either the classes rendering functions
494 or special one defined in the rendering_functions dict
495 """
496 x = cell_area.x
497 y = cell_area.y
498 w = cell_area.width
499 h = cell_area.height
500 pixbuf_w = self.pixbuf.get_width() if self.pixbuf else 0
501 pixbuf_h = self.pixbuf.get_height() if self.pixbuf else 0
502 if (pixbuf_w, pixbuf_h) == content_objects.SIZE_THUMBVIEW:
503 render_pixbuf(window, x, y, self.pixbuf)
504 else:
505 self.file_render_pixbuf(window, widget, x, y, w, h)
506 render_emblems(window, x, y, w, h, self.emblems)
507 path = widget.get_path_at_pos(cell_area.x, cell_area.y)
508 if path != None:
509 try:
510 if widget.active_list[path[0]]:
511 gobject.timeout_add(2, self.render_info_box, window, widget, cell_area, expose_area, self.event)
512 except:pass
513 return True
514
515 @staticmethod
516 def insert_file_markup(text):
517 text = text.replace("&", "&amp;")
518 text = "<span size='6400'>" + text + "</span>"
519 return text
520
521 def file_render_pixbuf(self, window, widget, x, y, w, h):
522 """
523 Renders a icon and file name for non-thumb objects
524 """
525 context = window.cairo_create()
526 pixbuf = self.pixbuf
527 if pixbuf:
528 imgw, imgh = pixbuf.get_width(), pixbuf.get_height()
529 ix = x + (self.width - imgw)
530 iy = y + self.height - imgh
531 context.rectangle(x, y, w, h)
532 context.set_source_rgb(1, 1, 1)
533 context.fill_preserve()
534 if pixbuf:
535 context.set_source_pixbuf(pixbuf, ix, iy)
536 context.fill()
537 draw_frame(context, x, y, w, h)
538 context = window.cairo_create()
539 text = self.insert_file_markup(self.content_obj.thumbview_text)
540
541 layout = widget.create_pango_layout(text)
542 draw_text(context, layout, text, x+5, y+5, self.width-10)
543
544 @staticmethod
545 def render_info_box(window, widget, cell_area, expose_area, event):
546 """
547 Renders a info box when the item is active
548 """
549 x = cell_area.x
550 y = cell_area.y - 10
551 w = cell_area.width
552 h = cell_area.height
553 context = window.cairo_create()
554 t0 = get_event_typename(event)
555 t1 = event.subjects[0].text
556 text = ("<span size='10240'>%s</span>\n<span size='8192'>%s</span>" % (t0, t1)).replace("&", "&amp;")
557 layout = widget.create_pango_layout(text)
558 layout.set_markup(text)
559 textw, texth = layout.get_pixel_size()
560 popuph = max(h/3 + 5, texth)
561 nw = w + 26
562 x = x - (nw - w)/2
563 width, height = window.get_geometry()[2:4]
564 popupy = min(y+h+10, height-popuph-5-1) - 5
565 draw_speech_bubble(context, layout, x, popupy, nw, popuph)
566 context.fill()
567 return False
568
569 def on_start_editing(self, event, widget, path, background_area, cell_area, flags):
570 pass
571
572 def on_activate(self, event, widget, path, background_area, cell_area, flags):
573 self.content_obj.launch()
574 return True
575
576
577class ThumbIconView(gtk.IconView):
578 """
579 A iconview which uses a custom cellrenderer to render square pixbufs
580 based on zeitgeist events
581 """
582 last_active = -1
583 child_width = _ThumbViewRenderer.width
584 child_height = _ThumbViewRenderer.height
585 def __init__(self):
586 super(ThumbIconView, self).__init__()
587 self.active_list = []
588 self.popupmenu = ContextMenu
589 self.add_events(gtk.gdk.LEAVE_NOTIFY_MASK)
590 self.connect("button-press-event", self.on_button_press)
591 self.connect("motion-notify-event", self.on_motion_notify)
592 self.connect("leave-notify-event", self.on_leave_notify)
593 self.set_selection_mode(gtk.SELECTION_NONE)
594 self.set_column_spacing(6)
595 self.set_row_spacing(6)
596 pcolumn = gtk.TreeViewColumn("Preview")
597 render = _ThumbViewRenderer()
598 self.pack_end(render)
599 self.add_attribute(render, "content_obj", 0)
600 self.set_margin(10)
601
602 def _set_model_in_thread(self, items):
603 """
604 A threaded which generates pixbufs and emblems for a list of events.
605 It takes those properties and appends them to the view's model
606 """
607 lock = threading.Lock()
608 self.active_list = []
609 liststore = gtk.ListStore(gobject.TYPE_PYOBJECT)
610 gtk.gdk.threads_enter()
611 self.set_model(liststore)
612 gtk.gdk.threads_leave()
613
614 for item in items:
615 obj = item.content_object
616 if not obj: continue
617 gtk.gdk.threads_enter()
618 lock.acquire()
619 self.active_list.append(False)
620 liststore.append((obj,))
621 lock.release()
622 gtk.gdk.threads_leave()
623
624 def set_model_from_list(self, items):
625 """
626 Sets creates/sets a model from a list of zeitgeist events
627 :param events: a list of :class:`Events <zeitgeist.datamodel.Event>`
628 """
629 self.last_active = -1
630 if not items:
631 self.set_model(None)
632 return
633 thread = threading.Thread(target=self._set_model_in_thread, args=(items,))
634 thread.start()
635
636 def on_button_press(self, widget, event):
637 if event.button == 3:
638 val = self.get_item_at_pos(int(event.x), int(event.y))
639 if val:
640 path, cell = val
641 model = self.get_model()
642 obj = model[path[0]][0]
643 self.popupmenu.do_popup(event.time, [obj])
644 return False
645
646 def on_leave_notify(self, widget, event):
647 try:
648 self.active_list[self.last_active] = False
649 except IndexError:pass
650 self.last_active = -1
651 self.queue_draw()
652
653 def on_motion_notify(self, widget, event):
654 val = self.get_item_at_pos(int(event.x), int(event.y))
655 if val:
656 path, cell = val
657 if path[0] != self.last_active:
658 self.active_list[self.last_active] = False
659 self.active_list[path[0]] = True
660 self.last_active = path[0]
661 self.queue_draw()
662 return True
663
664 def query_tooltip(self, widget, x, y, keyboard_mode, tooltip):
665 """
666 Displays a tooltip based on x, y
667 """
668 path = self.get_path_at_pos(int(x), int(y))
669 if path:
670 model = self.get_model()
671 uri = model[path[0]][3].uri
672 interpretation = model[path[0]][3].subjects[0].interpretation
673 tooltip_window = widget.get_tooltip_window()
674 if interpretation == Interpretation.VIDEO.uri:
675 self.set_tooltip_window(VideoPreviewTooltip)
676 else:
677 self.set_tooltip_window(StaticPreviewTooltip)
678 gio_file = GioFile.create(uri)
679 return tooltip_window.preview(gio_file)
680 return False
681
682
683class ThumbView(gtk.VBox):
684 """
685 A container for three image views representing periods in time
686 """
687 event_templates = (
688 Event.new_for_values(interpretation=Interpretation.VISIT_EVENT.uri),
689 Event.new_for_values(interpretation=Interpretation.MODIFY_EVENT.uri),
690 Event.new_for_values(interpretation=Interpretation.CREATE_EVENT.uri),
691 Event.new_for_values(interpretation=Interpretation.OPEN_EVENT.uri),
692 )
693 def __init__(self):
694 """Woo"""
695 gtk.VBox.__init__(self)
696 self.views = [ThumbIconView() for x in xrange(3)]
697 self.labels = [gtk.Label() for x in xrange(3)]
698 for i in xrange(3):
699 text = TIMELABELS[i]
700 line = 50 - len(text)
701 self.labels[i].set_markup(
702 "\n <span size='10336'>%s <s>%s</s></span>" % (text, " "*line))
703 self.labels[i].set_justify(gtk.JUSTIFY_RIGHT)
704 self.labels[i].set_alignment(0, 0)
705 self.pack_start(self.labels[i], False, False)
706 self.pack_start(self.views[i], False, False)
707 self.connect("style-set", self.change_style)
708
709 def set_phase_items(self, i, items):
710 """
711 Set a time phases events
712
713 :param i: a index for the three items in self.views. 0:Morning,1:AfterNoon,2:Evening
714 :param events: a list of :class:`Events <zeitgeist.datamodel.Event>`
715 """
716 view = self.views[i]
717 label = self.labels[i]
718 if not items or len(items) == 0:
719 view.set_model_from_list(None)
720 return False
721 view.show_all()
722 label.show_all()
723 view.set_model_from_list(items)
724
725 if len(items) == 0:
726 view.hide_all()
727 label.hide_all()
728
729 def set_day(self, day):
730 morning = []
731 afternoon = []
732 evening = []
733 for item in day.filter(self.event_templates, result_type=ResultType.MostRecentSubjects):
734 #if not item.content_object:continue
735 t = time.localtime(int(item.event.timestamp)/1000)
736 if t.tm_hour < 11:
737 morning.append(item)
738 elif t.tm_hour < 17:
739 afternoon.append(item)
740 else:
741 evening.append(item)
742 self.set_phase_items(0, morning)
743 self.set_phase_items(1, afternoon)
744 self.set_phase_items(2, evening)
745
746 def change_style(self, widget, style):
747 rc_style = self.style
748 parent = self.get_parent()
749 if parent:
750 parent = self.get_parent()
751 color = rc_style.bg[gtk.STATE_NORMAL]
752 parent.modify_bg(gtk.STATE_NORMAL, color)
753 for view in self.views: view.modify_base(gtk.STATE_NORMAL, color)
754 color = rc_style.text[4]
755 color = shade_gdk_color(color, 0.95)
756 for label in self.labels:
757 label.modify_fg(0, color)
758
759################
760## TimelineView
761################
762class TimelineViewContainer(_GenericViewWidget):
763
764 def __init__(self):
765 _GenericViewWidget.__init__(self)
766 self.ruler = _TimelineHeader()
767 self.scrolledwindow = gtk.ScrolledWindow()
768 self.scrolledwindow.set_shadow_type(gtk.SHADOW_NONE)
769 self.scrolledwindow.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_ALWAYS)
770 self.view = TimelineView()
771 self.scrolledwindow.add(self.view)
772 self.pack_end(self.scrolledwindow)
773 self.pack_end(self.ruler, False, False)
774 self.view.set_events(gtk.gdk.POINTER_MOTION_MASK | gtk.gdk.POINTER_MOTION_HINT_MASK)
775
776 def change_style(self, widget, style):
777 _GenericViewWidget.change_style(self, widget, style)
778 rc_style = self.style
779 color = rc_style.bg[gtk.STATE_NORMAL]
780 color = shade_gdk_color(color, 102/100.0)
781 self.ruler.modify_bg(gtk.STATE_NORMAL, color)
782
783
784
785
786class _TimelineRenderer(gtk.GenericCellRenderer):
787 """
788 Renders timeline columns, and text for a for properties
789 """
790
791 __gtype_name__ = "TimelineRenderer"
792 __gproperties__ = {
793 "content_obj" :
794 (gobject.TYPE_PYOBJECT,
795 "event to be displayed",
796 "event to be displayed",
797 gobject.PARAM_READWRITE,
798 ),
799 }
800
801 width = 32
802 height = 48
803 barsize = 5
804 properties = {}
805
806 textcolor = {gtk.STATE_NORMAL : ("#ff", "#ff"),
807 gtk.STATE_SELECTED : ("#ff", "#ff")}
808
809 @property
810 def content_obj(self):
811 return self.get_property("content_obj")
812
813 @property
814 def phases(self):
815 return self.content_obj.phases
816
817 @property
818 def event(self):
819 return self.content_obj.event
820
821 @property
822 def colors(self):
823 """ A tuple of two colors, the first being the base the outer being the outline"""
824 return self.content_obj.type_color_representation
825
826 @property
827 def text(self):
828 return self.content_obj.timelineview_text
829
830 @property
831 def pixbuf(self):
832 return self.content_obj.timelineview_pixbuf
833
834 def __init__(self):
835 super(_TimelineRenderer, self).__init__()
836 self.properties = {}
837 self.set_fixed_size(self.width, self.height)
838 self.set_property("mode", gtk.CELL_RENDERER_MODE_ACTIVATABLE)
839
840 def do_set_property(self, pspec, value):
841 self.properties[pspec.name] = value
842
843 def do_get_property(self, pspec):
844 return self.properties[pspec.name]
845
846 def on_get_size(self, widget, area):
847 if area:
848 return (0, 0, area.width, area.height)
849 return (0,0,0,0)
850
851 def on_render(self, window, widget, background_area, cell_area, expose_area, flags):
852 """
853 The primary rendering function. It calls either the classes rendering functions
854 or special one defined in the rendering_functions dict
855 """
856 x = int(cell_area.x)
857 y = int(cell_area.y)
858 w = int(cell_area.width)
859 h = int(cell_area.height)
860 self.render_phases(window, widget, x, y, w, h, flags)
861 return True
862
863 def render_phases(self, window, widget, x, y, w, h, flags):
864 context = window.cairo_create()
865 phases = self.phases
866 for start, end in phases:
867 context.set_source_rgb(*self.colors[0])
868 start = int(start * w)
869 end = max(int(end * w), 8)
870 if start + 8 > w:
871 start = w - 8
872 context.rectangle(x+ start, y, end, self.barsize)
873 context.fill()
874 context.set_source_rgb(*self.colors[1])
875 context.set_line_width(1)
876 context.rectangle(x + start+0.5, y+0.5, end, self.barsize)
877 context.stroke()
878 x = int(phases[0][0]*w)
879 # Pixbuf related junk which is really dirty
880 self.render_text_with_pixbuf(window, widget, x, y, w, h, flags)
881 return True
882
883 def render_text_with_pixbuf(self, window, widget, x, y, w, h, flags):
884 uri = self.content_obj.uri
885 imgw, imgh = self.pixbuf.get_width(), self.pixbuf.get_height()
886 x = max(x + imgw/2 + 4, 0 + imgw + 4)
887 x, y = self.render_text(window, widget, x, y, w, h, flags)
888 x -= imgw + 4
889 y += self.barsize + 3
890 pixbuf_w = self.pixbuf.get_width() if self.pixbuf else 0
891 pixbuf_h = self.pixbuf.get_height() if self.pixbuf else 0
892 if (pixbuf_w, pixbuf_h) == content_objects.SIZE_TIMELINEVIEW:
893 drawframe = True
894 else: drawframe = False
895 render_pixbuf(window, x, y, self.pixbuf, drawframe=drawframe)
896
897 def render_text(self, window, widget, x, y, w, h, flags):
898 w = window.get_geometry()[2]
899 y+= 2
900 x += 5
901 state = gtk.STATE_SELECTED if gtk.CELL_RENDERER_SELECTED & flags else gtk.STATE_NORMAL
902 color1, color2 = self.textcolor[state]
903 text = self._make_timelineview_text(self.text)
904 text = text % (color1.to_string(), color2.to_string())
905 layout = widget.create_pango_layout("")
906 layout.set_markup(text)
907 textw, texth = layout.get_pixel_size()
908 if textw + x > w:
909 layout.set_ellipsize(pango.ELLIPSIZE_MIDDLE)
910 layout.set_width(200*1024)
911 textw, texth = layout.get_pixel_size()
912 if x + textw > w:
913 x = w - textw
914 context = window.cairo_create()
915 pcontext = pangocairo.CairoContext(context)
916 pcontext.set_source_rgb(0, 0, 0)
917 pcontext.move_to(x, y + self.barsize)
918 pcontext.show_layout(layout)
919 return x, y
920
921 @staticmethod
922 def _make_timelineview_text(text):
923 """
924 :returns: a string of text markup used in timeline widget and elsewhere
925 """
926 text = text.split("\n")
927 if len(text) > 1:
928 p1 = text[0]
929 p2 = text[1]
930 else:
931 p1 = text[0]
932 p2 = " "
933 t1 = "<span color='%s'><b>" + p1 + "</b></span>"
934 t2 = "<span color='%s'>" + p2 + "</span> "
935 return (str(t1) + "\n" + str(t2) + "").replace("&", "&amp;")
936
937 def on_start_editing(self, event, widget, path, background_area, cell_area, flags):
938 pass
939
940 def on_activate(self, event, widget, path, background_area, cell_area, flags):
941 pass
942
943
944class TimelineView(gtk.TreeView):
945 @staticmethod
946 def make_area_from_event(timestamp, duration):
947 """
948 Generates a time box based on a objects timestamp and duration over 1.
949 Multiply the results by the width to get usable positions
950
951 :param timestamp: a timestamp int or string from which to calulate the start position
952 :param duration: the length to calulate the width
953 """
954 w = max(duration/3600.0/1000.0/24.0, 0)
955 x = ((int(timestamp)/1000.0 - time.timezone)%86400)/3600/24.0
956 return [x, w]
957
958 child_width = _TimelineRenderer.width
959 child_height = _TimelineRenderer.height
960
961 def __init__(self):
962 super(TimelineView, self).__init__()
963 self.popupmenu = ContextMenu
964 self.add_events(gtk.gdk.LEAVE_NOTIFY_MASK)
965 self.connect("button-press-event", self.on_button_press)
966 # self.connect("motion-notify-event", self.on_motion_notify)
967 # self.connect("leave-notify-event", self.on_leave_notify)
968 self.connect("row-activated" , self.on_activate)
969 self.connect("style-set", self.change_style)
970 pcolumn = gtk.TreeViewColumn("Timeline")
971 self.render = render = _TimelineRenderer()
972 pcolumn.pack_start(render)
973 self.append_column(pcolumn)
974 pcolumn.add_attribute(render, "content_obj", 0)
975 self.set_headers_visible(False)
976 self.set_property("has-tooltip", True)
977 self.set_tooltip_window(StaticPreviewTooltip)
978
979
980 def set_model_from_list(self, items):
981 """
982 Sets creates/sets a model from a list of zeitgeist events
983
984 :param events: a list of :class:`Events <zeitgeist.datamodel.Event>`
985 """
986 if not items:
987 self.set_model(None)
988 return
989 liststore = gtk.ListStore(gobject.TYPE_PYOBJECT)
990 for row in items:
991 item = row[0][0]
992 obj = item.content_object
993 if not obj: continue
994 obj.phases = [self.make_area_from_event(item.event.timestamp, stop) for (item, stop) in row]
995 obj.phases.sort(key=lambda x: x[0])
996 liststore.append((obj,))
997 self.set_model(liststore)
998
999 def set_day(self, day):
1000 items = day.get_time_map()
1001 self.set_model_from_list(items)
1002
1003 def on_button_press(self, widget, event):
1004 if event.button == 3:
1005 path = self.get_dest_row_at_pos(int(event.x), int(event.y))
1006 if path:
1007 model = self.get_model()
1008 obj = model[path[0]][0]
1009 self.popupmenu.do_popup(event.time, [obj])
1010 return True
1011 return False
1012
1013 def on_leave_notify(self, widget, event):
1014 return True
1015
1016 def on_motion_notify(self, widget, event):
1017 return True
1018
1019 def on_activate(self, widget, path, column):
1020 model = self.get_model()
1021 model[path][0].launch()
1022
1023 def change_style(self, widget, old_style):
1024 """
1025 Sets the widgets style and coloring
1026 """
1027 layout = self.create_pango_layout("")
1028 layout.set_markup("<b>qPqPqP|</b>\nqPqPqP|")
1029 tw, th = layout.get_pixel_size()
1030 self.render.height = max(_TimelineRenderer.height, th + 3 + _TimelineRenderer.barsize)
1031 if self.window:
1032 width = self.window.get_geometry()[2] - 4
1033 self.render.width = max(_TimelineRenderer.width, width)
1034 self.render.set_fixed_size(self.render.width, self.render.height)
1035 def change_color(color, inc):
1036 color = shade_gdk_color(color, inc/100.0)
1037 return color
1038 normal = (self.style.text[gtk.STATE_NORMAL], change_color(self.style.text[gtk.STATE_INSENSITIVE], 70))
1039 selected = (self.style.text[gtk.STATE_SELECTED], self.style.text[gtk.STATE_SELECTED])
1040 self.render.textcolor[gtk.STATE_NORMAL] = normal
1041 self.render.textcolor[gtk.STATE_SELECTED] = selected
1042
1043
1044class _TimelineHeader(gtk.DrawingArea):
1045 time_text = {4:"4:00", 8:"8:00", 12:"12:00", 16:"16:00", 20:"20:00"}
1046 odd_line_height = 6
1047 even_line_height = 12
1048
1049 line_color = (0, 0, 0, 1)
1050 def __init__(self):
1051 super(_TimelineHeader, self).__init__()
1052 self.connect("expose-event", self.expose)
1053 self.connect("style-set", self.change_style)
1054 self.set_size_request(100, 12)
1055
1056 def expose(self, widget, event):
1057 window = widget.window
1058 context = widget.window.cairo_create()
1059 layout = self.create_pango_layout(" ")
1060 width = event.area.width
1061 widget.style.set_background(window, gtk.STATE_NORMAL)
1062 context.set_source_rgba(*self.line_color)
1063 context.set_line_width(2)
1064 self.draw_lines(window, context, layout, width)
1065
1066 def draw_text(self, window, context, layout, x, text):
1067 x = int(x)
1068 color = self.style.text[gtk.STATE_NORMAL]
1069 markup = "<span color='%s'>%s</span>" % (color.to_string(), text)
1070 pcontext = pangocairo.CairoContext(context)
1071 layout.set_markup(markup)
1072 xs, ys = layout.get_pixel_size()
1073 pcontext.move_to(x - xs/2, 0)
1074 pcontext.show_layout(layout)
1075
1076 def draw_line(self, window, context, x, even):
1077 x = int(x)+0.5
1078 height = self.even_line_height if even else self.odd_line_height
1079 context.move_to(x, 0)
1080 context.line_to(x, height)
1081 context.stroke()
1082
1083 def draw_lines(self, window, context, layout, width):
1084 xinc = width/24
1085 for hour in xrange(1, 24):
1086 if self.time_text.has_key(hour):
1087 self.draw_text(window, context, layout, xinc*hour, self.time_text[hour])
1088 else:
1089 self.draw_line(window, context, xinc*hour, bool(hour % 2))
1090
1091 def change_style(self, widget, old_style):
1092 layout = self.create_pango_layout("")
1093 layout.set_markup("<b>qPqPqP|</b>")
1094 tw, th = layout.get_pixel_size()
1095 self.set_size_request(tw*5, th+4)
1096 self.line_color = get_gtk_rgba(widget.style, "bg", 0, 0.94)
1097
1098##
1099# Pinned Pane
1100
1101class PinBox(DayView):
1102
1103 def __init__(self):
1104 # Setup event criteria for querying
1105 self.event_timerange = TimeRange.until_now()
1106 # Initialize the widget
1107 super(PinBox, self).__init__(_("Pinned items"))
1108 # Connect to relevant signals
1109 bookmarker.connect("reload", self.set_from_templates)
1110 self.set_from_templates()
1111
1112 @property
1113 def event_templates(self):
1114 if not bookmarker.bookmarks:
1115 # Abort, or we will query with no templates and get lots of
1116 # irrelevant events.
1117 return None
1118
1119 templates = []
1120 for bookmark in bookmarker.bookmarks:
1121 templates.append(Event.new_for_values(subject_uri=bookmark))
1122 return templates
1123
1124 def set_from_templates(self, *args, **kwargs):
1125 if bookmarker.bookmarks:
1126 CLIENT.find_event_ids_for_templates(self.event_templates, self.do_set,
1127 self.event_timerange,
1128 StorageState.Any, 10000, ResultType.MostRecentSubjects)
1129
1130 def do_set(self, event_ids):
1131 objs = []
1132 for id_ in event_ids:
1133 objs += [ContentStruct(id_)]
1134 self.set_items(objs)
1135 # Make the pin icons visible
1136 self.view.show_all()
1137 self.show_all()
1138
1139 def set_items(self, items):
1140 self.clear()
1141 box = CategoryBox(None, items, True)
1142 self.view.pack_start(box)
1143
1144
1145class PinnedPane(Pane):
1146 def __init__(self):
1147 super(PinnedPane, self).__init__()
1148 vbox = gtk.VBox()
1149 self.pinbox = PinBox()
1150 vbox.pack_start(self.pinbox, False, False)
1151 self.add(vbox)
1152 self.set_size_request(200, -1)
1153 self.set_label_align(1,0)
1154
1155
1156## gobject registration
1157gobject.type_register(_TimelineRenderer)
1158gobject.type_register(_ThumbViewRenderer)
1159
01160
=== added file 'src/common.py'
--- src/common.py 1970-01-01 00:00:00 +0000
+++ src/common.py 2010-05-04 05:28:16 +0000
@@ -0,0 +1,582 @@
1# -.- coding: utf-8 -.-
2#
3# Filename
4#
5# Copyright © 2010 Randal Barlow <email.tehk@gmail.com>
6# Copyright © 2010 Siegfried Gevatter <siegfried@gevatter.com>
7#
8# This program is free software: you can redistribute it and/or modify
9# it under the terms of the GNU General Public License as published by
10# the Free Software Foundation, either version 3 of the License, or
11# (at your option) any later version.
12#
13# This program is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16# GNU General Public License for more details.
17#
18# You should have received a copy of the GNU General Public License
19# along with this program. If not, see <http://www.gnu.org/licenses/>.
20
21"""
22Common Functions and classes which are used to create the alternative views,
23and handle colors, pixbufs, and text
24"""
25
26import cairo
27import gobject
28import gtk
29import os
30import pango
31import pangocairo
32import time
33import math
34import operator
35import subprocess
36
37from gio_file import GioFile, SIZE_LARGE, SIZE_NORMAL, SIZE_THUMBVIEW, SIZE_TIMELINEVIEW, ICONS
38from config import get_data_path, get_icon_path
39
40from zeitgeist.datamodel import Interpretation, Event
41
42
43# Caches desktop files
44DESKTOP_FILES = {}
45DESKTOP_FILE_PATHS = []
46try:
47 desktop_file_paths = os.environ["XDG_DATA_DIRS"].split(":")
48 for path in desktop_file_paths:
49 if path.endswith("/"):
50 DESKTOP_FILE_PATHS.append(path + "applications/")
51 else:
52 DESKTOP_FILE_PATHS.append(path + "/applications/")
53except KeyError:pass
54
55# Placeholder pixbufs for common sizes
56PLACEHOLDER_PIXBUFFS = {
57 24 : gtk.gdk.pixbuf_new_from_file_at_size(get_icon_path("hicolor/scalable/apps/gnome-activity-journal.svg"), 24, 24),
58 16 : gtk.gdk.pixbuf_new_from_file_at_size(get_icon_path("hicolor/scalable/apps/gnome-activity-journal.svg"), 16, 16)
59 }
60
61# Color magic
62TANGOCOLORS = [
63 (252/255.0, 234/255.0, 79/255.0),#0
64 (237/255.0, 212/255.0, 0/255.0),
65 (196/255.0, 160/255.0, 0/255.0),
66
67 (252/255.0, 175/255.0, 62/255.0),#3
68 (245/255.0, 121/255.0, 0/255.0),
69 (206/255.0, 92/255.0, 0/255.0),
70
71 (233/255.0, 185/255.0, 110/255.0),#6
72 (193/255.0, 125/255.0, 17/255.0),
73 (143/255.0, 89/255.0, 02/255.0),
74
75 (138/255.0, 226/255.0, 52/255.0),#9
76 (115/255.0, 210/255.0, 22/255.0),
77 ( 78/255.0, 154/255.0, 06/255.0),
78
79 (114/255.0, 159/255.0, 207/255.0),#12
80 ( 52/255.0, 101/255.0, 164/255.0),
81 ( 32/255.0, 74/255.0, 135/255.0),
82
83 (173/255.0, 127/255.0, 168/255.0),#15
84 (117/255.0, 80/255.0, 123/255.0),
85 ( 92/255.0, 53/255.0, 102/255.0),
86
87 (239/255.0, 41/255.0, 41/255.0),#18
88 (204/255.0, 0/255.0, 0/255.0),
89 (164/255.0, 0/255.0, 0/255.0),
90
91 (136/255.0, 138/255.0, 133/255.0),#21
92 ( 85/255.0, 87/255.0, 83/255.0),
93 ( 46/255.0, 52/255.0, 54/255.0),
94]
95
96FILETYPES = {
97 Interpretation.VIDEO.uri : 0,
98 Interpretation.MUSIC.uri : 3,
99 Interpretation.DOCUMENT.uri : 12,
100 Interpretation.IMAGE.uri : 15,
101 Interpretation.SOURCECODE.uri : 12,
102 Interpretation.UNKNOWN.uri : 21,
103 Interpretation.IM_MESSAGE.uri : 21,
104 Interpretation.EMAIL.uri : 21
105}
106
107FILETYPESNAMES = {
108 Interpretation.VIDEO.uri : _("Video"),
109 Interpretation.MUSIC.uri : _("Music"),
110 Interpretation.DOCUMENT.uri : _("Document"),
111 Interpretation.IMAGE.uri : _("Image"),
112 Interpretation.SOURCECODE.uri : _("Source Code"),
113 Interpretation.UNKNOWN.uri : _("Unknown"),
114 Interpretation.IM_MESSAGE.uri : _("IM Message"),
115 Interpretation.EMAIL.uri :_("Email"),
116
117}
118
119MEDIAINTERPRETATIONS = [
120 Interpretation.VIDEO.uri,
121 Interpretation.IMAGE.uri,
122]
123
124TIMELABELS = [_("Morning"), _("Afternoon"), _("Evening")]
125ICON_THEME = gtk.icon_theme_get_default()
126
127def get_file_color(ftype, fmime):
128 """Uses hashing to choose a shade from a hue in the color tuple above
129
130 :param ftype: a :class:`Event <zeitgeist.datamodel.Interpretation>`
131 :param fmime: a mime type string
132 """
133 if ftype in FILETYPES.keys():
134 i = FILETYPES[ftype]
135 l = int(math.fabs(hash(fmime))) % 3
136 return TANGOCOLORS[min(i+l, len(TANGOCOLORS)-1)]
137 return (136/255.0, 138/255.0, 133/255.0)
138
139##
140## Zeitgeist event helper functions
141
142def get_event_typename(event):
143 """
144 :param event: a :class:`Event <zeitgeist.datamodel.Event>`
145
146 :returns: a plain text version of a interpretation
147 """
148 try:
149 return Interpretation[event.subjects[0].interpretation].display_name
150 except KeyError:
151 pass
152 return FILETYPESNAMES[event.subjects[0].interpretation]
153
154##
155# Cairo drawing functions
156
157def draw_frame(context, x, y, w, h):
158 """
159 Draws a 2 pixel frame around a area defined by x, y, w, h using a cairo context
160
161 :param context: a cairo context
162 :param x: x position of the frame
163 :param y: y position of the frame
164 :param w: width of the frame
165 :param h: height of the frame
166 """
167 x, y = int(x)+0.5, int(y)+0.5
168 w, h = int(w), int(h)
169 context.set_line_width(1)
170 context.rectangle(x-1, y-1, w+2, h+2)
171 context.set_source_rgba(0.5, 0.5, 0.5)#0.3, 0.3, 0.3)
172 context.stroke()
173 context.set_source_rgba(0.7, 0.7, 0.7)
174 context.rectangle(x, y, w, h)
175 context.stroke()
176 context.set_source_rgba(0.4, 0.4, 0.4)
177 context.rectangle(x+1, y+1, w-2, h-2)
178 context.stroke()
179
180def draw_rounded_rectangle(context, x, y, w, h, r=5):
181 """Draws a rounded rectangle
182
183 :param context: a cairo context
184 :param x: x position of the rectangle
185 :param y: y position of the rectangle
186 :param w: width of the rectangle
187 :param h: height of the rectangle
188 :param r: radius of the rectangle
189 """
190 context.new_sub_path()
191 context.arc(r+x, r+y, r, math.pi, 3 * math.pi /2)
192 context.arc(w-r+x, r+y, r, 3 * math.pi / 2, 0)
193 context.arc(w-r+x, h-r+y, r, 0, math.pi/2)
194 context.arc(r+x, h-r+y, r, math.pi/2, math.pi)
195 context.close_path()
196 return context
197
198def draw_speech_bubble(context, layout, x, y, w, h):
199 """
200 Draw a speech bubble at a position
201
202 Arguments:
203 :param context: a cairo context
204 :param layout: a pango layout
205 :param x: x position of the bubble
206 :param y: y position of the bubble
207 :param w: width of the bubble
208 :param h: height of the bubble
209 """
210 layout.set_width((w-10)*1024)
211 layout.set_ellipsize(pango.ELLIPSIZE_MIDDLE)
212 textw, texth = layout.get_pixel_size()
213 context.new_path()
214 context.move_to(x + 0.45*w, y+h*0.1 + 2)
215 context.line_to(x + 0.5*w, y)
216 context.line_to(x + 0.55*w, y+h*0.1 + 2)
217 h = max(texth + 5, h)
218 draw_rounded_rectangle(context, x, y+h*0.1, w, h, r = 5)
219 context.close_path()
220 context.set_line_width(2)
221 context.set_source_rgb(168/255.0, 165/255.0, 134/255.0)
222 context.stroke_preserve()
223 context.set_source_rgb(253/255.0, 248/255.0, 202/255.0)
224 context.fill()
225 pcontext = pangocairo.CairoContext(context)
226 pcontext.set_source_rgb(0, 0, 0)
227 pcontext.move_to(x+5, y+5)
228 pcontext.show_layout(layout)
229
230def draw_text(context, layout, markup, x, y, maxw = 0, color = (0.3, 0.3, 0.3)):
231 """
232 Draw text using a cairo context and a pango layout
233
234 Arguments:
235 :param context: a cairo context
236 :param layout: a pango layout
237 :param x: x position of the bubble
238 :param y: y position of the bubble
239 :param maxw: the max text width in pixels
240 :param color: a rgb tuple
241 """
242 pcontext = pangocairo.CairoContext(context)
243 layout.set_markup(markup)
244 layout.set_ellipsize(pango.ELLIPSIZE_MIDDLE)
245 pcontext.set_source_rgba(*color)
246 if maxw:
247 layout.set_width(maxw*1024)
248 pcontext.move_to(x, y)
249 pcontext.show_layout(layout)
250
251def render_pixbuf(window, x, y, pixbuf, drawframe = True):
252 """
253 Renders a pixbuf to be displayed on the cell
254
255 Arguments:
256 :param window: a gdk window
257 :param x: x position
258 :param y: y position
259 :param drawframe: if true we draw a frame around the pixbuf
260 """
261 imgw, imgh = pixbuf.get_width(), pixbuf.get_height()
262 context = window.cairo_create()
263 context.rectangle(x, y, imgw, imgh)
264 if drawframe:
265 context.set_source_rgb(1, 1, 1)
266 context.fill_preserve()
267 context.set_source_pixbuf(pixbuf, x, y)
268 context.fill()
269 if drawframe: # Draw a pretty frame
270 draw_frame(context, x, y, imgw, imgh)
271
272def render_emblems(window, x, y, w, h, emblems):
273 """
274 Renders emblems on the four corners of the rectangle
275
276 Arguments:
277 :param window: a gdk window
278 :param x: x position
279 :param y: y position
280 :param w: the width of the rectangle
281 :param y: the height of the rectangle
282 :param emblems: a list of pixbufs
283 """
284 # w = max(self.width, w)
285 corners = [[x, y],
286 [x+w, y],
287 [x, y+h],
288 [x+w-4, y+h-4]]
289 context = window.cairo_create()
290 for i in xrange(len(emblems)):
291 i = i % len(emblems)
292 pixbuf = emblems[i]
293 if pixbuf:
294 pbw, pbh = pixbuf.get_width()/2, pixbuf.get_height()/2
295 context.set_source_pixbuf(pixbuf, corners[i][0]-pbw, corners[i][1]-pbh)
296 context.rectangle(corners[i][0]-pbw, corners[i][1]-pbh, pbw*2, pbh*2)
297 context.fill()
298
299##
300## Color functions
301
302def shade_gdk_color(color, shade):
303 """
304 Shades a color by a fraction
305
306 Arguments:
307 :param color: a gdk color
308 :param shade: fraction by which to shade the color
309
310 :returns: a :class:`Color <gtk.gdk.Color>`
311 """
312 f = lambda num: min((num * shade, 65535.0))
313 if gtk.pygtk_version >= (2, 16, 0):
314 color.red = f(color.red)
315 color.green = f(color.green)
316 color.blue = f(color.blue)
317 else:
318 red = int(f(color.red))
319 green = int(f(color.green))
320 blue = int(f(color.blue))
321 color = gtk.gdk.Color(red=red, green=green, blue=blue)
322 return color
323
324def combine_gdk_color(color, fcolor):
325 """
326 Combines a color with another color
327
328 Arguments:
329 :param color: a gdk color
330 :param fcolor: a gdk color to combine with color
331
332 :returns: a :class:`Color <gtk.gdk.Color>`
333 """
334 if gtk.pygtk_version >= (2, 16, 0):
335 color.red = (2*color.red + fcolor.red)/3
336 color.green = (2*color.green + fcolor.green)/3
337 color.blue = (2*color.blue + fcolor.blue)/3
338 else:
339 red = int(((2*color.red + fcolor.red)/3))
340 green = int(((2*color.green + fcolor.green)/3))
341 blue = int(((2*color.blue + fcolor.blue)/3))
342 color = gtk.gdk.Color(red=red, green=green, blue=blue)
343 return color
344
345def get_gtk_rgba(style, palette, i, shade = 1, alpha = 1):
346 """Takes a gtk style and returns a RGB tuple
347
348 Arguments:
349 :param style: a gtk_style object
350 :param palette: a string representing the palette you want to pull a color from
351 Example: "bg", "fg"
352 :param shade: how much you want to shade the color
353
354 :returns: a rgba tuple
355 """
356 f = lambda num: (num/65535.0) * shade
357 color = getattr(style, palette)[i]
358 if isinstance(color, gtk.gdk.Color):
359 red = f(color.red)
360 green = f(color.green)
361 blue = f(color.blue)
362 return (min(red, 1), min(green, 1), min(blue, 1), alpha)
363 else: raise TypeError("Not a valid gtk.gdk.Color")
364
365
366##
367## Pixbuff work
368##
369
370def new_grayscale_pixbuf(pixbuf):
371 """
372 Makes a pixbuf grayscale
373
374 :param pixbuf: a :class:`Pixbuf <gtk.gdk.Pixbuf>`
375
376 :returns: a :class:`Pixbuf <gtk.gdk.Pixbuf>`
377
378 """
379 pixbuf2 = pixbuf.copy()
380 pixbuf.saturate_and_pixelate(pixbuf2, 0.0, False)
381 return pixbuf2
382
383def crop_pixbuf(pixbuf, x, y, width, height):
384 """
385 Crop a pixbuf
386
387 Arguments:
388 :param pixbuf: a :class:`Pixbuf <gtk.gdk.Pixbuf>`
389 :param x: the x position to crop from in the source
390 :param y: the y position to crop from in the source
391 :param width: crop width
392 :param height: crop height
393
394 :returns: a :class:`Pixbuf <gtk.gdk.Pixbuf>`
395 """
396 dest_pixbuf = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, True, 8, width, height)
397 pixbuf.copy_area(x, y, width, height, dest_pixbuf, 0, 0)
398 return dest_pixbuf
399
400def scale_to_fill(pixbuf, neww, newh):
401 """
402 Scales/crops a new pixbuf to a width and height at best fit and returns it
403
404 Arguments:
405 :param pixbuf: a :class:`Pixbuf <gtk.gdk.Pixbuf>`
406 :param neww: new width of the new pixbuf
407 :param newh: a new height of the new pixbuf
408
409 :returns: a :class:`Pixbuf <gtk.gdk.Pixbuf>`
410 """
411 imagew, imageh = pixbuf.get_width(), pixbuf.get_height()
412 if (imagew, imageh) != (neww, newh):
413 imageratio = float(imagew) / float(imageh)
414 newratio = float(neww) / float(newh)
415 if imageratio > newratio:
416 transformw = int(round(newh * imageratio))
417 pixbuf = pixbuf.scale_simple(transformw, newh, gtk.gdk.INTERP_BILINEAR)
418 pixbuf = crop_pixbuf(pixbuf, 0, 0, neww, newh)
419 elif imageratio < newratio:
420 transformh = int(round(neww / imageratio))
421 pixbuf = pixbuf.scale_simple(neww, transformh, gtk.gdk.INTERP_BILINEAR)
422 pixbuf = crop_pixbuf(pixbuf, 0, 0, neww, newh)
423 else:
424 pixbuf = pixbuf.scale_simple(neww, newh, gtk.gdk.INTERP_BILINEAR)
425 return pixbuf
426
427
428class PixbufCache(dict):
429 """
430 A pixbuf cache dict which stores, loads, and saves pixbufs to a cache and to
431 the users filesystem. The naming scheme for thumb files are use hash
432
433 There are huge flaws with this object. It does not have a ceiling, and it
434 does not remove thumbnails from the file system. Essentially meaning the
435 cache directory can grow forever.
436 """
437 def __init__(self, *args, **kwargs):
438 super(PixbufCache, self).__init__()
439
440 def check_cache(self, uri):
441 return self[uri]
442
443 def get_buff(self, key):
444 thumbpath = os.path.expanduser("~/.cache/GAJ/1_" + str(hash(key)))
445 if os.path.exists(thumbpath):
446 self[key] = (gtk.gdk.pixbuf_new_from_file(thumbpath), True)
447 return self[key]
448 return None
449
450 def __getitem__(self, key):
451 if self.has_key(key):
452 return super(PixbufCache, self).__getitem__(key)
453 return self.get_buff(key)
454
455 def __setitem__(self, key, (pb, isthumb)):
456 dir_ = os.path.expanduser("~/.cache/GAJ/")
457 if not os.path.exists(os.path.expanduser("~/.cache/GAJ/")):
458 os.makedirs(dir_)
459 path = dir_ + str(hash(isthumb)) + "_" + str(hash(key))
460 if not os.path.exists(path):
461 open(path, 'w').close()
462 pb.save(path, "png")
463 return super(PixbufCache, self).__setitem__(key, (pb, isthumb))
464
465 def get_pixbuf_from_uri(self, uri, size=SIZE_LARGE, iconscale=1, w=0, h=0):
466 """
467 Returns a pixbuf and True if a thumbnail was found, else False. Uses the
468 Pixbuf Cache for thumbnail compatible files. If the pixbuf is a thumb
469 it is cached.
470
471 Arguments:
472 :param uri: a uri on the disk
473 :param size: a size tuple from thumbfactory
474 :param iconscale: a factor to reduce icons by (not thumbs)
475 :param w: resulting width
476 :param h: resulting height
477
478 Warning! This function is in need of a serious clean up.
479
480 :returns: a tuple containing a :class:`Pixbuf <gtk.gdk.Pixbuf>` and bool
481 which is True if a thumbnail was found
482 """
483 try:
484 cached = self.check_cache(uri)
485 except gobject.GError:
486 cached = None
487 if cached:
488 return cached
489 gfile = GioFile.create(uri)
490 thumb = True
491 if gfile:
492 if gfile.has_preview():
493 pb = gfile.get_thumbnail(size=size)
494 else:
495 iconsize = int(size[0]*iconscale)
496 pb = gfile.get_icon(size=iconsize)
497 thumb = False
498 else: pb = None
499 if not pb:
500 pb = ICON_THEME.lookup_icon(gtk.STOCK_MISSING_IMAGE, int(size[0]*iconscale), gtk.ICON_LOOKUP_FORCE_SVG).load_icon()
501 thumb = False
502 if thumb:
503 pb = scale_to_fill(pb, w, h)
504 self[uri] = (pb, thumb)
505 return pb, thumb
506
507PIXBUFCACHE = PixbufCache()
508
509def get_icon_for_name(name, size):
510 """
511 return a icon for a name
512 """
513 size = int(size)
514 ICONS[(size, size)]
515 if ICONS[(size, size)].has_key(name):
516 return ICONS[(size, size)][name]
517 info = ICON_THEME.lookup_icon(name, size, gtk.ICON_LOOKUP_USE_BUILTIN)
518 if not info:
519 return None
520 location = info.get_filename()
521 return get_icon_for_uri(location, size)
522
523def get_icon_for_uri(uri, size):
524 pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(uri, size, size)
525 ICONS[(size, size)][uri] = pixbuf
526 return pixbuf
527
528def get_icon_from_object_at_uri(uri, size):
529 """
530 Returns a icon from a event at size
531
532 :param uri: a uri string
533 :param size: a int representing the size in pixels of the icon
534
535 :returns: a :class:`Pixbuf <gtk.gdk.Pixbuf>`
536 """
537 gfile = GioFile.create(uri)
538 if gfile:
539 pb = gfile.get_icon(size=size)
540 if pb:
541 return pb
542 return False
543
544
545
546##
547## Other useful methods
548##
549
550def is_command_available(command):
551 """
552 Checks whether the given command is available, by looking for it in
553 the PATH.
554
555 This is useful for ensuring that optional dependencies on external
556 applications are fulfilled.
557 """
558 assert len(" a".split()) == 1, "No arguments are accepted in command"
559 for directory in os.environ["PATH"].split(os.pathsep):
560 if os.path.exists(os.path.join(directory, command)):
561 return True
562 return False
563
564def launch_command(command, arguments=None):
565 """
566 Launches a program as an independent process.
567 """
568 if not arguments:
569 arguments = []
570 null = os.open(os.devnull, os.O_RDWR)
571 subprocess.Popen([command] + arguments, stdout=null, stderr=null,
572 close_fds=True)
573
574def launch_string_command(command):
575 """
576 Launches a program as an independent from a string
577 """
578 command = command.split(" ")
579 null = os.open(os.devnull, os.O_RDWR)
580 subprocess.Popen(command, stdout=null, stderr=null,
581 close_fds=True)
582
0583
=== removed file 'src/common.py'
--- src/common.py 2010-04-19 04:51:23 +0000
+++ src/common.py 1970-01-01 00:00:00 +0000
@@ -1,582 +0,0 @@
1# -.- coding: utf-8 -.-
2#
3# Filename
4#
5# Copyright © 2010 Randal Barlow <email.tehk@gmail.com>
6# Copyright © 2010 Siegfried Gevatter <siegfried@gevatter.com>
7#
8# This program is free software: you can redistribute it and/or modify
9# it under the terms of the GNU General Public License as published by
10# the Free Software Foundation, either version 3 of the License, or
11# (at your option) any later version.
12#
13# This program is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16# GNU General Public License for more details.
17#
18# You should have received a copy of the GNU General Public License
19# along with this program. If not, see <http://www.gnu.org/licenses/>.
20
21"""
22Common Functions and classes which are used to create the alternative views,
23and handle colors, pixbufs, and text
24"""
25
26import cairo
27import gobject
28import gtk
29import os
30import pango
31import pangocairo
32import time
33import math
34import operator
35import subprocess
36
37from gio_file import GioFile, SIZE_LARGE, SIZE_NORMAL, SIZE_THUMBVIEW, SIZE_TIMELINEVIEW, ICONS
38from config import get_data_path, get_icon_path
39
40from zeitgeist.datamodel import Interpretation, Event
41
42
43# Caches desktop files
44DESKTOP_FILES = {}
45DESKTOP_FILE_PATHS = []
46try:
47 desktop_file_paths = os.environ["XDG_DATA_DIRS"].split(":")
48 for path in desktop_file_paths:
49 if path.endswith("/"):
50 DESKTOP_FILE_PATHS.append(path + "applications/")
51 else:
52 DESKTOP_FILE_PATHS.append(path + "/applications/")
53except KeyError:pass
54
55# Placeholder pixbufs for common sizes
56PLACEHOLDER_PIXBUFFS = {
57 24 : gtk.gdk.pixbuf_new_from_file_at_size(get_icon_path("hicolor/scalable/apps/gnome-activity-journal.svg"), 24, 24),
58 16 : gtk.gdk.pixbuf_new_from_file_at_size(get_icon_path("hicolor/scalable/apps/gnome-activity-journal.svg"), 16, 16)
59 }
60
61# Color magic
62TANGOCOLORS = [
63 (252/255.0, 234/255.0, 79/255.0),#0
64 (237/255.0, 212/255.0, 0/255.0),
65 (196/255.0, 160/255.0, 0/255.0),
66
67 (252/255.0, 175/255.0, 62/255.0),#3
68 (245/255.0, 121/255.0, 0/255.0),
69 (206/255.0, 92/255.0, 0/255.0),
70
71 (233/255.0, 185/255.0, 110/255.0),#6
72 (193/255.0, 125/255.0, 17/255.0),
73 (143/255.0, 89/255.0, 02/255.0),
74
75 (138/255.0, 226/255.0, 52/255.0),#9
76 (115/255.0, 210/255.0, 22/255.0),
77 ( 78/255.0, 154/255.0, 06/255.0),
78
79 (114/255.0, 159/255.0, 207/255.0),#12
80 ( 52/255.0, 101/255.0, 164/255.0),
81 ( 32/255.0, 74/255.0, 135/255.0),
82
83 (173/255.0, 127/255.0, 168/255.0),#15
84 (117/255.0, 80/255.0, 123/255.0),
85 ( 92/255.0, 53/255.0, 102/255.0),
86
87 (239/255.0, 41/255.0, 41/255.0),#18
88 (204/255.0, 0/255.0, 0/255.0),
89 (164/255.0, 0/255.0, 0/255.0),
90
91 (136/255.0, 138/255.0, 133/255.0),#21
92 ( 85/255.0, 87/255.0, 83/255.0),
93 ( 46/255.0, 52/255.0, 54/255.0),
94]
95
96FILETYPES = {
97 Interpretation.VIDEO.uri : 0,
98 Interpretation.MUSIC.uri : 3,
99 Interpretation.DOCUMENT.uri : 12,
100 Interpretation.IMAGE.uri : 15,
101 Interpretation.SOURCECODE.uri : 12,
102 Interpretation.UNKNOWN.uri : 21,
103 Interpretation.IM_MESSAGE.uri : 21,
104 Interpretation.EMAIL.uri : 21
105}
106
107FILETYPESNAMES = {
108 Interpretation.VIDEO.uri : _("Video"),
109 Interpretation.MUSIC.uri : _("Music"),
110 Interpretation.DOCUMENT.uri : _("Document"),
111 Interpretation.IMAGE.uri : _("Image"),
112 Interpretation.SOURCECODE.uri : _("Source Code"),
113 Interpretation.UNKNOWN.uri : _("Unknown"),
114 Interpretation.IM_MESSAGE.uri : _("IM Message"),
115 Interpretation.EMAIL.uri :_("Email"),
116
117}
118
119MEDIAINTERPRETATIONS = [
120 Interpretation.VIDEO.uri,
121 Interpretation.IMAGE.uri,
122]
123
124TIMELABELS = [_("Morning"), _("Afternoon"), _("Evening")]
125ICON_THEME = gtk.icon_theme_get_default()
126
127def get_file_color(ftype, fmime):
128 """Uses hashing to choose a shade from a hue in the color tuple above
129
130 :param ftype: a :class:`Event <zeitgeist.datamodel.Interpretation>`
131 :param fmime: a mime type string
132 """
133 if ftype in FILETYPES.keys():
134 i = FILETYPES[ftype]
135 l = int(math.fabs(hash(fmime))) % 3
136 return TANGOCOLORS[min(i+l, len(TANGOCOLORS)-1)]
137 return (136/255.0, 138/255.0, 133/255.0)
138
139##
140## Zeitgeist event helper functions
141
142def get_event_typename(event):
143 """
144 :param event: a :class:`Event <zeitgeist.datamodel.Event>`
145
146 :returns: a plain text version of a interpretation
147 """
148 try:
149 return Interpretation[event.subjects[0].interpretation].display_name
150 except KeyError:
151 pass
152 return FILETYPESNAMES[event.subjects[0].interpretation]
153
154##
155# Cairo drawing functions
156
157def draw_frame(context, x, y, w, h):
158 """
159 Draws a 2 pixel frame around a area defined by x, y, w, h using a cairo context
160
161 :param context: a cairo context
162 :param x: x position of the frame
163 :param y: y position of the frame
164 :param w: width of the frame
165 :param h: height of the frame
166 """
167 x, y = int(x)+0.5, int(y)+0.5
168 w, h = int(w), int(h)
169 context.set_line_width(1)
170 context.rectangle(x-1, y-1, w+2, h+2)
171 context.set_source_rgba(0.5, 0.5, 0.5)#0.3, 0.3, 0.3)
172 context.stroke()
173 context.set_source_rgba(0.7, 0.7, 0.7)
174 context.rectangle(x, y, w, h)
175 context.stroke()
176 context.set_source_rgba(0.4, 0.4, 0.4)
177 context.rectangle(x+1, y+1, w-2, h-2)
178 context.stroke()
179
180def draw_rounded_rectangle(context, x, y, w, h, r=5):
181 """Draws a rounded rectangle
182
183 :param context: a cairo context
184 :param x: x position of the rectangle
185 :param y: y position of the rectangle
186 :param w: width of the rectangle
187 :param h: height of the rectangle
188 :param r: radius of the rectangle
189 """
190 context.new_sub_path()
191 context.arc(r+x, r+y, r, math.pi, 3 * math.pi /2)
192 context.arc(w-r+x, r+y, r, 3 * math.pi / 2, 0)
193 context.arc(w-r+x, h-r+y, r, 0, math.pi/2)
194 context.arc(r+x, h-r+y, r, math.pi/2, math.pi)
195 context.close_path()
196 return context
197
198def draw_speech_bubble(context, layout, x, y, w, h):
199 """
200 Draw a speech bubble at a position
201
202 Arguments:
203 :param context: a cairo context
204 :param layout: a pango layout
205 :param x: x position of the bubble
206 :param y: y position of the bubble
207 :param w: width of the bubble
208 :param h: height of the bubble
209 """
210 layout.set_width((w-10)*1024)
211 layout.set_ellipsize(pango.ELLIPSIZE_MIDDLE)
212 textw, texth = layout.get_pixel_size()
213 context.new_path()
214 context.move_to(x + 0.45*w, y+h*0.1 + 2)
215 context.line_to(x + 0.5*w, y)
216 context.line_to(x + 0.55*w, y+h*0.1 + 2)
217 h = max(texth + 5, h)
218 draw_rounded_rectangle(context, x, y+h*0.1, w, h, r = 5)
219 context.close_path()
220 context.set_line_width(2)
221 context.set_source_rgb(168/255.0, 165/255.0, 134/255.0)
222 context.stroke_preserve()
223 context.set_source_rgb(253/255.0, 248/255.0, 202/255.0)
224 context.fill()
225 pcontext = pangocairo.CairoContext(context)
226 pcontext.set_source_rgb(0, 0, 0)
227 pcontext.move_to(x+5, y+5)
228 pcontext.show_layout(layout)
229
230def draw_text(context, layout, markup, x, y, maxw = 0, color = (0.3, 0.3, 0.3)):
231 """
232 Draw text using a cairo context and a pango layout
233
234 Arguments:
235 :param context: a cairo context
236 :param layout: a pango layout
237 :param x: x position of the bubble
238 :param y: y position of the bubble
239 :param maxw: the max text width in pixels
240 :param color: a rgb tuple
241 """
242 pcontext = pangocairo.CairoContext(context)
243 layout.set_markup(markup)
244 layout.set_ellipsize(pango.ELLIPSIZE_MIDDLE)
245 pcontext.set_source_rgba(*color)
246 if maxw:
247 layout.set_width(maxw*1024)
248 pcontext.move_to(x, y)
249 pcontext.show_layout(layout)
250
251def render_pixbuf(window, x, y, pixbuf, drawframe = True):
252 """
253 Renders a pixbuf to be displayed on the cell
254
255 Arguments:
256 :param window: a gdk window
257 :param x: x position
258 :param y: y position
259 :param drawframe: if true we draw a frame around the pixbuf
260 """
261 imgw, imgh = pixbuf.get_width(), pixbuf.get_height()
262 context = window.cairo_create()
263 context.rectangle(x, y, imgw, imgh)
264 if drawframe:
265 context.set_source_rgb(1, 1, 1)
266 context.fill_preserve()
267 context.set_source_pixbuf(pixbuf, x, y)
268 context.fill()
269 if drawframe: # Draw a pretty frame
270 draw_frame(context, x, y, imgw, imgh)
271
272def render_emblems(window, x, y, w, h, emblems):
273 """
274 Renders emblems on the four corners of the rectangle
275
276 Arguments:
277 :param window: a gdk window
278 :param x: x position
279 :param y: y position
280 :param w: the width of the rectangle
281 :param y: the height of the rectangle
282 :param emblems: a list of pixbufs
283 """
284 # w = max(self.width, w)
285 corners = [[x, y],
286 [x+w, y],
287 [x, y+h],
288 [x+w-4, y+h-4]]
289 context = window.cairo_create()
290 for i in xrange(len(emblems)):
291 i = i % len(emblems)
292 pixbuf = emblems[i]
293 if pixbuf:
294 pbw, pbh = pixbuf.get_width()/2, pixbuf.get_height()/2
295 context.set_source_pixbuf(pixbuf, corners[i][0]-pbw, corners[i][1]-pbh)
296 context.rectangle(corners[i][0]-pbw, corners[i][1]-pbh, pbw*2, pbh*2)
297 context.fill()
298
299##
300## Color functions
301
302def shade_gdk_color(color, shade):
303 """
304 Shades a color by a fraction
305
306 Arguments:
307 :param color: a gdk color
308 :param shade: fraction by which to shade the color
309
310 :returns: a :class:`Color <gtk.gdk.Color>`
311 """
312 f = lambda num: min((num * shade, 65535.0))
313 if gtk.pygtk_version >= (2, 16, 0):
314 color.red = f(color.red)
315 color.green = f(color.green)
316 color.blue = f(color.blue)
317 else:
318 red = int(f(color.red))
319 green = int(f(color.green))
320 blue = int(f(color.blue))
321 color = gtk.gdk.Color(red=red, green=green, blue=blue)
322 return color
323
324def combine_gdk_color(color, fcolor):
325 """
326 Combines a color with another color
327
328 Arguments:
329 :param color: a gdk color
330 :param fcolor: a gdk color to combine with color
331
332 :returns: a :class:`Color <gtk.gdk.Color>`
333 """
334 if gtk.pygtk_version >= (2, 16, 0):
335 color.red = (2*color.red + fcolor.red)/3
336 color.green = (2*color.green + fcolor.green)/3
337 color.blue = (2*color.blue + fcolor.blue)/3
338 else:
339 red = int(((2*color.red + fcolor.red)/3))
340 green = int(((2*color.green + fcolor.green)/3))
341 blue = int(((2*color.blue + fcolor.blue)/3))
342 color = gtk.gdk.Color(red=red, green=green, blue=blue)
343 return color
344
345def get_gtk_rgba(style, palette, i, shade = 1, alpha = 1):
346 """Takes a gtk style and returns a RGB tuple
347
348 Arguments:
349 :param style: a gtk_style object
350 :param palette: a string representing the palette you want to pull a color from
351 Example: "bg", "fg"
352 :param shade: how much you want to shade the color
353
354 :returns: a rgba tuple
355 """
356 f = lambda num: (num/65535.0) * shade
357 color = getattr(style, palette)[i]
358 if isinstance(color, gtk.gdk.Color):
359 red = f(color.red)
360 green = f(color.green)
361 blue = f(color.blue)
362 return (min(red, 1), min(green, 1), min(blue, 1), alpha)
363 else: raise TypeError("Not a valid gtk.gdk.Color")
364
365
366##
367## Pixbuff work
368##
369
370def new_grayscale_pixbuf(pixbuf):
371 """
372 Makes a pixbuf grayscale
373
374 :param pixbuf: a :class:`Pixbuf <gtk.gdk.Pixbuf>`
375
376 :returns: a :class:`Pixbuf <gtk.gdk.Pixbuf>`
377
378 """
379 pixbuf2 = pixbuf.copy()
380 pixbuf.saturate_and_pixelate(pixbuf2, 0.0, False)
381 return pixbuf2
382
383def crop_pixbuf(pixbuf, x, y, width, height):
384 """
385 Crop a pixbuf
386
387 Arguments:
388 :param pixbuf: a :class:`Pixbuf <gtk.gdk.Pixbuf>`
389 :param x: the x position to crop from in the source
390 :param y: the y position to crop from in the source
391 :param width: crop width
392 :param height: crop height
393
394 :returns: a :class:`Pixbuf <gtk.gdk.Pixbuf>`
395 """
396 dest_pixbuf = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, True, 8, width, height)
397 pixbuf.copy_area(x, y, width, height, dest_pixbuf, 0, 0)
398 return dest_pixbuf
399
400def scale_to_fill(pixbuf, neww, newh):
401 """
402 Scales/crops a new pixbuf to a width and height at best fit and returns it
403
404 Arguments:
405 :param pixbuf: a :class:`Pixbuf <gtk.gdk.Pixbuf>`
406 :param neww: new width of the new pixbuf
407 :param newh: a new height of the new pixbuf
408
409 :returns: a :class:`Pixbuf <gtk.gdk.Pixbuf>`
410 """
411 imagew, imageh = pixbuf.get_width(), pixbuf.get_height()
412 if (imagew, imageh) != (neww, newh):
413 imageratio = float(imagew) / float(imageh)
414 newratio = float(neww) / float(newh)
415 if imageratio > newratio:
416 transformw = int(round(newh * imageratio))
417 pixbuf = pixbuf.scale_simple(transformw, newh, gtk.gdk.INTERP_BILINEAR)
418 pixbuf = crop_pixbuf(pixbuf, 0, 0, neww, newh)
419 elif imageratio < newratio:
420 transformh = int(round(neww / imageratio))
421 pixbuf = pixbuf.scale_simple(neww, transformh, gtk.gdk.INTERP_BILINEAR)
422 pixbuf = crop_pixbuf(pixbuf, 0, 0, neww, newh)
423 else:
424 pixbuf = pixbuf.scale_simple(neww, newh, gtk.gdk.INTERP_BILINEAR)
425 return pixbuf
426
427
428class PixbufCache(dict):
429 """
430 A pixbuf cache dict which stores, loads, and saves pixbufs to a cache and to
431 the users filesystem. The naming scheme for thumb files are use hash
432
433 There are huge flaws with this object. It does not have a ceiling, and it
434 does not remove thumbnails from the file system. Essentially meaning the
435 cache directory can grow forever.
436 """
437 def __init__(self, *args, **kwargs):
438 super(PixbufCache, self).__init__()
439
440 def check_cache(self, uri):
441 return self[uri]
442
443 def get_buff(self, key):
444 thumbpath = os.path.expanduser("~/.cache/GAJ/1_" + str(hash(key)))
445 if os.path.exists(thumbpath):
446 self[key] = (gtk.gdk.pixbuf_new_from_file(thumbpath), True)
447 return self[key]
448 return None
449
450 def __getitem__(self, key):
451 if self.has_key(key):
452 return super(PixbufCache, self).__getitem__(key)
453 return self.get_buff(key)
454
455 def __setitem__(self, key, (pb, isthumb)):
456 dir_ = os.path.expanduser("~/.cache/GAJ/")
457 if not os.path.exists(os.path.expanduser("~/.cache/GAJ/")):
458 os.makedirs(dir_)
459 path = dir_ + str(hash(isthumb)) + "_" + str(hash(key))
460 if not os.path.exists(path):
461 open(path, 'w').close()
462 pb.save(path, "png")
463 return super(PixbufCache, self).__setitem__(key, (pb, isthumb))
464
465 def get_pixbuf_from_uri(self, uri, size=SIZE_LARGE, iconscale=1, w=0, h=0):
466 """
467 Returns a pixbuf and True if a thumbnail was found, else False. Uses the
468 Pixbuf Cache for thumbnail compatible files. If the pixbuf is a thumb
469 it is cached.
470
471 Arguments:
472 :param uri: a uri on the disk
473 :param size: a size tuple from thumbfactory
474 :param iconscale: a factor to reduce icons by (not thumbs)
475 :param w: resulting width
476 :param h: resulting height
477
478 Warning! This function is in need of a serious clean up.
479
480 :returns: a tuple containing a :class:`Pixbuf <gtk.gdk.Pixbuf>` and bool
481 which is True if a thumbnail was found
482 """
483 try:
484 cached = self.check_cache(uri)
485 except gobject.GError:
486 cached = None
487 if cached:
488 return cached
489 gfile = GioFile.create(uri)
490 thumb = True
491 if gfile:
492 if gfile.has_preview():
493 pb = gfile.get_thumbnail(size=size)
494 else:
495 iconsize = int(size[0]*iconscale)
496 pb = gfile.get_icon(size=iconsize)
497 thumb = False
498 else: pb = None
499 if not pb:
500 pb = ICON_THEME.lookup_icon(gtk.STOCK_MISSING_IMAGE, int(size[0]*iconscale), gtk.ICON_LOOKUP_FORCE_SVG).load_icon()
501 thumb = False
502 if thumb:
503 pb = scale_to_fill(pb, w, h)
504 self[uri] = (pb, thumb)
505 return pb, thumb
506
507PIXBUFCACHE = PixbufCache()
508
509def get_icon_for_name(name, size):
510 """
511 return a icon for a name
512 """
513 size = int(size)
514 ICONS[(size, size)]
515 if ICONS[(size, size)].has_key(name):
516 return ICONS[(size, size)][name]
517 info = ICON_THEME.lookup_icon(name, size, gtk.ICON_LOOKUP_USE_BUILTIN)
518 if not info:
519 return None
520 location = info.get_filename()
521 return get_icon_for_uri(location, size)
522
523def get_icon_for_uri(uri, size):
524 pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(uri, size, size)
525 ICONS[(size, size)][uri] = pixbuf
526 return pixbuf
527
528def get_icon_from_object_at_uri(uri, size):
529 """
530 Returns a icon from a event at size
531
532 :param uri: a uri string
533 :param size: a int representing the size in pixels of the icon
534
535 :returns: a :class:`Pixbuf <gtk.gdk.Pixbuf>`
536 """
537 gfile = GioFile.create(uri)
538 if gfile:
539 pb = gfile.get_icon(size=size)
540 if pb:
541 return pb
542 return False
543
544
545
546##
547## Other useful methods
548##
549
550def is_command_available(command):
551 """
552 Checks whether the given command is available, by looking for it in
553 the PATH.
554
555 This is useful for ensuring that optional dependencies on external
556 applications are fulfilled.
557 """
558 assert len(" a".split()) == 1, "No arguments are accepted in command"
559 for directory in os.environ["PATH"].split(os.pathsep):
560 if os.path.exists(os.path.join(directory, command)):
561 return True
562 return False
563
564def launch_command(command, arguments=None):
565 """
566 Launches a program as an independent process.
567 """
568 if not arguments:
569 arguments = []
570 null = os.open(os.devnull, os.O_RDWR)
571 subprocess.Popen([command] + arguments, stdout=null, stderr=null,
572 close_fds=True)
573
574def launch_string_command(command):
575 """
576 Launches a program as an independent from a string
577 """
578 command = command.split(" ")
579 null = os.open(os.devnull, os.O_RDWR)
580 subprocess.Popen(command, stdout=null, stderr=null,
581 close_fds=True)
582
5830
=== removed file 'src/daywidgets.py'
--- src/daywidgets.py 2010-04-22 14:05:34 +0000
+++ src/daywidgets.py 1970-01-01 00:00:00 +0000
@@ -1,791 +0,0 @@
1# -.- coding: utf-8 -.-
2#
3# GNOME Activity Journal
4#
5# Copyright © 2009-2010 Seif Lotfy <seif@lotfy.com>
6# Copyright © 2010 Randal Barlow <email.tehk@gmail.com>
7# Copyright © 2010 Siegfried Gevatter <siegfried@gevatter.com>
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <http://www.gnu.org/licenses/>.
21
22import gtk
23import time
24import gobject
25import gettext
26import cairo
27import pango
28import math
29import os
30import urllib
31from datetime import date
32
33from zeitgeist.client import ZeitgeistClient
34from zeitgeist.datamodel import Event, Subject, Interpretation, Manifestation, \
35 ResultType, TimeRange
36
37from common import shade_gdk_color, combine_gdk_color, get_gtk_rgba
38from widgets import *
39from thumb import ThumbBox
40from timeline import TimelineView, TimelineHeader
41from eventgatherer import get_dayevents, get_file_events
42
43CLIENT = ZeitgeistClient()
44
45
46class GenericViewWidget(gtk.VBox):
47 __gsignals__ = {
48 "unfocus-day" : (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ()),
49 # Sends a list zeitgeist events
50 }
51
52 def __init__(self):
53 gtk.VBox.__init__(self)
54 self.daylabel = None
55 self.connect("style-set", self.change_style)
56
57 def _set_date_strings(self):
58 self.date_string = date.fromtimestamp(self.day_start).strftime("%d %B")
59 self.year_string = date.fromtimestamp(self.day_start).strftime("%Y")
60 if time.time() < self.day_end and time.time() > self.day_start:
61 self.week_day_string = _("Today")
62 elif time.time() - 86400 < self.day_end and time.time() - 86400> self.day_start:
63 self.week_day_string = _("Yesterday")
64 else:
65 self.week_day_string = date.fromtimestamp(self.day_start).strftime("%A")
66 self.emit("style-set", None)
67
68 def click(self, widget, event):
69 if event.button in (1, 3):
70 self.emit("unfocus-day")
71
72 def change_style(self, widget, style):
73 rc_style = self.style
74 color = rc_style.bg[gtk.STATE_NORMAL]
75 color = shade_gdk_color(color, 102/100.0)
76 self.view.modify_bg(gtk.STATE_NORMAL, color)
77 self.view.modify_base(gtk.STATE_NORMAL, color)
78
79
80class ThumbnailDayWidget(GenericViewWidget):
81
82 def __init__(self):
83 GenericViewWidget.__init__(self)
84 self.monitors = []
85 self.scrolledwindow = gtk.ScrolledWindow()
86 self.scrolledwindow.set_shadow_type(gtk.SHADOW_NONE)
87 self.scrolledwindow.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
88 self.view = ThumbBox()
89 self.scrolledwindow.add_with_viewport(self.view)
90 self.scrolledwindow.get_children()[0].set_shadow_type(gtk.SHADOW_NONE)
91 self.pack_end(self.scrolledwindow)
92
93 def set_day(self, start, end):
94
95 self.day_start = start
96 self.day_end = end
97 for widget in self:
98 if self.scrolledwindow != widget:
99 self.remove(widget)
100 self._set_date_strings()
101 today = int(time.time() ) - 7*86400
102 if self.daylabel:
103 #Disconnect here
104 pass
105 if self.day_start < today:
106 self.daylabel = DayLabel(self.date_string, self.week_day_string+", "+ self.year_string)
107 else:
108 self.daylabel = DayLabel(self.week_day_string, self.date_string+", "+ self.year_string)
109 self.daylabel.set_size_request(100, 60)
110 self.daylabel.connect("button-press-event", self.click)
111 self.daylabel.set_tooltip_text(_("Click to return to multiday view"))
112 self.pack_start(self.daylabel, False, False)
113 self.show_all()
114 self.view.hide_all()
115 self.daylabel.show_all()
116 self.view.show()
117
118 hour = 60*60
119 get_file_events(start*1000, (start + 12*hour -1) * 1000, self.set_morning_events)
120 get_file_events((start + 12*hour)*1000, (start + 18*hour - 1)*1000, self.set_afternoon_events)
121 get_file_events((start + 18*hour)*1000, end*1000, self.set_evening_events)
122
123 def set_morning_events(self, events):
124 if len(events) > 0:
125 timestamp = int(events[0].timestamp)
126 if self.day_start*1000 <= timestamp and timestamp < (self.day_start + 12*60*60)*1000:
127 self.view.set_morning_events(events)
128 self.view.views[0].show_all()
129 self.view.labels[0].show_all()
130 else:
131 self.view.set_morning_events(events)
132 self.view.views[0].hide_all()
133 self.view.labels[0].hide_all()
134
135 def set_afternoon_events(self, events):
136 if len(events) > 0:
137 timestamp = int(events[0].timestamp)
138 if (self.day_start + 12*60*60)*1000 <= timestamp and timestamp < (self.day_start + 18*60*60)*1000:
139 self.view.set_afternoon_events(events)
140 self.view.views[1].show_all()
141 self.view.labels[1].show_all()
142 else:
143 self.view.set_afternoon_events(events)
144 self.view.views[1].hide_all()
145 self.view.labels[1].hide_all()
146
147 def set_evening_events(self, events):
148 if len(events) > 0:
149 timestamp = int(events[0].timestamp)
150 if (self.day_start + 18*60*60)*1000 <= timestamp and timestamp < self.day_end*1000:
151 self.view.set_evening_events(events)
152 self.view.views[2].show_all()
153 self.view.labels[2].show_all()
154 else:
155 self.view.set_evening_events(events)
156 self.view.views[2].hide_all()
157 self.view.labels[2].hide_all()
158
159
160class SingleDayWidget(GenericViewWidget):
161
162 def __init__(self):
163 GenericViewWidget.__init__(self)
164 self.ruler = TimelineHeader()
165 self.scrolledwindow = gtk.ScrolledWindow()
166 self.scrolledwindow.set_shadow_type(gtk.SHADOW_NONE)
167 self.scrolledwindow.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_ALWAYS)
168 self.view = TimelineView()
169 self.scrolledwindow.add(self.view)
170 self.pack_end(self.scrolledwindow)
171 self.pack_end(self.ruler, False, False)
172 self.view.set_events(gtk.gdk.POINTER_MOTION_MASK | gtk.gdk.POINTER_MOTION_HINT_MASK)
173
174 def set_day(self, start, end):
175 self.day_start = start
176 self.day_end = end
177 for widget in self:
178 if widget not in (self.ruler, self.scrolledwindow):
179 self.remove(widget)
180 self._set_date_strings()
181 today = int(time.time() ) - 7*86400
182 if self.daylabel:
183 #Disconnect here
184 pass
185 if self.day_start < today:
186 self.daylabel = DayLabel(self.date_string, self.week_day_string+", "+ self.year_string)
187 else:
188 self.daylabel = DayLabel(self.week_day_string, self.date_string+", "+ self.year_string)
189 self.daylabel.set_size_request(100, 60)
190 self.daylabel.connect("button-press-event", self.click)
191 self.daylabel.set_tooltip_text(_("Click to return to multiday view"))
192
193 self.pack_start(self.daylabel, False, False)
194 get_dayevents(start*1000, end*1000, 1, self.view.set_model_from_list)
195 self.show_all()
196
197 def change_style(self, widget, style):
198 GenericViewWidget.change_style(self, widget, style)
199 rc_style = self.style
200 color = rc_style.bg[gtk.STATE_NORMAL]
201 color = shade_gdk_color(color, 102/100.0)
202 self.ruler.modify_bg(gtk.STATE_NORMAL, color)
203
204
205class DayWidget(gtk.VBox):
206
207 __gsignals__ = {
208 "focus-day" : (gobject.SIGNAL_RUN_FIRST,
209 gobject.TYPE_NONE,
210 (gobject.TYPE_INT,))
211 }
212
213 def __init__(self, start, end):
214 super(DayWidget, self).__init__()
215 hour = 60*60
216 self.day_start = start
217 self.day_end = end
218
219 self._set_date_strings()
220 self._periods = [
221 (_("Morning"), start, start + 12*hour - 1),
222 (_("Afternoon"), start + 12*hour, start + 18*hour - 1),
223 (_("Evening"), start + 18*hour, end),
224 ]
225
226 self._init_widgets()
227 self._init_pinbox()
228 gobject.timeout_add_seconds(
229 86400 - (int(time.time() - time.timezone) % 86400), self._refresh)
230
231
232 self.show_all()
233 self._init_events()
234
235 def refresh(self):
236 pass
237
238 def _set_date_strings(self):
239 self.date_string = date.fromtimestamp(self.day_start).strftime("%d %B")
240 self.year_string = date.fromtimestamp(self.day_start).strftime("%Y")
241 if time.time() < self.day_end and time.time() > self.day_start:
242 self.week_day_string = _("Today")
243 elif time.time() - 86400 < self.day_end and time.time() - 86400> self.day_start:
244 self.week_day_string = _("Yesterday")
245 else:
246 self.week_day_string = date.fromtimestamp(self.day_start).strftime("%A")
247 self.emit("style-set", None)
248
249 def _refresh(self):
250 self._init_date_label()
251 self._init_pinbox()
252 pinbox.show_all()
253
254 def _init_pinbox(self):
255 if self.day_start <= time.time() < self.day_end:
256 self.view.pack_start(pinbox, False, False)
257
258 def _init_widgets(self):
259 self.vbox = gtk.VBox()
260 self.pack_start(self.vbox)
261
262 self.daylabel = None
263
264 self._init_date_label()
265
266 #label.modify_bg(gtk.STATE_SELECTED, style.bg[gtk.STATE_SELECTED])
267
268 self.view = gtk.VBox()
269 scroll = gtk.ScrolledWindow()
270 scroll.set_shadow_type(gtk.SHADOW_NONE)
271
272 evbox2 = gtk.EventBox()
273 evbox2.add(self.view)
274 self.view.set_border_width(6)
275
276 scroll.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
277 scroll.add_with_viewport(evbox2)
278 for w in scroll.get_children():
279 w.set_shadow_type(gtk.SHADOW_NONE)
280 self.vbox.pack_start(scroll)
281 self.show_all()
282
283 def change_style(widget, style):
284 rc_style = self.style
285 color = rc_style.bg[gtk.STATE_NORMAL]
286 color = shade_gdk_color(color, 102/100.0)
287 evbox2.modify_bg(gtk.STATE_NORMAL, color)
288
289 self.connect("style-set", change_style)
290
291 def _init_date_label(self):
292 self._set_date_strings()
293
294 today = int(time.time() ) - 7*86400
295 if self.daylabel:
296 # Disconnect HERE
297 pass
298 if self.day_start < today:
299 self.daylabel = DayLabel(self.date_string, self.week_day_string+", "+ self.year_string)
300 else:
301 self.daylabel = DayLabel(self.week_day_string, self.date_string+", "+ self.year_string)
302 self.daylabel.connect("button-press-event", self.click)
303 self.daylabel.set_tooltip_text(
304 _("Left click for a detailed timeline view")
305 + u"\n" +
306 _("Right click for a thumbnail view"))
307 self.daylabel.set_size_request(100, 60)
308 evbox = gtk.EventBox()
309 evbox.add(self.daylabel)
310 evbox.set_size_request(100, 60)
311 self.vbox.pack_start(evbox, False, False)
312 def cursor_func(x, y):
313 if evbox.window:
314 evbox.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.HAND2))
315 self.connect("motion-notify-event", cursor_func)
316
317 def change_style(widget, style):
318 rc_style = self.style
319 color = rc_style.bg[gtk.STATE_NORMAL]
320 evbox.modify_bg(gtk.STATE_NORMAL, color)
321 self.daylabel.modify_bg(gtk.STATE_NORMAL, color)
322
323 self.connect("style-set", change_style)
324 #self.connect("leave-notify-event", lambda x, y: evbox.window.set_cursor(None))
325
326 self.vbox.reorder_child(self.daylabel, 0)
327
328 def click(self, widget, event):
329 if event.button == 1:
330 self.emit("focus-day", 1)
331 elif event.button == 3:
332 self.emit("focus-day", 2)
333
334 def _init_events(self):
335 for w in self.view:
336 if not w == pinbox:
337 self.view.remove(w)
338 for period in self._periods:
339 part = DayPartWidget(period[0], period[1], period[2])
340 self.view.pack_start(part, False, False)
341
342
343class CategoryBox(gtk.HBox):
344
345 def __init__(self, category, events, pinnable = False):
346 super(CategoryBox, self).__init__()
347 self.view = gtk.VBox(True)
348 self.vbox = gtk.VBox()
349 for event in events:
350 item = Item(event, pinnable)
351 hbox = gtk.HBox ()
352 #label = gtk.Label("")
353 #hbox.pack_start(label, False, False, 7)
354 hbox.pack_start(item, True, True, 0)
355 self.view.pack_start(hbox, False, False, 0)
356 hbox.show()
357 #label.show()
358
359 # If this isn't a set of ungrouped events, give it a label
360 if category:
361 # Place the items into a box and simulate left padding
362 self.box = gtk.HBox()
363 #label = gtk.Label("")
364 self.box.pack_start(self.view)
365
366 hbox = gtk.HBox()
367 # Add the title button
368 if category in SUPPORTED_SOURCES:
369 text = SUPPORTED_SOURCES[category].group_label(len(events))
370 else:
371 text = "Unknown"
372
373 label = gtk.Label()
374 label.set_markup("<span>%s</span>" % text)
375 #label.set_ellipsize(pango.ELLIPSIZE_END)
376
377 hbox.pack_start(label, True, True, 0)
378
379 label = gtk.Label()
380 label.set_markup("<span>(%d)</span>" % len(events))
381 label.set_alignment(1.0,0.5)
382 label.set_alignment(1.0,0.5)
383 hbox.pack_end(label, False, False, 2)
384
385 hbox.set_border_width(3)
386
387 self.expander = gtk.Expander()
388 self.expander.set_label_widget(hbox)
389
390 self.vbox.pack_start(self.expander, False, False)
391 self.expander.add(self.box)#
392
393 self.pack_start(self.vbox, True, True, 24)
394
395 self.expander.show_all()
396 self.show()
397 hbox.show_all()
398 label.show_all()
399 self.view.show()
400
401 else:
402 self.box = self.view
403 self.vbox.pack_end(self.box)
404 self.box.show()
405 self.show()
406
407 self.pack_start(self.vbox, True, True, 16)
408
409 self.show_all()
410
411 def on_toggle(self, view, bool):
412 if bool:
413 self.box.show()
414 else:
415 self.box.hide()
416 pinbox.show_all()
417
418
419class DayLabel(gtk.DrawingArea):
420
421 _events = (
422 gtk.gdk.ENTER_NOTIFY_MASK | gtk.gdk.LEAVE_NOTIFY_MASK |
423 gtk.gdk.KEY_PRESS_MASK | gtk.gdk.BUTTON_MOTION_MASK |
424 gtk.gdk.POINTER_MOTION_HINT_MASK | gtk.gdk.BUTTON_RELEASE_MASK |
425 gtk.gdk.BUTTON_PRESS_MASK
426 )
427
428 def __init__(self, day, date):
429 if day == _("Today"):
430 self.leading = True
431 else:
432 self.leading = False
433 super(DayLabel, self).__init__()
434 self.date = date
435 self.day = day
436 self.set_events(self._events)
437 self.connect("expose_event", self.expose)
438 self.connect("enter-notify-event", self._on_enter)
439 self.connect("leave-notify-event", self._on_leave)
440
441 def _on_enter(self, widget, event):
442 widget.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.HAND2))
443
444 def _on_leave(self, widget, event):
445 widget.window.set_cursor(None)
446
447 def expose(self, widget, event):
448 context = widget.window.cairo_create()
449 self.context = context
450
451 bg = self.style.bg[0]
452 red, green, blue = bg.red/65535.0, bg.green/65535.0, bg.blue/65535.0
453 self.font_name = self.style.font_desc.get_family()
454
455 widget.style.set_background(widget.window, gtk.STATE_NORMAL)
456
457 # set a clip region for the expose event
458 context.rectangle(event.area.x, event.area.y, event.area.width, event.area.height)
459 context.clip()
460 self.draw(widget, event, context)
461 self.day_text(widget, event, context)
462 return False
463
464 def day_text(self, widget, event, context):
465 actual_y = self.get_size_request()[1]
466 if actual_y > event.area.height:
467 y = actual_y
468 else:
469 y = event.area.height
470 x = event.area.width
471 gc = self.style.fg_gc[gtk.STATE_SELECTED if self.leading else gtk.STATE_NORMAL]
472 layout = widget.create_pango_layout(self.day)
473 layout.set_font_description(pango.FontDescription(self.font_name + " Bold 15"))
474 w, h = layout.get_pixel_size()
475 widget.window.draw_layout(gc, (x-w)/2, (y)/2 - h + 5, layout)
476 self.date_text(widget, event, context, (y)/2 + 5)
477
478 def date_text(self, widget, event, context, lastfontheight):
479 gc = self.style.fg_gc[gtk.STATE_SELECTED if self.leading else gtk.STATE_INSENSITIVE]
480 layout = widget.create_pango_layout(self.date)
481 layout.set_font_description(pango.FontDescription(self.font_name + " 10"))
482 w, h = layout.get_pixel_size()
483 widget.window.draw_layout(gc, (event.area.width-w)/2, lastfontheight, layout)
484
485 def draw(self, widget, event, context):
486 if self.leading:
487 bg = self.style.bg[gtk.STATE_SELECTED]
488 red, green, blue = bg.red/65535.0, bg.green/65535.0, bg.blue/65535.0
489 else:
490 bg = self.style.bg[gtk.STATE_NORMAL]
491 red = (bg.red * 125 / 100)/65535.0
492 green = (bg.green * 125 / 100)/65535.0
493 blue = (bg.blue * 125 / 100)/65535.0
494 x = 0; y = 0
495 r = 5
496 w, h = event.area.width, event.area.height
497 context.set_source_rgba(red, green, blue, 1)
498 context.new_sub_path()
499 context.arc(r+x, r+y, r, math.pi, 3 * math.pi /2)
500 context.arc(w-r, r+y, r, 3 * math.pi / 2, 0)
501 context.close_path()
502 context.rectangle(0, r, w, h)
503 context.fill_preserve()
504
505
506class DayButton(gtk.DrawingArea):
507 leading = False
508 pressed = False
509 sensitive = True
510 hover = False
511 header_size = 60
512 bg_color = (0, 0, 0, 0)
513 header_color = (1, 1, 1, 1)
514 leading_header_color = (1, 1, 1, 1)
515 internal_color = (0, 1, 0, 1)
516 arrow_color = (1,1,1,1)
517 arrow_color_selected = (1, 1, 1, 1)
518
519 __gsignals__ = {
520 "clicked": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,()),
521 }
522 _events = (
523 gtk.gdk.ENTER_NOTIFY_MASK | gtk.gdk.LEAVE_NOTIFY_MASK |
524 gtk.gdk.KEY_PRESS_MASK | gtk.gdk.BUTTON_RELEASE_MASK | gtk.gdk.BUTTON_PRESS_MASK |
525 gtk.gdk.MOTION_NOTIFY | gtk.gdk.POINTER_MOTION_MASK
526 )
527 def __init__(self, side = 0, leading = False):
528 super(DayButton, self).__init__()
529 self.set_events(self._events)
530 self.set_flags(gtk.CAN_FOCUS)
531 self.leading = leading
532 self.side = side
533 self.connect("button_press_event", self.on_press)
534 self.connect("button_release_event", self.clicked_sender)
535 self.connect("key_press_event", self.keyboard_clicked_sender)
536 self.connect("motion_notify_event", self.on_hover)
537 self.connect("leave_notify_event", self._enter_leave_notify, False)
538 self.connect("expose_event", self.expose)
539 self.connect("style-set", self.change_style)
540 self.set_size_request(20, -1)
541
542 def set_sensitive(self, case):
543 self.sensitive = case
544 self.queue_draw()
545
546 def _enter_leave_notify(self, widget, event, bol):
547 self.hover = bol
548 self.queue_draw()
549
550 def on_hover(self, widget, event):
551 if event.y > self.header_size:
552 if not self.hover:
553 self.hover = True
554 self.queue_draw()
555 else:
556 if self.hover:
557 self.hover = False
558 self.queue_draw()
559 return False
560
561 def on_press(self, widget, event):
562 if event.y > self.header_size:
563 self.pressed = True
564 self.queue_draw()
565
566 def keyboard_clicked_sender(self, widget, event):
567 if event.keyval in (gtk.keysyms.Return, gtk.keysyms.space):
568 if self.sensitive:
569 self.emit("clicked")
570 self.pressed = False
571 self.queue_draw()
572 return True
573 return False
574
575 def clicked_sender(self, widget, event):
576 if event.y > self.header_size:
577 if self.sensitive:
578 self.emit("clicked")
579 self.pressed = False
580 self.queue_draw()
581 return True
582
583 def change_style(self, *args, **kwargs):
584 self.bg_color = get_gtk_rgba(self.style, "bg", 0)
585 self.header_color = get_gtk_rgba(self.style, "bg", 0, 1.25)
586 self.leading_header_color = get_gtk_rgba(self.style, "bg", 3)
587 self.internal_color = get_gtk_rgba(self.style, "bg", 0, 1.02)
588 self.arrow_color = get_gtk_rgba(self.style, "text", 0, 0.6)
589 self.arrow_color_selected = get_gtk_rgba(self.style, "bg", 3)
590 self.arrow_color_insensitive = get_gtk_rgba(self.style, "text", 4)
591
592 def expose(self, widget, event):
593 context = widget.window.cairo_create()
594
595 context.set_source_rgba(*self.bg_color)
596 context.set_operator(cairo.OPERATOR_SOURCE)
597 context.paint()
598 context.rectangle(event.area.x, event.area.y, event.area.width, event.area.height)
599 context.clip()
600
601 x = 0; y = 0
602 r = 5
603 w, h = event.area.width, event.area.height
604 size = 20
605 if self.sensitive:
606 context.set_source_rgba(*(self.leading_header_color if self.leading else self.header_color))
607 context.new_sub_path()
608 context.move_to(x+r,y)
609 context.line_to(x+w-r,y)
610 context.curve_to(x+w,y,x+w,y,x+w,y+r)
611 context.line_to(x+w,y+h-r)
612 context.curve_to(x+w,y+h,x+w,y+h,x+w-r,y+h)
613 context.line_to(x+r,y+h)
614 context.curve_to(x,y+h,x,y+h,x,y+h-r)
615 context.line_to(x,y+r)
616 context.curve_to(x,y,x,y,x+r,y)
617 context.set_source_rgba(*(self.leading_header_color if self.leading else self.header_color))
618 context.close_path()
619 context.rectangle(0, r, w, self.header_size)
620 context.fill()
621 context.set_source_rgba(*self.internal_color)
622 context.rectangle(0, self.header_size, w, h)
623 context.fill()
624 if self.hover:
625 widget.style.paint_box(widget.window, gtk.STATE_PRELIGHT, gtk.SHADOW_OUT,
626 event.area, widget, "button",
627 event.area.x, self.header_size,
628 w, h-self.header_size)
629 size = 10
630 if not self.sensitive:
631 state = gtk.STATE_INSENSITIVE
632 elif self.is_focus() or self.pressed:
633 widget.style.paint_focus(widget.window, gtk.STATE_ACTIVE, event.area,
634 widget, None, event.area.x, self.header_size,
635 w, h-self.header_size)
636 state = gtk.STATE_SELECTED
637 else:
638 state = gtk.STATE_NORMAL
639 arrow = gtk.ARROW_RIGHT if self.side else gtk.ARROW_LEFT
640 self.style.paint_arrow(widget.window, state, gtk.SHADOW_NONE, None,
641 self, "arrow", arrow, True,
642 w/2-size/2, h/2 + size/2, size, size)
643
644
645class EventGroup(gtk.VBox):
646
647 def __init__(self, title):
648 super(EventGroup, self).__init__()
649
650 # Create the title label
651 self.label = gtk.Label(title)
652 self.label.set_alignment(0.03, 0.5)
653 self.pack_start(self.label, False, False, 6)
654 self.events = []
655 # Create the main container
656 self.view = gtk.VBox()
657 self.pack_start(self.view)
658
659 # Connect to relevant signals
660 self.connect("style-set", self.on_style_change)
661
662 # Populate the widget with content
663 self.get_events()
664
665 def on_style_change(self, widget, style):
666 """ Update used colors according to the system theme. """
667 color = self.style.bg[gtk.STATE_NORMAL]
668 fcolor = self.style.fg[gtk.STATE_NORMAL]
669 color = combine_gdk_color(color, fcolor)
670 self.label.modify_fg(gtk.STATE_NORMAL, color)
671
672 @staticmethod
673 def event_exists(uri):
674 # TODO: Move this into Zeitgeist's datamodel.py
675 return not uri.startswith("file://") or os.path.exists(
676 urllib.unquote(str(uri[7:])))
677
678 def set_events(self, events):
679 self.events = []
680 for widget in self.view:
681 self.view.remove(widget)
682
683 if self == pinbox:
684 box = CategoryBox(None, events, True)
685 self.view.pack_start(box)
686 else:
687 categories = {}
688 for event in events:
689 subject = event.subjects[0]
690 if self.event_exists(subject.uri):
691 if not categories.has_key(subject.interpretation):
692 categories[subject.interpretation] = []
693 categories[subject.interpretation].append(event)
694 self.events.append(event)
695
696 if not categories:
697 self.hide_all()
698 else:
699 # Make the group title, etc. visible
700 self.show_all()
701
702 ungrouped_events = []
703 for key in sorted(categories.iterkeys()):
704 events = categories[key]
705 if len(events) > 3:
706 box = CategoryBox(key, list(reversed(events)))
707 self.view.pack_start(box)
708 else:
709 ungrouped_events += events
710
711 ungrouped_events.sort(key=lambda x: x.timestamp)
712 box = CategoryBox(None, ungrouped_events)
713 self.view.pack_start(box)
714
715 # Make the group's contents visible
716 self.view.show()
717 pinbox.show_all()
718
719 if len(self.events) == 0:
720 self.hide()
721 else:
722 self.show()
723
724 def get_events(self, *discard):
725 if self.event_templates and len(self.event_templates) > 0:
726 CLIENT.find_events_for_templates(self.event_templates,
727 self.set_events, self.event_timerange, num_events=50000,
728 result_type=ResultType.MostRecentSubjects)
729 else:
730 self.view.hide()
731
732
733class DayPartWidget(EventGroup):
734
735 def __init__(self, title, start, end):
736 # Setup event criteria for querying
737 self.event_timerange = [start * 1000, end * 1000]
738 self.event_templates = (
739 Event.new_for_values(interpretation=Interpretation.VISIT_EVENT.uri),
740 Event.new_for_values(interpretation=Interpretation.MODIFY_EVENT.uri),
741 Event.new_for_values(interpretation=Interpretation.CREATE_EVENT.uri),
742 Event.new_for_values(interpretation=Interpretation.OPEN_EVENT.uri),
743 )
744
745 # Initialize the widget
746 super(DayPartWidget, self).__init__(title)
747
748 # FIXME: Move this into EventGroup
749 CLIENT.install_monitor(self.event_timerange, self.event_templates,
750 self.notify_insert_handler, self.notify_delete_handler)
751
752 def notify_insert_handler(self, time_range, events):
753 # FIXME: Don't regenerate everything, we already get the
754 # information we need
755 self.get_events()
756
757 def notify_delete_handler(self, time_range, event_ids):
758 # FIXME: Same as above
759 self.get_events()
760
761class PinBox(EventGroup):
762
763 def __init__(self):
764 # Setup event criteria for querying
765 self.event_timerange = TimeRange.until_now()
766
767 # Initialize the widget
768 super(PinBox, self).__init__(_("Pinned items"))
769
770 # Connect to relevant signals
771 bookmarker.connect("reload", self.get_events)
772
773 @property
774 def event_templates(self):
775 if not bookmarker.bookmarks:
776 # Abort, or we will query with no templates and get lots of
777 # irrelevant events.
778 return None
779
780 templates = []
781 for bookmark in bookmarker.bookmarks:
782 templates.append(Event.new_for_values(subject_uri=bookmark))
783 return templates
784
785 def set_events(self, *args, **kwargs):
786 super(PinBox, self).set_events(*args, **kwargs)
787 # Make the pin icons visible
788 self.view.show_all()
789 self.show_all()
790
791pinbox = PinBox()
7920
=== removed file 'src/eventgatherer.py'
--- src/eventgatherer.py 2010-04-07 03:17:49 +0000
+++ src/eventgatherer.py 1970-01-01 00:00:00 +0000
@@ -1,224 +0,0 @@
1# -.- coding: utf-8 -.-
2#
3# GNOME Activity Journal
4#
5# Copyright © 2010 Seif Lotfy <seif@lotfy.com>
6# Copyright © 2010 Siegfried Gevatter <siegfried@gevatter.com>
7#
8# This program is free software: you can redistribute it and/or modify
9# it under the terms of the GNU General Public License as published by
10# the Free Software Foundation, either version 3 of the License, or
11# (at your option) any later version.
12#
13# This program is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16# GNU General Public License for more details.
17#
18# You should have received a copy of the GNU General Public License
19# along with this program. If not, see <http://www.gnu.org/licenses/>.
20
21import time
22from datetime import timedelta, datetime
23import gtk
24import random
25import os
26import urllib
27from zeitgeist.client import ZeitgeistClient
28from zeitgeist.datamodel import Event, Subject, Interpretation, Manifestation, \
29 ResultType, TimeRange, StorageState
30
31CLIENT = ZeitgeistClient()
32
33event_templates = (
34 Event.new_for_values(interpretation=Interpretation.VISIT_EVENT.uri),
35 Event.new_for_values(interpretation=Interpretation.MODIFY_EVENT.uri),
36 Event.new_for_values(interpretation=Interpretation.CREATE_EVENT.uri),
37 Event.new_for_values(interpretation=Interpretation.OPEN_EVENT.uri),
38 Event.new_for_values(interpretation=Interpretation.CLOSE_EVENT.uri),
39)
40
41EVENTS = {}
42
43def event_exists(uri):
44 # TODO: Move this into Zeitgeist's datamodel.py
45 if uri.startswith("trash://"):
46 return False
47 return not uri.startswith("file://") or os.path.exists(
48 urllib.unquote(str(uri[7:])))
49
50def get_dayevents(start, end, result_type, callback, force = False):
51 """
52 :param start: a int time from which to start gathering events in milliseconds
53 :param end: a int time from which to stop gathering events in milliseconds
54 :callback: a callable to be called when the query is done
55 """
56 def handle_find_events(events):
57 results = {}
58 for event in events:
59 uri = event.subjects[0].uri
60 if not event.subjects[0].uri in results:
61 results[uri] = []
62 if not event.interpretation == Interpretation.CLOSE_EVENT.uri:
63 results[uri].append([event, 0])
64 else:
65 if not len(results[uri]) == 0:
66 #print "***", results[uri]
67 results[uri][len(results[uri])-1][1] = (int(event.timestamp)) - int(results[uri][-1][0].timestamp)
68 else:
69 tend = int(event.timestamp)
70 event.timestamp = str(start)
71 results[uri].append([event, tend - start])
72 events = list(sorted(results.itervalues(), key=lambda r: \
73 r[0][0].timestamp))
74 EVENTS[start+end] = events
75 callback(events)
76
77 def notify_insert_handler_morning(timerange, events):
78 find_events()
79
80 def find_events():
81 event_templates = []
82 CLIENT.find_events_for_templates(event_templates, handle_find_events,
83 [start, end], num_events=50000,
84 result_type=result_type)
85
86 if not EVENTS.has_key(start+end) or force:
87 find_events()
88 event_timerange = [start, end]
89 event_templates = (
90 Event.new_for_values(interpretation=Interpretation.VISIT_EVENT.uri),
91 Event.new_for_values(interpretation=Interpretation.MODIFY_EVENT.uri),
92 Event.new_for_values(interpretation=Interpretation.CREATE_EVENT.uri),
93 Event.new_for_values(interpretation=Interpretation.OPEN_EVENT.uri),
94 )
95 # FIXME: Move this into EventGroup
96
97 CLIENT.install_monitor([start, end], event_templates,
98 notify_insert_handler_morning, notify_insert_handler_morning)
99
100
101
102 else:
103 callback(EVENTS[start+end])
104
105
106
107def get_file_events(start, end, callback, force = False):
108 """
109 :param start: a int time from which to start gathering events in milliseconds
110 :param end: a int time from which to stop gathering events in milliseconds
111 :callback: a callable to be called when the query is done
112 """
113
114 def handle_find_events(events):
115 results = {}
116 for event in events:
117 uri = event.subjects[0].uri
118 if not event.subjects[0].uri in results:
119 results[uri] = []
120 results[uri].append(event)
121 events = [result[0] for result in results.values()]
122 EVENTS[start+end] = events
123 callback(events)
124
125 def notify_insert_handler_morning(timerange, events):
126 find_events()
127
128 event_templates = (
129 #Event.new_for_values(interpretation=Interpretation.VISIT_EVENT.uri),
130 #Event.new_for_values(interpretation=Interpretation.MODIFY_EVENT.uri),
131 #Event.new_for_values(interpretation=Interpretation.CREATE_EVENT.uri),
132 #Event.new_for_values(interpretation=Interpretation.OPEN_EVENT.uri),
133 )
134
135 def find_events():
136 CLIENT.find_events_for_templates(event_templates, handle_find_events,
137 [start, end], num_events=50000,
138 result_type=ResultType.LeastRecentEvents)
139
140 if not EVENTS.has_key(start+end) or force:
141 find_events()
142 event_timerange = [start, end]
143
144 # FIXME: Move this into EventGroup
145
146 CLIENT.install_monitor([start, end], event_templates,
147 notify_insert_handler_morning, notify_insert_handler_morning)
148
149
150
151 else:
152 callback(EVENTS[start+end])
153
154
155def datelist(n, callback):
156 """
157 :param n: number of days to query
158 :callback: a callable to be called when the query is done
159 """
160 event_templates = (
161 Event.new_for_values(interpretation=Interpretation.VISIT_EVENT.uri,
162 storage_state=StorageState.Available),
163 Event.new_for_values(interpretation=Interpretation.MODIFY_EVENT.uri,
164 storage_state=StorageState.Available),
165 Event.new_for_values(interpretation=Interpretation.CREATE_EVENT.uri,
166 storage_state=StorageState.Available),
167 Event.new_for_values(interpretation=Interpretation.OPEN_EVENT.uri,
168 storage_state=StorageState.Available),
169 Event.new_for_values(interpretation=Interpretation.CLOSE_EVENT.uri,
170 storage_state=StorageState.Available),
171 )
172 if n == -1:
173 n = int(time.time()/86400)
174 today = int(time.mktime(time.strptime(time.strftime("%d %B %Y"),
175 "%d %B %Y")))
176 today = today - n*86400
177
178 x = []
179
180 def _handle_find_events(ids):
181 x.append((today+len(x)*86400, len(ids)))
182 if len(x) == n+1:
183 callback(x)
184
185 def get_ids(start, end):
186 CLIENT.find_event_ids_for_templates(event_templates,
187 _handle_find_events, [start * 1000, end * 1000],
188 num_events=50000,
189 result_type=0,)
190
191 for i in xrange(n+1):
192 get_ids(today+i*86400, today+i*86400+86399)
193
194
195def get_related_events_for_uri(uri, callback):
196 """
197 :param uri: A uri for which to request related uris using zetigeist
198 :param callback: this callback is called once the events are retrieved for
199 the uris. It is called with a list of events.
200 """
201 def _event_request_handler(uris):
202 """
203 :param uris: a list of uris which are related to the windows current uri
204 Seif look here
205 """
206 templates = []
207 if len(uris) > 0:
208 for i, uri in enumerate(uris):
209 templates += [
210 Event.new_for_values(interpretation=Interpretation.VISIT_EVENT.uri, subject_uri=uri),
211 Event.new_for_values(interpretation=Interpretation.MODIFY_EVENT.uri, subject_uri=uri),
212 Event.new_for_values(interpretation=Interpretation.CREATE_EVENT.uri, subject_uri=uri),
213 Event.new_for_values(interpretation=Interpretation.OPEN_EVENT.uri, subject_uri=uri)
214 ]
215 CLIENT.find_events_for_templates(templates, callback,
216 [0, time.time()*1000], num_events=50000,
217 result_type=ResultType.MostRecentSubjects)
218
219 end = time.time() * 1000
220 start = end - (86400*30*1000)
221 CLIENT.find_related_uris_for_uris([uri], _event_request_handler)
222
223
224
2250
=== added file 'src/histogram.py'
--- src/histogram.py 1970-01-01 00:00:00 +0000
+++ src/histogram.py 2010-05-04 05:28:16 +0000
@@ -0,0 +1,532 @@
1# -.- coding: utf-8 -.-
2#
3# GNOME Activity Journal
4#
5# Copyright © 2010 Randal Barlow <email.tehk@gmail.com>
6# Copyright © 2010 Markus Korn
7# Copyright © 2010 Siegfried Gevatter <siegfried@gevatter.com>
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <http://www.gnu.org/licenses/>.
21
22import datetime
23import cairo
24import calendar
25import gettext
26import gobject
27import gtk
28from math import pi as PI
29import pango
30from common import *
31
32
33def get_gc_from_colormap(widget, shade):
34 """
35 Gets a gtk.gdk.GC and modifies the color by shade
36 """
37 gc = widget.style.text_gc[gtk.STATE_INSENSITIVE]
38 if gc:
39 color = widget.style.text[4]
40 color = shade_gdk_color(color, shade)
41 gc.set_rgb_fg_color(color)
42 return gc
43
44
45class CairoHistogram(gtk.DrawingArea):
46 """
47 A histogram which is represented by a list of dates, and nitems.
48
49 There are a few maintenance issues due to the movement abilities. The widget
50 currently is able to capture motion events when the mouse is outside
51 the widget and the button is pressed if it was initially pressed inside
52 the widget. This event mask magic leaves a few flaws open.
53 """
54 _selected = (0,)
55 padding = 2
56 bottom_padding = 23
57 top_padding = 2
58 wcolumn = 12
59 xincrement = wcolumn + padding
60 start_x_padding = 2
61 max_width = xincrement
62 column_radius = 0
63 stroke_width = 1
64 stroke_offset = 0
65 min_column_height = 4
66 max_column_height = 101
67 gc = None
68 pangofont = None
69 _disable_mouse_motion = False
70 selected_range = 0
71 _highlighted = tuple()
72 _last_location = -1
73 _single_day_only = False
74 colors = {
75 "bg" : (1, 1, 1, 1),
76 "base" : (1, 1, 1, 1),
77 "column_normal" : (1, 1, 1, 1),
78 "column_selected" : (1, 1, 1, 1),
79 "column_alternative" : (1, 1, 1, 1),
80 "column_selected_alternative" : (1, 1, 1, 1),
81 "font_color" : "#ffffff",
82 "stroke" : (1, 1, 1, 0),
83 "shadow" : (1, 1, 1, 0),
84 }
85
86 _store = None
87
88 __gsignals__ = {
89 "selection-set" : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
90 (gobject.TYPE_PYOBJECT,)),
91 "data-updated" : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,()),
92 "column_clicked" : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
93 (gobject.TYPE_PYOBJECT,))
94 }
95 _connections = {"style-set": "change_style",
96 "expose_event": "_expose",
97 "button_press_event": "mouse_press_interaction",
98 "motion_notify_event": "mouse_motion_interaction",
99 "key_press_event": "keyboard_interaction",
100 "scroll-event" : "mouse_scroll_interaction",
101 }
102 _events = (gtk.gdk.KEY_PRESS_MASK | gtk.gdk.BUTTON_MOTION_MASK |
103 gtk.gdk.POINTER_MOTION_HINT_MASK | gtk.gdk.BUTTON_RELEASE_MASK |
104 gtk.gdk.BUTTON_PRESS_MASK)
105
106 def __init__(self):
107 """
108 :param datastore: The.CairoHistograms two dimensional list of dates and nitems
109 :param selected_range: the number of days displayed at once
110 """
111 super(CairoHistogram, self).__init__()
112 self._selected = []
113 self.set_events(self._events)
114 self.set_flags(gtk.CAN_FOCUS)
115 for key, val in self._connections.iteritems():
116 self.connect(key, getattr(self, val))
117 self.font_name = self.style.font_desc.get_family()
118
119 def change_style(self, widget, old_style):
120 """
121 Sets the widgets style and coloring
122 """
123 self.colors = self.colors.copy()
124 self.colors["bg"] = get_gtk_rgba(self.style, "bg", 0)
125 self.colors["base"] = get_gtk_rgba(self.style, "base", 0)
126 self.colors["column_normal"] = get_gtk_rgba(self.style, "text", 4, 1.17)
127 self.colors["column_selected"] = get_gtk_rgba(self.style, "bg", 3)
128 color = self.style.bg[gtk.STATE_NORMAL]
129 fcolor = self.style.fg[gtk.STATE_NORMAL]
130 self.colors["font_color"] = combine_gdk_color(color, fcolor).to_string()
131
132 pal = get_gtk_rgba(self.style, "bg", 3, 1.2)
133 self.colors["column_alternative"] = (pal[2], pal[1], pal[0], 1)
134 self.colors["column_selected_alternative"] = get_gtk_rgba(self.style, "bg", 3, 0.6)
135 self.colors["stroke"] = get_gtk_rgba(self.style, "text", 4)
136 self.colors["shadow"] = get_gtk_rgba(self.style, "text", 4)
137 self.font_size = self.style.font_desc.get_size()/1024
138 self.pangofont = pango.FontDescription(self.font_name + " %d" % self.font_size)
139 self.pangofont.set_weight(pango.WEIGHT_BOLD)
140 self.bottom_padding = self.font_size + 9 + widget.style.ythickness
141 self.gc = get_gc_from_colormap(widget, 0.6)
142
143 def set_store(self, store):
144 if self._store:
145 self._store.disconnect(self._store_connection)
146 self._store = store
147 self.largest = min(max(max(map(lambda x: len(x), store.days)), 1), 100)
148 if not self.get_selected():
149 self.set_selected([datetime.date.today()])
150 self._store_connection = store.connect("update", self.set_store)
151
152 def get_store(self):
153 return self._store
154
155 def _expose(self, widget, event):
156 """
157 The major drawing method that the expose event calls directly
158 """
159 widget.style.set_background(widget.window, gtk.STATE_NORMAL)
160 context = widget.window.cairo_create()
161 self.expose(widget, event, context)
162
163 def expose(self, widget, event, context):
164 """
165 The minor drawing method
166
167 :param event: a gtk event with x and y values
168 :param context: This drawingarea's cairo context from the expose event
169 """
170 if not self.pangofont:
171 self.pangofont = pango.FontDescription(self.font_name + " %d" % self.font_size)
172 self.pangofont.set_weight(pango.WEIGHT_BOLD)
173 if not self.gc:
174 self.gc = get_gc_from_colormap(widget, 0.6)
175 context.set_source_rgba(*self.colors["base"])
176 context.set_operator(cairo.OPERATOR_SOURCE)
177 #context.paint()
178 context.rectangle(event.area.x, event.area.y, event.area.width, event.area.height)
179 context.clip()
180 #context.set_source_rgba(*self.colors["bg"])
181 context.rectangle(event.area.x, event.area.y, event.area.width, event.area.height - self.bottom_padding)
182 context.fill()
183 self.draw_columns_from_store(context, event, self.get_selected())
184 context.set_line_width(1)
185 if type(self) == CairoHistogram:
186 widget.style.paint_shadow(widget.window, gtk.STATE_NORMAL, gtk.SHADOW_IN,
187 event.area, widget, "treeview", event.area.x, event.area.y,
188 event.area.width, event.area.height - self.bottom_padding)
189 if self.is_focus():
190 widget.style.paint_focus(widget.window, gtk.STATE_NORMAL, event.area, widget, None, event.area.x, event.area.y,
191 event.area.width, event.area.height - self.bottom_padding)
192
193 def draw_columns_from_store(self, context, event, selected):
194 """
195 Draws columns from a datastore
196
197 :param context: This drawingarea's cairo context from the expose event
198 :param event: a gtk event with x and y values
199 :param selected: a list of the selected dates
200 """
201 x = self.start_x_padding
202 months_positions = []
203 for day in self.get_store().days:
204 if day.date.day == 1:
205 months_positions += [(day.date, x)]
206 if day.date in self._highlighted:
207 color = self.colors["column_selected_alternative"] if day.date in selected else self.colors["column_alternative"]
208 elif not selected:
209 color = self.colors["column_normal"]
210 if day.date in selected:
211 color = self.colors["column_selected"]
212 else:
213 color = self.colors["column_normal"]
214 self.draw_column(context, x, event.area.height, len(day), color)
215 x += self.xincrement
216 if x > event.area.width: # Check for resize
217 self.set_size_request(x+self.xincrement, event.area.height)
218 for date, xpos in months_positions:
219 edge = 0
220 if (date, xpos) == months_positions[-1]:
221 edge = len(self._store)*self.xincrement
222 self.draw_month(context, xpos - self.padding, event.area.height, date, edge)
223 self.max_width = x # remove me
224
225 def draw_column(self, context, x, maxheight, nitems, color):
226 """
227 Draws a columns at x with height based on nitems, and maxheight
228
229 :param context: The drawingarea's cairo context from the expose event
230 :param x: The current position in the image
231 :param maxheight: The event areas height
232 :param nitems: The number of items in the column to be drawn
233 :param color: A RGBA tuple Example: (0.3, 0.4, 0.8, 1)
234 """
235 if nitems < 2:
236 nitems = 2
237 elif nitems > self.max_column_height:
238 nitems = self.max_column_height
239 maxheight = maxheight - self.bottom_padding - 2
240 #height = int((maxheight-self.top_padding-2) * (self.largest*math.log(nitems)/math.log(self.largest))/100)
241 height = int(((float(nitems)/self.largest)*(maxheight-2))) - self.top_padding
242 #height = min(int((maxheight*self.largest/100) * (1 - math.e**(-0.025*nitems))), maxheight)
243 if height < self.min_column_height:
244 height = self.min_column_height
245 y = maxheight - height
246 context.set_source_rgba(*color)
247 context.move_to(x + self.column_radius, y)
248 context.new_sub_path()
249 if nitems > 4:
250 context.arc(self.column_radius + x, self.column_radius + y, self.column_radius, PI, 3 * PI /2)
251 context.arc(x + self.wcolumn - self.column_radius, self.column_radius + y, self.column_radius, 3 * PI / 2, 0)
252 context.rectangle(x, y + self.column_radius, self.wcolumn, height - self.column_radius)
253 else:
254 context.rectangle(x, y, self.wcolumn, height)
255 context.close_path()
256 context.fill()
257
258 def draw_month(self, context, x, height, date, edge=0):
259 """
260 Draws a line signifying the start of a month
261 """
262 context.set_source_rgba(*self.colors["stroke"])
263 context.set_line_width(self.stroke_width)
264 context.move_to(x+self.stroke_offset, 0)
265 context.line_to(x+self.stroke_offset, height - self.bottom_padding)
266 context.stroke()
267 month = calendar.month_name[date.month]
268 date = "<span color='%s'>%s %d</span>" % (self.colors["font_color"], month, date.year)
269 layout = self.create_pango_layout(date)
270 layout.set_markup(date)
271 layout.set_font_description(self.pangofont)
272 w, h = layout.get_pixel_size()
273 if edge:
274 if x + w > edge: x = edge - w - 5
275 self.window.draw_layout(self.gc, int(x + 3), int(height - self.bottom_padding/2 - h/2), layout)
276
277 def set_selected(self, dates):
278 if dates == self._selected:
279 return False
280 self._selected = dates
281 if dates:
282 date = dates[-1]
283 self.emit("selection-set", dates)
284 self.queue_draw()
285 return True
286
287 def get_selected(self):
288 """
289 returns a list of selected indices
290 """
291 return self._selected
292
293 def clear_selection(self):
294 """
295 clears the selected items
296 """
297 self._selected = []
298 self.queue_draw()
299
300 def set_highlighted(self, highlighted):
301 """
302 Sets the widgets which should be highlighted with an alternative color
303
304 :param highlighted: a list of indexes to be highlighted
305 """
306 if isinstance(highlighted, list):
307 self._highlighted = highlighted
308 else: raise TypeError("highlighted is not a list")
309 self.queue_draw()
310
311 def clear_highlighted(self):
312 """Clears the highlighted color"""
313 self._highlighted = []
314 self.queue_draw()
315
316 def set_single_day(self, choice):
317 """
318 Allows the cal to enter a mode where the trailing days are not selected but still kept
319 """
320 self._single_day_only = choice
321 self.queue_draw()
322
323 def get_store_index_from_cartesian(self, x, y):
324 """
325 Gets the datastore index from a x, y value
326 """
327 return int((x - self.start_x_padding) / self.xincrement)
328
329 def keyboard_interaction(self, widget, event):
330 if event.keyval in (gtk.keysyms.space, gtk.keysyms.Right, gtk.keysyms.Left, gtk.keysyms.BackSpace):
331 i = self.get_selected()
332 if isinstance(i, list) and len(i) > 0: i = i[-1]
333 if event.keyval in (gtk.keysyms.space, gtk.keysyms.Right):
334 i = i + datetime.timedelta(days=1)
335 elif event.keyval in (gtk.keysyms.Left, gtk.keysyms.BackSpace):
336 i = i + datetime.timedelta(days=-1)
337 if i < datetime.date.today() + datetime.timedelta(days=1):
338 self.change_location(i)
339
340 def mouse_motion_interaction(self, widget, event, *args, **kwargs):
341 """
342 Reacts to mouse moving (while pressed), and clicks
343 """
344 #if (event.state == gtk.gdk.BUTTON1_MASK and not self._disable_mouse_motion):
345 location = min((self.get_store_index_from_cartesian(event.x, event.y), len(self._store.days) - 1))
346 if location != self._last_location:
347 self.change_location(location)
348 self._last_location = location
349 #return True
350 return False
351
352 def mouse_press_interaction(self, widget, event, *args, **kwargs):
353 if (event.y > self.get_size_request()[1] - self.bottom_padding and
354 event.y < self.get_size_request()[1]):
355 return False
356 location = min((self.get_store_index_from_cartesian(event.x, event.y), len(self._store.days) - 1))
357 if location != self._last_location:
358 self.change_location(location)
359 self._last_location = location
360 return True
361
362 def mouse_scroll_interaction(self, widget, event):
363 date = self.get_selected()[-1]
364 i = self.get_store().dates.index(date)
365 if (event.direction in (gtk.gdk.SCROLL_UP, gtk.gdk.SCROLL_RIGHT)):
366 if i+1< len(self.get_store().days):
367 self.change_location(i+1)
368 elif (event.direction in (gtk.gdk.SCROLL_DOWN, gtk.gdk.SCROLL_LEFT)):
369 if 0 <= i-1:
370 self.change_location(i-1)
371
372 def change_location(self, location):
373 """
374 Handles click events
375 """
376 if isinstance(location, int):
377 if location < 0:
378 return False
379 store = self.get_store()
380 date = store.days[location].date
381 else: date = location
382 self.emit("column_clicked", date)
383 return True
384
385
386def _in_area(coord_x, coord_y, area):
387 """check if some given X,Y coordinates are within an area.
388 area is either None or a (top_left_x, top_left_y, width, height)-tuple"""
389 if area is None:
390 return False
391 area_x, area_y, area_width, area_height = area
392 return (area_x <= coord_x <= area_x + area_width) and \
393 (area_y <= coord_y <= area_y + area_height)
394
395
396def _in_area(coord_x, coord_y, area):
397 """check if some given X,Y coordinates are within an area.
398 area is either None or a (top_left_x, top_left_y, width, height)-tuple"""
399 if area is None:
400 return False
401 area_x, area_y, area_width, area_height = area
402 return (area_x <= coord_x <= area_x + area_width) and \
403 (area_y <= coord_y <= area_y + area_height)
404
405
406class TooltipEventBox(gtk.EventBox):
407 """
408 A event box housing the tool tip logic that can be used for a CairoHistogram.
409 Otherwise it interferes with the scrubbing mask code
410 """
411 _saved_tooltip_location = None
412 def __init__(self, histogram, container):
413 super(TooltipEventBox, self).__init__()
414 self.add(histogram)
415 self.histogram = histogram
416 self.container = container
417 self.set_property("has-tooltip", True)
418 #self.connect("query-tooltip", self.query_tooltip)
419
420 def query_tooltip(self, widget, x, y, keyboard_mode, tooltip):
421 if y < self.histogram.get_size_request()[1] - self.histogram.bottom_padding:
422 location = self.histogram.get_store_index_from_cartesian(x, y)
423 if location != self._saved_tooltip_location:
424 # don't show the previous tooltip if we moved to another
425 # location
426 self._saved_tooltip_location = location
427 return False
428 try:
429 timestamp, count = self.histogram.get_store()[location]
430 except IndexError:
431 # there is no bar for at this location
432 # don't show a tooltip
433 return False
434 date = datetime.date.fromtimestamp(timestamp).strftime("%A, %d %B, %Y")
435 tooltip.set_text("%s\n%i %s" % (date, count,
436 gettext.ngettext("item", "items", count)))
437 else:
438 return False
439 return True
440
441
442class JournalHistogram(CairoHistogram):
443 """
444 A subclass of CairoHistogram with theming to fit into Journal
445 """
446 padding = 2
447 column_radius = 1.3
448 top_padding = 6
449 bottom_padding = 29
450 wcolumn = 10
451 xincrement = wcolumn + padding
452 column_radius = 2
453 stroke_width = 2
454 stroke_offset = 1
455 font_size = 12
456 min_column_height = 2
457
458 def change_style(self, widget, *args, **kwargs):
459 self.colors = self.colors.copy()
460 self.colors["bg"] = get_gtk_rgba(self.style, "bg", 0)
461 self.colors["color"] = get_gtk_rgba(self.style, "base", 0)
462 self.colors["column_normal"] = get_gtk_rgba(self.style, "bg", 1)
463 self.colors["column_selected"] = get_gtk_rgba(self.style, "bg", 3)
464 self.colors["column_selected_alternative"] = get_gtk_rgba(self.style, "bg", 3, 0.7)
465 self.colors["column_alternative"] = get_gtk_rgba(self.style, "text", 2)
466 self.colors["stroke"] = get_gtk_rgba(self.style, "bg", 0)
467 self.colors["shadow"] = get_gtk_rgba(self.style, "bg", 0, 0.98)
468 self.font_size = self.style.font_desc.get_size()/1024
469 self.bottom_padding = self.font_size + 9 + widget.style.ythickness
470 self.gc = self.style.text_gc[gtk.STATE_NORMAL]
471 self.pangofont = pango.FontDescription(self.font_name + " %d" % self.font_size)
472 self.pangofont.set_weight(pango.WEIGHT_BOLD)
473
474
475class HistogramWidget(gtk.Viewport):
476 """
477 A container for a CairoHistogram which allows you to scroll
478 """
479 __gsignals__ = {
480 # the index of the first selected item in the datastore.
481 "date-changed" : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
482 (gobject.TYPE_PYOBJECT,)),
483 }
484
485 def __init__(self, histo_type=CairoHistogram, size = (600, 75)):
486 """
487 :param histo_type: a :class:`CairoHistogram <CairoHistogram>` or a derivative
488 """
489 super(HistogramWidget, self).__init__()
490 self.set_shadow_type(gtk.SHADOW_NONE)
491 self.histogram = histo_type()
492 self.eventbox = TooltipEventBox(self.histogram, self)
493 self.set_size_request(*size)
494 self.add(self.eventbox)
495 self.histogram.connect("column_clicked", self.date_changed)
496 self.histogram.connect("selection-set", self.scrubbing_fix)
497 self.histogram.queue_draw()
498 self.queue_draw()
499
500 def date_changed(self, widget, date):
501 self.emit("date-changed", date)
502
503 def set_store(self, store):
504 self.histogram.set_store(store)
505 self.scroll_to_end()
506
507 def set_dates(self, dates):
508 self.histogram.set_selected(dates)
509
510 def scroll_to_end(self, *args, **kwargs):
511 """
512 Scroll to the end of the drawing area's viewport
513 """
514 hadjustment = self.get_hadjustment()
515 hadjustment.set_value(1)
516 hadjustment.set_value(self.histogram.max_width - hadjustment.page_size)
517
518 def scrubbing_fix(self, widget, dates):
519 """
520 Allows scrubbing to scroll the scroll window
521 """
522 if not len(dates):
523 return
524 store = widget.get_store()
525 i = store.dates.index(dates[0])
526 hadjustment = self.get_hadjustment()
527 proposed_xa = ((i) * self.histogram.xincrement) + self.histogram.start_x_padding
528 proposed_xb = ((i + len(dates)) * self.histogram.xincrement) + self.histogram.start_x_padding
529 if proposed_xa < hadjustment.value:
530 hadjustment.set_value(proposed_xa)
531 elif proposed_xb > hadjustment.value + hadjustment.page_size:
532 hadjustment.set_value(proposed_xb - hadjustment.page_size)
0533
=== removed file 'src/histogram.py'
--- src/histogram.py 2010-04-15 14:34:37 +0000
+++ src/histogram.py 1970-01-01 00:00:00 +0000
@@ -1,625 +0,0 @@
1# -.- coding: utf-8 -.-
2#
3# GNOME Activity Journal
4#
5# Copyright © 2010 Randal Barlow <email.tehk@gmail.com>
6# Copyright © 2010 Markus Korn
7# Copyright © 2010 Siegfried Gevatter <siegfried@gevatter.com>
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <http://www.gnu.org/licenses/>.
21
22"""
23Takes a two dementional list of ints and turns it into a graph based on
24the first value a int date, and the second value the number of items on that date
25where items are
26datastore = []
27datastore.append(time, nitems)
28CairoHistogram.set_datastore(datastore)
29"""
30import datetime
31import cairo
32import calendar
33import gettext
34import gobject
35import gtk
36from math import pi as PI
37import pango
38from common import *
39
40
41def get_gc_from_colormap(widget, shade):
42 """
43 Gets a gtk.gdk.GC and modifies the color by shade
44 """
45 gc = widget.style.text_gc[gtk.STATE_INSENSITIVE]
46 if gc:
47 color = widget.style.text[4]
48 color = shade_gdk_color(color, shade)
49 gc.set_rgb_fg_color(color)
50 return gc
51
52
53class CairoHistogram(gtk.DrawingArea):
54 """
55 A histogram which is represented by a list of dates, and nitems.
56
57 There are a few maintenance issues due to the movement abilities. The widget
58 currently is able to capture motion events when the mouse is outside
59 the widget and the button is pressed if it was initially pressed inside
60 the widget. This event mask magic leaves a few flaws open.
61 """
62 _selected = (0,)
63 padding = 2
64 bottom_padding = 23
65 top_padding = 2
66 wcolumn = 12
67 xincrement = wcolumn + padding
68 start_x_padding = 2
69 max_width = xincrement
70 column_radius = 0
71 stroke_width = 1
72 stroke_offset = 0
73 min_column_height = 4
74 max_column_height = 101
75 gc = None
76 pangofont = None
77 _disable_mouse_motion = False
78 selected_range = 0
79 _highlighted = tuple()
80 _last_location = -1
81 _single_day_only = False
82 colors = {
83 "bg" : (1, 1, 1, 1),
84 "base" : (1, 1, 1, 1),
85 "column_normal" : (1, 1, 1, 1),
86 "column_selected" : (1, 1, 1, 1),
87 "column_alternative" : (1, 1, 1, 1),
88 "column_selected_alternative" : (1, 1, 1, 1),
89 "font_color" : "#ffffff",
90 "stroke" : (1, 1, 1, 0),
91 "shadow" : (1, 1, 1, 0),
92 }
93
94 # Today button stuff
95 _today_width = 0
96 _today_text = ""
97 _today_area = None
98 _today_hover = False
99
100 _datastore = None
101 __gsignals__ = {
102 # the index of the first selected item in the datastore.
103 "selection-set" : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
104 (gobject.TYPE_INT,gobject.TYPE_INT)),
105 "data-updated" : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,()),
106 "column_clicked" : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
107 (gobject.TYPE_INT,))
108 }
109 _connections = {"style-set": "change_style",
110 "expose_event": "_expose",
111 "button_press_event": "mouse_press_interaction",
112 "motion_notify_event": "mouse_motion_interaction",
113 "key_press_event": "keyboard_interaction",
114 "scroll-event" : "mouse_scroll_interaction",
115 "selection-set": "check_for_today",
116 }
117 _events = (gtk.gdk.KEY_PRESS_MASK | gtk.gdk.BUTTON_MOTION_MASK |
118 gtk.gdk.POINTER_MOTION_HINT_MASK | gtk.gdk.BUTTON_RELEASE_MASK |
119 gtk.gdk.BUTTON_PRESS_MASK)
120
121 def __init__(self, datastore = None, selected_range = 0):
122 """
123 :param datastore: The.CairoHistograms two dimensional list of dates and nitems
124 :param selected_range: the number of days displayed at once
125 """
126 super(CairoHistogram, self).__init__()
127 self.set_events(self._events)
128 self.set_flags(gtk.CAN_FOCUS)
129 for key, val in self._connections.iteritems():
130 self.connect(key, getattr(self, val))
131 self.font_name = self.style.font_desc.get_family()
132 self.set_datastore(datastore if datastore else [], draw = False)
133 self.selected_range = selected_range
134
135 def change_style(self, widget, old_style):
136 """
137 Sets the widgets style and coloring
138 """
139 self.colors = self.colors.copy()
140 self.colors["bg"] = get_gtk_rgba(self.style, "bg", 0)
141 self.colors["base"] = get_gtk_rgba(self.style, "base", 0)
142 self.colors["column_normal"] = get_gtk_rgba(self.style, "text", 4, 1.17)
143 self.colors["column_selected"] = get_gtk_rgba(self.style, "bg", 3)
144 color = self.style.bg[gtk.STATE_NORMAL]
145 fcolor = self.style.fg[gtk.STATE_NORMAL]
146 self.colors["font_color"] = combine_gdk_color(color, fcolor).to_string()
147
148 pal = get_gtk_rgba(self.style, "bg", 3, 1.2)
149 self.colors["column_alternative"] = (pal[2], pal[1], pal[0], 1)
150 self.colors["column_selected_alternative"] = get_gtk_rgba(self.style, "bg", 3, 0.6)
151 self.colors["stroke"] = get_gtk_rgba(self.style, "text", 4)
152 self.colors["shadow"] = get_gtk_rgba(self.style, "text", 4)
153 self.font_size = self.style.font_desc.get_size()/1024
154 self.pangofont = pango.FontDescription(self.font_name + " %d" % self.font_size)
155 self.pangofont.set_weight(pango.WEIGHT_BOLD)
156 self.bottom_padding = self.font_size + 9 + widget.style.ythickness
157 self.gc = get_gc_from_colormap(widget, 0.6)
158
159 def set_selected_range(self, selected_range):
160 """
161 Set the number of days to be colored as selected
162
163 :param selected_range: the range to be used when setting selected coloring
164 """
165 self.selected_range = selected_range
166 return True
167
168 def set_datastore(self, datastore, draw = True):
169 """
170 Sets the objects datastore attribute using a list
171
172 :param datastore: A list that is comprised of rows containing
173 a int time and a int nitems
174 """
175 if isinstance(datastore, list):
176 self._datastore = datastore
177 self.largest = 1
178 for date, nitems in self._datastore:
179 if nitems > self.largest: self.largest = nitems
180 if self.largest > self.max_column_height: self.largest = self.max_column_height
181 self.max_width = self.xincrement + (self.xincrement *len(datastore))
182 else:
183 raise TypeError("Datastore is not a <list>")
184 self.emit("data-updated")
185 self.set_selected(len(datastore) - self.selected_range)
186
187 def get_datastore(self):
188 return self._datastore
189
190 def prepend_data(self, newdatastore):
191 """
192 Adds the items of a new list before the items of the current datastore
193
194 :param newdatastore: the new list to be prepended
195
196 ## WARNING SELECTION WILL CHANGE WHEN DOING THIS TO BE FIXED ##
197 """
198 selected = self.get_selected()[-1]
199 self._datastore = newdatastore + self._datastore
200 self.queue_draw()
201 self.set_selected(len(newdatastore) + selected)
202
203 def _expose(self, widget, event):
204 """
205 The major drawing method that the expose event calls directly
206 """
207 widget.style.set_background(widget.window, gtk.STATE_NORMAL)
208 context = widget.window.cairo_create()
209 self.expose(widget, event, context)
210 if len(self._today_text):
211 self.draw_today(widget, event, context)
212
213 def expose(self, widget, event, context):
214 """
215 The minor drawing method
216
217 :param event: a gtk event with x and y values
218 :param context: This drawingarea's cairo context from the expose event
219 """
220 if not self.pangofont:
221 self.pangofont = pango.FontDescription(self.font_name + " %d" % self.font_size)
222 self.pangofont.set_weight(pango.WEIGHT_BOLD)
223 if not self.gc:
224 self.gc = get_gc_from_colormap(widget, 0.6)
225 context.set_source_rgba(*self.colors["base"])
226 context.set_operator(cairo.OPERATOR_SOURCE)
227 #context.paint()
228 context.rectangle(event.area.x, event.area.y, event.area.width, event.area.height)
229 context.clip()
230 #context.set_source_rgba(*self.colors["bg"])
231 context.rectangle(event.area.x, event.area.y, event.area.width, event.area.height - self.bottom_padding)
232 context.fill()
233 self.draw_columns_from_datastore(context, event, self.get_selected())
234 context.set_line_width(1)
235 if type(self) == CairoHistogram:
236 widget.style.paint_shadow(widget.window, gtk.STATE_NORMAL, gtk.SHADOW_IN,
237 event.area, widget, "treeview", event.area.x, event.area.y,
238 event.area.width, event.area.height - self.bottom_padding)
239 if self.is_focus():
240 widget.style.paint_focus(widget.window, gtk.STATE_NORMAL, event.area, widget, None, event.area.x, event.area.y,
241 event.area.width, event.area.height - self.bottom_padding)
242
243 def draw_today(self, widget, event, context):
244 """
245 """
246 layout = widget.create_pango_layout(self._today_text)
247 pangofont = pango.FontDescription(widget.font_name + " %d" % (widget.font_size - 1))
248 if not widget.gc:
249 widget.gc = get_gc_from_colormap(widget, 0.6)
250 layout.set_font_description(pangofont)
251 w, h = layout.get_pixel_size()
252 self._today_width = w + 10
253 self._today_area = (
254 int(event.area.x + event.area.width - self._today_width),
255 int(event.area.height - widget.bottom_padding + 2),
256 self._today_width,
257 widget.bottom_padding - 2)
258 state = gtk.STATE_PRELIGHT
259 shadow = gtk.SHADOW_OUT
260 widget.style.paint_box(
261 widget.window, state, shadow, event.area, widget, "button", *self._today_area)
262 widget.window.draw_layout(
263 widget.gc, int(event.area.x + event.area.width - w -5),
264 int(event.area.height - widget.bottom_padding/2 - h/2), layout)
265
266 def draw_columns_from_datastore(self, context, event, selected):
267 """
268 Draws columns from a datastore
269
270 :param context: This drawingarea's cairo context from the expose event
271 :param event: a gtk event with x and y values
272 :param selected: a list of the selected columns
273 """
274 x = self.start_x_padding
275 months_positions = []
276 for i, (date, nitems) in enumerate(self.get_datastore()):
277 if datetime.date.fromtimestamp(date).day == 1:
278 months_positions += [(date, x)]
279 if len(self._highlighted) > 0 and i >= self._highlighted[0] and i <= self._highlighted[-1] and i in self._highlighted:
280 color = self.colors["column_selected_alternative"] if i in selected else self.colors["column_alternative"]
281 elif not selected:
282 color = self.colors["column_normal"]
283 elif self._single_day_only and i != selected[-1]:
284 color = self.colors["column_normal"]
285 elif i >= selected[0] and i <= selected[-1] and i in selected:
286 color = self.colors["column_selected"]
287 else:
288 color = self.colors["column_normal"]
289 self.draw_column(context, x, event.area.height, nitems, color)
290 x += self.xincrement
291 if x > event.area.width: # Check for resize
292 self.set_size_request(x+self.xincrement, event.area.height)
293 for date, xpos in months_positions:
294 edge = 0
295 if (date, xpos) == months_positions[-1]:
296 edge = len(self._datastore)*self.xincrement
297 self.draw_month(context, xpos - self.padding, event.area.height, date, edge)
298 self.max_width = x # remove me
299
300 def draw_column(self, context, x, maxheight, nitems, color):
301 """
302 Draws a columns at x with height based on nitems, and maxheight
303
304 :param context: The drawingarea's cairo context from the expose event
305 :param x: The current position in the image
306 :param maxheight: The event areas height
307 :param nitems: The number of items in the column to be drawn
308 :param color: A RGBA tuple Example: (0.3, 0.4, 0.8, 1)
309 """
310 if nitems < 2:
311 nitems = 2
312 elif nitems > self.max_column_height:
313 nitems = self.max_column_height
314 maxheight = maxheight - self.bottom_padding - 2
315 #height = int((maxheight-self.top_padding-2) * (self.largest*math.log(nitems)/math.log(self.largest))/100)
316 height = int(((float(nitems)/self.largest)*(maxheight-2))) - self.top_padding
317 #height = min(int((maxheight*self.largest/100) * (1 - math.e**(-0.025*nitems))), maxheight)
318 if height < self.min_column_height:
319 height = self.min_column_height
320 y = maxheight - height
321 context.set_source_rgba(*color)
322 context.move_to(x + self.column_radius, y)
323 context.new_sub_path()
324 if nitems > 4:
325 context.arc(self.column_radius + x, self.column_radius + y, self.column_radius, PI, 3 * PI /2)
326 context.arc(x + self.wcolumn - self.column_radius, self.column_radius + y, self.column_radius, 3 * PI / 2, 0)
327 context.rectangle(x, y + self.column_radius, self.wcolumn, height - self.column_radius)
328 else:
329 context.rectangle(x, y, self.wcolumn, height)
330 context.close_path()
331 context.fill()
332
333 def draw_month(self, context, x, height, date, edge=0):
334 """
335 Draws a line signifying the start of a month
336 """
337 context.set_source_rgba(*self.colors["stroke"])
338 context.set_line_width(self.stroke_width)
339 context.move_to(x+self.stroke_offset, 0)
340 context.line_to(x+self.stroke_offset, height - self.bottom_padding)
341 context.stroke()
342 date = datetime.date.fromtimestamp(date)
343 month = calendar.month_name[date.month]
344 date = "<span color='%s'>%s %d</span>" % (self.colors["font_color"], month, date.year)
345 layout = self.create_pango_layout(date)
346 layout.set_markup(date)
347 layout.set_font_description(self.pangofont)
348 w, h = layout.get_pixel_size()
349 if edge:
350 if x + w > edge: x = edge - w - 5
351 self.window.draw_layout(self.gc, int(x + 3), int(height - self.bottom_padding/2 - h/2), layout)
352
353 def set_selected(self, i):
354 """
355 Set the selected items using a int or a list of the selections
356 If you pass this method a int it will select the index + selected_range
357
358 Emits:
359 self._selected[0] and self._selected[-1]
360
361 :param i: a list or a int where the int will select i + selected_range
362 """
363 if len(self._selected):
364 if i == self._selected[0]:
365 return False
366 if isinstance(i, int):
367 self._selected = range(i, i + self.selected_range)
368 self.emit("selection-set", max(i, 0), max(i + self.selected_range - 1, 0))
369 else: self._selected = (-1,)
370 self.queue_draw()
371 return True
372
373 def get_selected(self):
374 """
375 returns a list of selected indices
376 """
377 return self._selected
378
379 def clear_selection(self):
380 """
381 clears the selected items
382 """
383 self._selected = range(len(self._datastore))[-self.selected_range:]
384 self.queue_draw()
385
386 def set_highlighted(self, highlighted):
387 """
388 Sets the widgets which should be highlighted with an alternative color
389
390 :param highlighted: a list of indexes to be highlighted
391 """
392 if isinstance(highlighted, list):
393 self._highlighted = highlighted
394 else: raise TypeError("highlighted is not a list")
395 self.queue_draw()
396
397 def clear_highlighted(self):
398 """Clears the highlighted color"""
399 self._highlighted = []
400 self.queue_draw()
401
402 def set_single_day(self, choice):
403 """
404 Allows the cal to enter a mode where the trailing days are not selected but still kept
405 """
406 self._single_day_only = choice
407 self.queue_draw()
408
409 def get_datastore_index_from_cartesian(self, x, y):
410 """
411 Gets the datastore index from a x, y value
412 """
413 return int((x - self.start_x_padding) / self.xincrement)
414
415 def keyboard_interaction(self, widget, event):
416 if event.keyval in (gtk.keysyms.space, gtk.keysyms.Right, gtk.keysyms.Left, gtk.keysyms.BackSpace):
417 i = self.get_selected()
418 if isinstance(i, list) and len(i) > 0: i = i[-1]
419 if event.keyval in (gtk.keysyms.space, gtk.keysyms.Right):
420 i += 1
421 elif event.keyval in (gtk.keysyms.Left, gtk.keysyms.BackSpace):
422 i -= 1
423 if i < len(self.get_datastore()):
424 self.change_location(i)
425
426 def mouse_motion_interaction(self, widget, event, *args, **kwargs):
427 """
428 Reacts to mouse moving (while pressed), and clicks
429 """
430 #if (event.state == gtk.gdk.BUTTON1_MASK and not self._disable_mouse_motion):
431 location = min((self.get_datastore_index_from_cartesian(event.x, event.y), len(self._datastore) - 1))
432 if location != self._last_location:
433 self.change_location(location)
434 self._last_location = location
435 #return True
436 return False
437
438 def mouse_press_interaction(self, widget, event, *args, **kwargs):
439 if (event.y > self.get_size_request()[1] - self.bottom_padding and
440 event.y < self.get_size_request()[1]):
441 return False
442 location = min((self.get_datastore_index_from_cartesian(event.x, event.y), len(self._datastore) - 1))
443 if location != self._last_location:
444 self.change_location(location)
445 self._last_location = location
446 return True
447
448 def mouse_scroll_interaction(self, widget, event):
449 i = self.get_selected()[-1]
450 if (event.direction in (gtk.gdk.SCROLL_UP, gtk.gdk.SCROLL_RIGHT)):
451 if i+1< len(self.get_datastore()):
452 self.change_location(i+1)
453 elif (event.direction in (gtk.gdk.SCROLL_DOWN, gtk.gdk.SCROLL_LEFT)):
454 if 0 <= i-1:
455 self.change_location(i-1)
456
457 def change_location(self, location):
458 """
459 Handles click events
460 """
461 if location < 0:
462 return False
463 self.set_selected(max(location - self.selected_range + 1, 0))
464 self.emit("column_clicked", location)
465 return True
466
467 # Today stuff
468 def check_for_today(self, widget, i, ii):
469 """
470 Changes today to a empty string if the selected item is not today
471 """
472 if ii == len(self.get_datastore())-1:
473 self._today_text = ""
474 self._today_area = None
475 elif len(self._today_text) == 0:
476 self._today_text = _("Today") + " »"
477 self.queue_draw()
478 return True
479
480
481def _in_area(coord_x, coord_y, area):
482 """check if some given X,Y coordinates are within an area.
483 area is either None or a (top_left_x, top_left_y, width, height)-tuple"""
484 if area is None:
485 return False
486 area_x, area_y, area_width, area_height = area
487 return (area_x <= coord_x <= area_x + area_width) and \
488 (area_y <= coord_y <= area_y + area_height)
489
490
491def _in_area(coord_x, coord_y, area):
492 """check if some given X,Y coordinates are within an area.
493 area is either None or a (top_left_x, top_left_y, width, height)-tuple"""
494 if area is None:
495 return False
496 area_x, area_y, area_width, area_height = area
497 return (area_x <= coord_x <= area_x + area_width) and \
498 (area_y <= coord_y <= area_y + area_height)
499
500
501class TooltipEventBox(gtk.EventBox):
502 """
503 A event box housing the tool tip logic that can be used for a CairoHistogram.
504 Otherwise it interferes with the scrubbing mask code
505 """
506 _saved_tooltip_location = None
507 def __init__(self, histogram, container):
508 super(TooltipEventBox, self).__init__()
509 self.add(histogram)
510 self.histogram = histogram
511 self.container = container
512 self.set_property("has-tooltip", True)
513 self.connect("query-tooltip", self.query_tooltip)
514
515 def query_tooltip(self, widget, x, y, keyboard_mode, tooltip):
516 if y < self.histogram.get_size_request()[1] - self.histogram.bottom_padding:
517 location = self.histogram.get_datastore_index_from_cartesian(x, y)
518 if location != self._saved_tooltip_location:
519 # don't show the previous tooltip if we moved to another
520 # location
521 self._saved_tooltip_location = location
522 return False
523 try:
524 timestamp, count = self.histogram.get_datastore()[location]
525 except IndexError:
526 # there is no bar for at this location
527 # don't show a tooltip
528 return False
529 date = datetime.date.fromtimestamp(timestamp).strftime("%A, %d %B, %Y")
530 tooltip.set_text("%s\n%i %s" % (date, count,
531 gettext.ngettext("item", "items", count)))
532 elif self.container.histogram._today_text and _in_area(x, y, self.container.histogram._today_area):
533 tooltip.set_text(_("Click today to return to today"))
534 else:
535 return False
536 return True
537
538
539class JournalHistogram(CairoHistogram):
540 """
541 A subclass of CairoHistogram with theming to fit into Journal
542 """
543 padding = 2
544 column_radius = 1.3
545 top_padding = 6
546 bottom_padding = 29
547 wcolumn = 10
548 xincrement = wcolumn + padding
549 column_radius = 2
550 stroke_width = 2
551 stroke_offset = 1
552 font_size = 12
553 min_column_height = 2
554
555 def change_style(self, widget, *args, **kwargs):
556 self.colors = self.colors.copy()
557 self.colors["bg"] = get_gtk_rgba(self.style, "bg", 0)
558 self.colors["color"] = get_gtk_rgba(self.style, "base", 0)
559 self.colors["column_normal"] = get_gtk_rgba(self.style, "bg", 1)
560 self.colors["column_selected"] = get_gtk_rgba(self.style, "bg", 3)
561 self.colors["column_selected_alternative"] = get_gtk_rgba(self.style, "bg", 3, 0.7)
562 self.colors["column_alternative"] = get_gtk_rgba(self.style, "text", 2)
563 self.colors["stroke"] = get_gtk_rgba(self.style, "bg", 0)
564 self.colors["shadow"] = get_gtk_rgba(self.style, "bg", 0, 0.98)
565 self.font_size = self.style.font_desc.get_size()/1024
566 self.bottom_padding = self.font_size + 9 + widget.style.ythickness
567 self.gc = self.style.text_gc[gtk.STATE_NORMAL]
568 self.pangofont = pango.FontDescription(self.font_name + " %d" % self.font_size)
569 self.pangofont.set_weight(pango.WEIGHT_BOLD)
570
571
572class HistogramWidget(gtk.Viewport):
573 """
574 A container for a CairoHistogram which allows you to scroll
575 """
576
577
578 def __init__(self, histo_type, size = (600, 75)):
579 """
580 :param histo_type: a :class:`CairoHistogram <CairoHistogram>` or a derivative
581 """
582 super(HistogramWidget, self).__init__()
583 self.set_shadow_type(gtk.SHADOW_NONE)
584 self.histogram = histo_type()
585 self.eventbox = TooltipEventBox(self.histogram, self)
586 self.set_size_request(*size)
587 self.add(self.eventbox)
588 self.histogram.connect("button_press_event", self.footer_clicked)
589 self.histogram.connect("selection-set", self.scrubbing_fix)
590 self.histogram.queue_draw()
591 self.queue_draw()
592
593 def footer_clicked(self, widget, event):
594 """
595 Handles all rejected clicks from bellow the histogram internal view and
596 checks to see if they were inside of the today text
597 """
598 hadjustment = self.get_hadjustment()
599 # Check for today button click
600 if (widget._today_text and event.x > hadjustment.value + hadjustment.page_size - widget._today_width):
601 self.histogram.change_location(len(self.histogram.get_datastore()) - 1)
602 return True
603 else:
604 pass # Drag here
605 return False
606
607 def scroll_to_end(self, *args, **kwargs):
608 """
609 Scroll to the end of the drawing area's viewport
610 """
611 hadjustment = self.get_hadjustment()
612 hadjustment.set_value(1)
613 hadjustment.set_value(self.histogram.max_width - hadjustment.page_size)
614
615 def scrubbing_fix(self, widget, i, ii):
616 """
617 Allows scrubbing to scroll the scroll window
618 """
619 hadjustment = self.get_hadjustment()
620 proposed_xa = ((i) * self.histogram.xincrement) + self.histogram.start_x_padding
621 proposed_xb = ((i + self.histogram.selected_range) * self.histogram.xincrement) + self.histogram.start_x_padding
622 if proposed_xa < hadjustment.value:
623 hadjustment.set_value(proposed_xa)
624 elif proposed_xb > hadjustment.value + hadjustment.page_size:
625 hadjustment.set_value(proposed_xb - hadjustment.page_size)
6260
=== added file 'src/infopane.py'
--- src/infopane.py 1970-01-01 00:00:00 +0000
+++ src/infopane.py 2010-05-04 05:28:16 +0000
@@ -0,0 +1,577 @@
1# -.- coding: utf-8 -.-
2#
3# Filename
4#
5# Copyright © 2010 Randal Barlow
6# Copyright © 2010 Markus Korn <thekorn@gmx.de>
7#
8# This program is free software: you can redistribute it and/or modify
9# it under the terms of the GNU General Public License as published by
10# the Free Software Foundation, either version 3 of the License, or
11# (at your option) any later version.
12#
13# This program is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16# GNU General Public License for more details.
17#
18# You should have received a copy of the GNU General Public License
19# along with this program. If not, see <http://www.gnu.org/licenses/>.
20
21# Purpose:
22
23import gobject
24import gtk
25import mimetypes
26import os
27import pango
28try: import gst
29except ImportError:
30 gst = None
31try: import gtksourceview2
32except ImportError: gtksourceview2 = None
33import threading
34
35from zeitgeist.client import ZeitgeistClient
36from zeitgeist.datamodel import Event, Subject, Interpretation, Manifestation, \
37 ResultType, TimeRange
38
39import content_objects
40from common import *
41from gio_file import GioFile
42import supporting_widgets
43
44
45GENERIC_DISPLAY_NAME = "other"
46
47MIMETYPEMAP = {
48 GENERIC_DISPLAY_NAME : ("image", None),
49 #"multimedia" : ("video", "audio"),
50 #"text" : ("text",),
51}
52
53CLIENT = ZeitgeistClient()
54
55def get_related_events_for_uri(uri, callback):
56 """
57 :param uri: A uri for which to request related uris using zetigeist
58 :param callback: this callback is called once the events are retrieved for
59 the uris. It is called with a list of events.
60 """
61 def _event_request_handler(uris):
62 """
63 :param uris: a list of uris which are related to the windows current uri
64 Seif look here
65 """
66 templates = []
67 if len(uris) > 0:
68 for i, uri in enumerate(uris):
69 templates += [
70 Event.new_for_values(interpretation=Interpretation.VISIT_EVENT.uri, subject_uri=uri),
71 Event.new_for_values(interpretation=Interpretation.MODIFY_EVENT.uri, subject_uri=uri),
72 Event.new_for_values(interpretation=Interpretation.CREATE_EVENT.uri, subject_uri=uri),
73 Event.new_for_values(interpretation=Interpretation.OPEN_EVENT.uri, subject_uri=uri)
74 ]
75 CLIENT.find_events_for_templates(templates, callback,
76 [0, time.time()*1000], num_events=50000,
77 result_type=ResultType.MostRecentSubjects)
78
79 end = time.time() * 1000
80 start = end - (86400*30*1000)
81 CLIENT.find_related_uris_for_uris([uri], _event_request_handler)
82
83
84def get_media_type(gfile):
85 uri = gfile.uri
86 if not uri.startswith("file://") or not gfile:
87 return GENERIC_DISPLAY_NAME
88 majortype = gfile.mime_type.split("/")[0]
89 for key, mimes in MIMETYPEMAP.iteritems():
90 if majortype in mimes:
91 return key
92 #if isinstance(gfile, GioFile):
93 # if "text-x-generic" in gfile.icon_names or "text-x-script" in gfile.icon_names:
94 # return "text"
95 return GENERIC_DISPLAY_NAME
96
97
98class ContentDisplay(object):
99 """
100 The abstract base class for content displays
101 """
102 def set_content_object(self, obj):
103 """
104 :param obj a content object which the Content Display displays
105 """
106 pass
107
108 def set_inactive(self):
109 """
110 This method performs clean when the displays are swapped
111 """
112 pass
113
114
115class ScrolledDisplay(gtk.ScrolledWindow):
116 """
117 A scrolled window container that acts as a proxy for a child
118 use type to make wrapers for your type
119 """
120 child_type = gtk.Widget
121 def __init__(self):
122 super(ScrolledDisplay, self).__init__()
123 self._child_obj = self.child_type()
124 self.add(self._child_obj)
125 self.set_shadow_type(gtk.SHADOW_IN)
126 self.set_size_request(-1, 200)
127
128 def set_content_object(self, obj): self._child_obj.set_content_object(obj)
129 def set_inactive(self): self._child_obj.set_inactive()
130
131
132class TextDisplay(gtksourceview2.View if gtksourceview2
133 else gtk.TextView, ContentDisplay):
134 """
135 A text preview display which uses a sourceview or a textview if sourceview
136 modules are not found
137 """
138 def __init__(self):
139 """"""
140 super(TextDisplay, self).__init__()
141 self.textbuffer = (gtksourceview2.Buffer() if gtksourceview2
142 else gtk.TextBuffer())
143 self.set_buffer(self.textbuffer)
144 self.set_editable(False)
145 font = pango.FontDescription()
146 font.set_family("Monospace")
147 self.modify_font(font)
148 if gtksourceview2:
149 self.manager = gtksourceview2.LanguageManager()
150 self.textbuffer.set_highlight_syntax(True)
151
152 def get_language_from_mime_type(self, mime):
153 for id_ in self.manager.get_language_ids():
154 temp_language = self.manager.get_language(id_)
155 if mime in temp_language.get_mime_types():
156 return temp_language
157 return None
158
159 def set_content_object(self, obj):
160 if obj:
161 content = obj.get_content()
162 self.textbuffer.set_text(content)
163 if gtksourceview2:
164 lang = self.get_language_from_mime_type(obj.mime_type)
165 self.textbuffer.set_language(lang)
166
167
168class ImageDisplay(gtk.Image, ContentDisplay):
169 """
170 A display based on GtkImage to display a uri's thumb or icon using GioFile
171 """
172 def set_content_object(self, obj):
173 if obj:
174 if isinstance(obj, GioFile) and obj.has_preview():
175 pixbuf = obj.get_thumbnail(size=SIZE_NORMAL, border=3)
176 else:
177 pixbuf = obj.get_icon(size=128)
178 self.set_from_pixbuf(pixbuf)
179
180
181class MultimediaDisplay(gtk.VBox, ContentDisplay):
182 """
183 a display which words for video and audio using gstreamer
184 """
185 def __init__(self):
186 super(MultimediaDisplay, self).__init__()
187 self.playing = False
188 self.mediascreen = gtk.DrawingArea()
189 self.player = gst.element_factory_make("playbin", "player")
190 bus = self.player.get_bus()
191 bus.add_signal_watch()
192 bus.enable_sync_message_emission()
193 bus.connect("message", self.on_message)
194 bus.connect("sync-message::element", self.on_sync_message)
195 buttonbox = gtk.HBox()
196 self.playbutton = gtk.Button()
197 buttonbox.pack_start(self.playbutton, True, False)
198 self.playbutton.gtkimage = gtk.Image()
199 self.playbutton.add(self.playbutton.gtkimage)
200 self.playbutton.gtkimage.set_from_stock(gtk.STOCK_MEDIA_PAUSE, 2)
201 self.pack_start(self.mediascreen, True, True, 10)
202 self.pack_end(buttonbox, False, False)
203 self.playbutton.connect("clicked", self.on_play_click)
204 self.playbutton.set_relief(gtk.RELIEF_NONE)
205 self.connect("hide", self._handle_hide)
206
207 def _handle_hide(self, widget):
208 self.player.set_state(gst.STATE_NULL)
209
210 def set_playing(self):
211 """
212 Set MultimediaDisplay.player's state to playing
213 """
214 self.player.set_state(gst.STATE_PLAYING)
215 self.playbutton.gtkimage.set_from_stock(gtk.STOCK_MEDIA_PAUSE, 2)
216 self.playing = True
217
218 def set_paused(self):
219 """
220 Set MultimediaDisplay.player's state to paused
221 """
222 self.player.set_state(gst.STATE_PAUSED)
223 self.playbutton.gtkimage.set_from_stock(gtk.STOCK_MEDIA_PLAY, 2)
224 self.playing = False
225
226
227 def set_content_object(self, obj):
228 if isinstance(obj, GioFile):
229 self.player.set_state(gst.STATE_NULL)
230 self.player.set_property("uri", obj.uri)
231 self.set_playing()
232
233 def set_inactive(self):
234 self.player.set_state(gst.STATE_NULL)
235 self.playing = False
236
237 def on_play_click(self, widget):
238 if self.playing:
239 return self.set_paused()
240 return self.set_playing()
241
242 def on_sync_message(self, bus, message):
243 if message.structure is None:
244 return
245 message_name = message.structure.get_name()
246 if message_name == "prepare-xwindow-id":
247 imagesink = message.src
248 imagesink.set_property("force-aspect-ratio", True)
249 gtk.gdk.threads_enter()
250 try:
251 self.show_all()
252 imagesink.set_xwindow_id(self.mediascreen.window.xid)
253 finally:
254 gtk.gdk.threads_leave()
255
256 def on_message(self, bus, message):
257 t = message.type
258 if t == gst.MESSAGE_EOS:
259 self.player.set_state(gst.STATE_NULL)
260 elif t == gst.MESSAGE_ERROR:
261 self.player.set_state(gst.STATE_NULL)
262 err, debug = message.parse_error()
263 print "Error: %s" % err, debug
264
265
266class EventDataPane(gtk.Table):
267 x = 2
268 y = 8
269
270 column_names = (
271 _("Actor"), # 0
272 _("Time"), # 1
273 _(""), # 2
274 _("Interpretation"), # 3
275 _("Subject Interpretation"), # 4
276 _("Manifestation"),
277 )
278
279
280 def __init__(self):
281 super(EventDataPane, self).__init__(self.x, self.y)
282 self.set_col_spacings(4)
283 self.set_row_spacings(4)
284 self.labels = []
285 for i, name in enumerate(self.column_names):
286 #for i in xrange(len(self.column_names)):
287 namelabel = gtk.Label()
288 if name:
289 namelabel.set_markup("<b>" + name + ":</b>")
290 namelabel.set_alignment(0, 0)
291 self.attach(namelabel, 0, 1, i, i+1)
292 label = gtk.Label()
293 label.set_alignment(0, 0)
294 self.attach(label, 1, 2, i, i+1)
295 self.labels.append(label)
296
297 def set_content_object(self, obj):
298 event = obj.event
299 # Actor
300 desktop_file = obj.get_actor_desktop_file()
301 if desktop_file:
302 actor = desktop_file.getName()
303 else: actor = event.actor
304 self.labels[0].set_text(actor)
305 # Time
306 local_t = time.localtime(int(event.timestamp)/1000)
307 time_str = time.strftime("%b %d %Y %H:%M:%S", local_t)
308 self.labels[1].set_text(time_str)
309 #self.labels[2].set_text(event.subjects[0].uri)
310 # Interpetation
311 try: interpretation_name = Interpretation[event.interpretation].display_name
312 except KeyError: interpretation_name = ""
313 self.labels[3].set_text(interpretation_name)
314 # Subject Interpetation
315 try: subject_interpretation_name = Interpretation[event.subjects[0].interpretation].display_name
316 except KeyError: subject_interpretation_name = ""
317 self.labels[4].set_text(subject_interpretation_name)
318 # Manifestation
319 try: manifestation_name = Manifestation[event.manifestation].display_name
320 except KeyError: manifestation_name = ""
321 self.labels[5].set_text(manifestation_name)
322
323
324class InformationPane(gtk.VBox):
325 """
326 . . . . . . . .
327 . .
328 . Info .
329 . .
330 . .
331 . . . . . . . .
332
333 Holds widgets which display information about a uri
334 """
335 displays = {
336 GENERIC_DISPLAY_NAME : ImageDisplay,
337 "multimedia" : MultimediaDisplay if gst else ImageDisplay,
338 "text" : type("TextScrolledWindow", (ScrolledDisplay,),
339 {"child_type" : TextDisplay}),
340 }
341
342 obj = None
343
344 def __init__(self):
345 super(InformationPane, self).__init__()
346 vbox = gtk.VBox()
347 buttonhbox = gtk.HBox()
348 self.box = gtk.Frame()
349 self.label = gtk.Label()
350 self.pathlabel = gtk.Label()
351 labelvbox = gtk.VBox()
352 labelvbox.pack_start(self.label)
353 labelvbox.pack_end(self.pathlabel)
354 self.openbutton = gtk.Button(stock=gtk.STOCK_OPEN)
355 self.displays = self.displays.copy()
356 #self.set_shadow_type(gtk.SHADOW_NONE)
357 #self.set_label_widget(labelvbox)
358 self.pack_start(labelvbox)
359 self.box.set_shadow_type(gtk.SHADOW_NONE)
360 buttonhbox.pack_end(self.openbutton, False, False, 5)
361 buttonhbox.set_border_width(5)
362 vbox.pack_start(self.box, True, True)
363 vbox.pack_end(buttonhbox, False, False)
364 #self.set_label_align(0.5, 0.5)
365 #self.label.set_ellipsize(pango.ELLIPSIZE_MIDDLE)
366 #self.pathlabel.set_size_request(100, -1)
367 #self.pathlabel.set_size_request(300, -1)
368 self.pathlabel.set_ellipsize(pango.ELLIPSIZE_MIDDLE)
369 def _launch(w):
370 self.obj.launch()
371 self.openbutton.connect("clicked", _launch)
372
373 #self.datapane = EventDataPane()
374 #vbox.pack_end(self.datapane, False, False)
375 self.add(vbox)
376 self.show_all()
377
378 def set_displaytype(self, obj):
379 """
380 Determines the ContentDisplay to use for a given uri
381 """
382 media_type = get_media_type(obj)
383 display_widget = self.displays[media_type]
384 if isinstance(display_widget, type):
385 display_widget = self.displays[media_type] = display_widget()
386 if display_widget.parent != self.box:
387 child = self.box.get_child()
388 if child:
389 self.box.remove(child)
390 child.set_inactive()
391 self.box.add(display_widget)
392 display_widget.set_content_object(obj)
393 self.show_all()
394
395 def set_content_object(self, obj):
396 self.obj = obj
397 self.set_displaytype(obj)
398 self.label.set_markup("<span size='12336'>" + obj.text.replace("&", "&amp;") + "</span>")
399 self.pathlabel.set_markup("<span color='#979797'>" + obj.uri + "</span>")
400 #self.datapane.set_content_object(obj)
401
402 def set_inactive(self):
403 display = self.box.get_child()
404 if display: display.set_inactive()
405
406
407class RelatedPane(gtk.TreeView):
408 """
409 . . .
410 . .
411 . . <--- Related files
412 . .
413 . .
414 . . .
415
416 Displays related events using a widget based on gtk.TreeView
417 """
418 def __init__(self):
419 super(RelatedPane, self).__init__()
420 self.popupmenu = supporting_widgets.ContextMenu
421 self.connect("button-press-event", self.on_button_press)
422 self.connect("row-activated", self.row_activated)
423 pcolumn = gtk.TreeViewColumn(_("Related Items"))
424 pixbuf_render = gtk.CellRendererPixbuf()
425 pcolumn.pack_start(pixbuf_render, False)
426 pcolumn.set_cell_data_func(pixbuf_render, self.celldatamethod, "pixbuf")
427 text_render = gtk.CellRendererText()
428 text_render.set_property("ellipsize", pango.ELLIPSIZE_MIDDLE)
429 pcolumn.pack_end(text_render, True)
430 pcolumn.set_cell_data_func(text_render, self.celldatamethod, "text")
431 self.append_column(pcolumn)
432 #self.set_headers_visible(False)
433
434 def celldatamethod(self, column, cell, model, iter_, user_data):
435 if model:
436 obj = model.get_value(iter_, 0)
437 if user_data == "text":
438 cell.set_property("text", obj.text.replace("&", "&amp;"))
439 elif user_data == "pixbuf":
440 cell.set_property("pixbuf", obj.icon)
441
442 def _set_model_in_thread(self, events):
443 """
444 A threaded which generates pixbufs and emblems for a list of events.
445 It takes those properties and appends them to the view's model
446 """
447 lock = threading.Lock()
448 self.active_list = []
449 liststore = gtk.ListStore(gobject.TYPE_PYOBJECT)
450 gtk.gdk.threads_enter()
451 self.set_model(liststore)
452 gtk.gdk.threads_leave()
453 for event in events:
454 obj = content_objects.choose_content_object(event)
455 if not obj: continue
456 gtk.gdk.threads_enter()
457 lock.acquire()
458 self.active_list.append(False)
459 liststore.append((obj,))
460 lock.release()
The diff has been truncated for viewing.