1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
|
#! /usr/bin/python
import sys
from gi.repository import GLib, GObject, Gio
from gi.repository import Dee
# FIXME: Some weird bug in Dee or PyGI makes Dee fail unless we probe
# it *before* we import the Unity module... ?!
_m = dir(Dee.SequenceModel)
from gi.repository import Unity
import datetime, os
import vobject
from time import mktime
# I'd love to be able to use gobject introspection of Evolution rather than
# python-evolution and it would be much faster and more comprehensive,
# but alas, there appear to be a few bits missing in the gobject introspection
# implementation.
import evolution
#from gi.repository import ECalendar
MAX = 25
#
# The primary bus name we grab *must* match what we specify in our .lens file
#
BUS_NAME = "net.launchpad.Lens.calendar"
# These category ids must match the order in which we add them to the lens
MISC = 0
UPCOMING = 1
PENDING = 2
RECENT = 3
COMPLETE = 4
class Daemon:
def __init__ (self):
# Set python's locale to the system locale (so it displays dates in the correct format)
import locale
locale.setlocale(locale.LC_ALL, '')
# The path for the Lens *must* also match the one in our .lens file
self.lens = Unity.Lens.new ("/net/launchpad/lens/calendar", "calendar")
self.scope = Unity.Scope.new ("/net/launchpad/lens/calendar/scope/calendar")
self.lens.props.search_hint = "Search Calendar Events"
self.lens.props.visible = True;
self.lens.props.search_in_global = False;
self.scope.props.search_in_global = False;
now = datetime.datetime.now()
icon_hint = "/usr/lib/calendar-lens/icons/%s.svg" % now.day
# Populate categories
cats = []
cats.append (Unity.Category.new ("Actions",
Gio.ThemedIcon.new(icon_hint),
Unity.CategoryRenderer.VERTICAL_TILE))
cats.append (Unity.Category.new ("Upcoming Events",
Gio.ThemedIcon.new(icon_hint),
Unity.CategoryRenderer.HORIZONTAL_TILE))
cats.append (Unity.Category.new ("Pending Tasks",
Gio.ThemedIcon.new("gtk-paste"),
Unity.CategoryRenderer.HORIZONTAL_TILE))
cats.append (Unity.Category.new ("Recent Events",
Gio.ThemedIcon.new(icon_hint),
Unity.CategoryRenderer.HORIZONTAL_TILE))
cats.append (Unity.Category.new ("Completed Tasks",
Gio.ThemedIcon.new("gtk-paste"),
Unity.CategoryRenderer.HORIZONTAL_TILE))
self.lens.props.categories = cats
# Populate filters
filters = []
f = Unity.RadioOptionFilter.new ("calendars",
"Calendars",
Gio.ThemedIcon.new("input-keyboard-symbolic"),
False)
for calendar in evolution.ecal.list_calendars():
f.add_option (calendar[0], calendar[0], None)
filters.append (f)
# Timerange filters. The values are the range in seconds.
f = Unity.RadioOptionFilter.new ("time",
"Time Range",
Gio.ThemedIcon.new("input-keyboard-symbolic"),
False)
f.add_option ("86400", "Today", None)
f.add_option ("604800", "7 Days", None)
f.add_option ("2592000", "30 Days", None)
f.add_option ("7776000", "90 Days", None)
filters.append (f)
self.lens.props.filters = filters
# Listen for changes and requests
self.scope.connect ("search-changed", self.on_search_changed)
self.scope.connect ("filters-changed", self.on_filters_changed)
self.scope.connect ("notify::active", self.on_lens_active)
self.scope.connect ("activate-uri", self.activate_uri)
self.lens.add_local_scope (self.scope);
self.lens.export ();
def on_filters_changed(self, *_):
""" Called when a filter is clicked. Queue's a new search
"""
self.scope.queue_search_changed(Unity.SearchType.DEFAULT)
def on_lens_active(self, *_):
""" Called when the lens is activated. Queue's a new search
"""
if self.scope.props.active:
self.scope.queue_search_changed(Unity.SearchType.DEFAULT)
def on_search_changed (self, scope, search=None, search_type=0, cancellable=None):
""" Called when the search is changed. Gets the search string and search
type and passes it to update_results_model()
Args:
scope: a scope object
search: the search string
search_type: the search type (global or local)
cancellable: cancellable object
"""
if hasattr(search, "props"):
search_string = search.props.search_string
else:
search_string = ""
if search_type == Unity.SearchType.DEFAULT:
results = scope.props.results_model
else:
results = scope.props.global_results_model
print "Search changed to: '%s'" % search_string
self.update_results_model (search_string, results)
if hasattr(search, "finished"):
search.finished()
def update_results_model (self, search, model, _global=False):
import inspect
model.clear ()
if _global == False:
model.append ("", Gio.ThemedIcon.new ("appointment-new").to_string(), MISC, "taglib/rfc822", "Create a New Event", "", "")
f = self.scope.get_filter("time")
o = f.get_active_option()
if o:
timerange = o.props.id
else:
# Make timerange a really big number if 'All' or nothing is selected so that all events get selected
timerange = "200000000"
timerange = int(timerange)
f = self.scope.get_filter("calendars")
o = f.get_active_option()
if o:
selectedcalendar = o.props.id
else:
selectedcalendar = "all"
self.events = []
for calendar in evolution.ecal.list_calendars():
if calendar[0] == selectedcalendar or selectedcalendar == "all":
eventSource = evolution.ecal.open_calendar_source(calendar[1], evolution.ecal.CAL_SOURCE_TYPE_EVENT)
try:
for c in eventSource.get_all_objects():
sortableevent = []
event = vobject.readOne(c.get_as_string())
dtstart = event.dtstart.value
starttime = mktime(dtstart.timetuple())
sortableevent.append(starttime)
sortableevent.append(c)
sortableevent.append(calendar[1])
self.events.append(sortableevent)
except:
print "Cannot open calendar %s" % calendar[0]
self.events.sort(key=lambda event: event[0])
for event in self.events:
event = vobject.readOne(event[1].get_as_string())
dtstart = event.dtstart.value
summary = event.summary.value
if hasattr(event, "location") :
location = event.location.value
else:
location = ""
now = datetime.datetime.now()
uri = event.uid.value
start = mktime(dtstart.timetuple())
now = mktime(now.timetuple())
upperlimit = mktime(datetime.datetime.now().timetuple()) + timerange
lowerlimit = mktime(datetime.datetime.now().timetuple()) - timerange
icon_hint = "/usr/lib/calendar-lens/icons/%s.svg" % dtstart.day
if start > now and start < upperlimit:
group = UPCOMING
if hasattr(event, "dtend"):
start = event.dtstart.value.strftime('%x %H:%M') + " - " + event.dtend.value.strftime('%H:%M')
if event.dtstart.value.strftime('%x %H:%M') == event.dtend.value.strftime('%x %H:%M'):
start = event.dtstart.value.strftime('%x %H:%M')
if event.dtstart.value.strftime('%H:%M') == "00:00" and event.dtend.value.strftime('%H:%M') == "00:00":
start = event.dtstart.value.strftime('%x') + " All Day"
else:
start = event.dtstart.value.strftime('%x %H:%M')
if not summary.lower().find(search.lower()) == -1:
model.append (uri, # uri
icon_hint, # string formatted GIcon
group, # numeric group id
"taglib/rfc822", # mimetype
summary, # display name
start + "\n" + location,# comment
uri) # drag and drop uri
continue
if not location.lower().find(search.lower()) == -1:
model.append (uri, # uri
icon_hint, # string formatted GIcon
group, # numeric group id
"taglib/rfc822", # mimetype
summary, # display name
start + "\n" + location,# comment
uri) # drag and drop uri
self.events.sort(key=lambda event: event[0], reverse=True)
for event in self.events:
event = vobject.readOne(event[1].get_as_string())
dtstart = event.dtstart.value
summary = event.summary.value
if hasattr(event, "location") :
location = event.location.value
else:
location = ""
now = datetime.datetime.now()
uri = event.uid.value
start = mktime(dtstart.timetuple())
now = mktime(now.timetuple())
icon_hint = "/usr/lib/calendar-lens/icons/%s.svg" % dtstart.day
if start < now and start > lowerlimit:
group = RECENT
if hasattr(event, "dtend"):
start = event.dtstart.value.strftime('%x %H:%M') + " - " + event.dtend.value.strftime('%H:%M')
if event.dtstart.value.strftime('%x %H:%M') == event.dtend.value.strftime('%x %H:%M'):
start = event.dtstart.value.strftime('%x %H:%M')
if event.dtstart.value.strftime('%H:%M') == "00:00" and event.dtend.value.strftime('%H:%M') == "00:00":
start = event.dtstart.value.strftime('%x') + " All Day"
else:
start = event.dtstart.value.strftime('%x %H:%M')
if not summary.lower().find(search.lower()) == -1:
model.append (uri, # uri
icon_hint, # string formatted GIcon
group, # numeric group id
"taglib/rfc822", # mimetype
summary, # display name
start + "\n" + location,# comment
uri) # drag and drop uri
continue
if not location.lower().find(search.lower()) == -1:
model.append (uri, # uri
icon_hint, # string formatted GIcon
group, # numeric group id
"taglib/rfc822", # mimetype
summary, # display name
start + "\n" + location,# comment
uri) # drag and drop uri
def activate_uri(self, scope, uri):
import subprocess
arguments = []
arguments.append("/home/mark/Projects/Lenses/Calendar Lens/dialog.py")
if uri == "":
arguments.append("")
arguments.append("local:system")
arguments.append("False")
else:
for event in self.events:
vevent = vobject.readOne(event[1].get_as_string())
if vevent.uid.value == uri:
selectedevent = vevent
calendar = event[2]
arguments.append(selectedevent.uid.value)
arguments.append(calendar)
arguments.append("True")
subprocess.Popen(arguments)
return Unity.ActivationResponse (handled = Unity.HandledType.HIDE_DASH, goto_uri = '')
if __name__ == "__main__":
# NOTE: If we used the normal 'dbus' module for Python we'll get
# slightly odd results because it uses a default connection
# to the session bus that is different from the default connection
# GDBus (hence libunity) will use. Meaning that the daemon name
# will be owned by a connection different from the one all our
# Dee + Unity magic is working on...
# Still waiting for nice GDBus bindings to land:
# http://www.piware.de/2011/01/na-zdravi-pygi/
session_bus_connection = Gio.bus_get_sync (Gio.BusType.SESSION, None)
session_bus = Gio.DBusProxy.new_sync (session_bus_connection, 0, None,
'org.freedesktop.DBus',
'/org/freedesktop/DBus',
'org.freedesktop.DBus', None)
result = session_bus.call_sync('RequestName',
GLib.Variant ("(su)", (BUS_NAME, 0x4)),
0, -1, None)
# Unpack variant response with signature "(u)". 1 means we got it.
result = result.unpack()[0]
if result != 1 :
print >> sys.stderr, "Failed to own name %s. Bailing out." % BUS_NAME
raise SystemExit (1)
daemon = Daemon()
GObject.MainLoop().run()
|