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