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