Added support for 0release.
[memo.git] / memos.py
blobe75fa7aa0a0159fc76f33ed563181577dc72ea5b
1 from __future__ import generators
3 import rox, gobject
4 from rox import g, app_options, options, basedir
6 import time
7 import os
9 from Memo import Memo, memo_from_node
11 max_visible = options.Option('max_visible', 5)
12 max_future = options.Option('max_future', 6)
14 # Columns
15 TIME = 0
16 BRIEF = 1
17 MEMO = 2
18 HIDDEN = 3
20 class MemoList(g.ListStore):
21 __gsignals__ = {
22 'MemoListChanged' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, [])
25 def __init__(self):
26 g.ListStore.__init__(self, gobject.TYPE_STRING, # Time
27 gobject.TYPE_STRING, # Brief
28 gobject.TYPE_OBJECT, # Memo
29 gobject.TYPE_BOOLEAN) # Deleted
31 def __iter__(self):
32 "When used as a python iterator, return a list of TreeIters"
33 iter = self.get_iter_first()
34 while iter:
35 yield iter
36 iter = self.iter_next(iter)
38 def delete(self, memo, update = 1):
39 import dbus_notify
40 dbus_notify.close(memo)
41 for iter in self:
42 m = self.get_value(iter, MEMO)
43 if m is memo:
44 self.remove(iter)
45 if update:
46 self.notify_changed()
47 return
48 # Not found. That's OK.
50 def new_day(self):
51 "Recalculate the time display after midnight."
52 for memo in self:
53 self.set(memo, TIME, self.get_value(memo, MEMO).str_when())
54 self.notify_changed()
56 def add(self, memo, update = 1):
57 assert isinstance(memo, Memo)
59 for iter in self:
60 m = self.get_value(iter, MEMO)
61 if m.comes_after(memo):
62 break
63 else:
64 iter = None
66 if iter:
67 new = self.insert_before(iter)
68 else:
69 # PyGtk bug
70 new = self.append()
72 self.set(new,
73 TIME, memo.str_when(),
74 BRIEF, memo.brief,
75 MEMO, memo,
76 HIDDEN, memo.hidden)
78 if update:
79 self.notify_changed()
81 def notify_changed(self):
82 "Called after a Memo is added, removed or updated."
83 self.emit( "MemoListChanged" );
85 def get_memo_by_path(self, path):
86 iter = self.get_iter(path)
87 return self.get_memo_by_iter(iter)
89 def get_memo_by_iter(self, iter):
90 return self.get_value(iter, MEMO)
92 def catch_up(self, early = 0):
93 "Returns a list of alarms to go off, and the time until the "
94 "next alarm (in seconds) or None."
96 missed = []
97 now = time.time()
99 minDelay = None
100 for iter in self:
101 m = self.get_value(iter, MEMO)
103 if m.hidden or not m.at or m.state == Memo.DONE:
104 continue
106 delay = m.time - now
107 earlyDelay = delay - (early*60)
109 if (delay <= 0) or (m.state == Memo.READY and earlyDelay <= 0):
110 missed.append(m)
112 if earlyDelay > 0 and (minDelay is None or earlyDelay < minDelay):
113 minDelay = earlyDelay
114 elif delay > 0 and (minDelay is None or delay < minDelay):
115 minDelay = delay
116 elif minDelay is not None and \
117 earlyDelay > minDelay and delay > minDelay:
118 # Memos are sorted by delay time, so if you hit one that has both
119 # delay and early delay after the lowest registered delay time, you
120 # can stop looking.
121 # This will no longer be true if we allow custom per-memo early
122 # delay times. Which would be ugly.
123 break;
125 return (missed, minDelay)
127 class MasterList(MemoList):
128 def __init__(self):
129 MemoList.__init__(self)
131 self.visible = MemoList()
133 path = basedir.load_first_config('rox.sourceforge.net', 'Memo', 'Entries')
134 if path:
135 try:
136 from xml.dom import minidom, Node
137 doc = minidom.parse(path)
138 except:
139 rox.report_exception()
141 errors = 0
142 root = doc.documentElement
143 for node in root.getElementsByTagName('memo'):
144 try:
145 memo = memo_from_node(node)
146 self.add(memo, update = 0)
147 except:
148 if not errors:
149 rox.report_exception()
150 errors = 1
151 self.update_visible()
152 app_options.add_notify(self.update_visible)
154 def new_day(self):
155 MemoList.new_day(self)
156 self.visible.new_day()
158 def toggle_hidden(self, path):
159 if g.pygtk_version == (1, 99, 12):
160 iter = self.get_iter_first()
161 self.get_iter_from_string(iter, path)
162 else:
163 iter = self.get_iter_from_string(path)
165 memo = self.get_memo_by_iter(iter)
166 self.set_hidden(memo, not memo.hidden)
168 def set_hidden(self, memo, hidden):
169 self.delete(memo, update = 0)
170 memo.set_hidden(hidden)
171 self.add(memo)
173 def save(self):
174 save_dir = basedir.save_config_path('rox.sourceforge.net', 'Memo')
175 path = os.path.join(save_dir, 'Entries.new')
176 if not path:
177 sys.stderr.write(
178 "Memo: Saving disabled by CHOICESPATH\n")
179 return
180 try:
181 f = os.open(path, os.O_CREAT | os.O_WRONLY, 0600)
182 self.save_to_stream(os.fdopen(f, 'w'))
184 real_path = os.path.join(save_dir, 'Entries')
185 os.rename(path, real_path)
186 except:
187 rox.report_exception()
189 def save_to_stream(self, stream):
190 from xml.dom import minidom
191 doc = minidom.Document()
193 root = doc.createElement('memos')
194 doc.appendChild(root)
195 for iter in self:
196 m = self.get_value(iter, MEMO)
197 m.save(root)
198 root.appendChild(doc.createTextNode('\n'))
199 doc.writexml(stream)
201 def notify_changed(self):
202 "Called after a Memo is added, removed or updated."
203 MemoList.notify_changed(self)
204 self.update_visible()
205 self.save()
207 def count_today(self):
208 "Return the count of memos with today's date in a tuple: (all,hidden)"
209 # TODO: Could be more efficient and just return an int that is
210 # automatically kept up-to-date by all add/remove/new_day routines instead
211 # of counting all memos every time.
212 import datetime
213 now = datetime.datetime.now()
214 todayStart = now.replace(hour=0, minute=0, second=0, microsecond=0)
215 todayEnd = now.replace(hour=23, minute=59, second=59, microsecond=999999)
216 START_OF_TODAY = time.mktime(todayStart.timetuple())
217 END_OF_TODAY = time.mktime(todayEnd.timetuple())
218 all = 0
219 hidden = 0
220 for iter in self:
221 memo = self.get_value(iter, MEMO)
222 if memo.time < START_OF_TODAY:
223 continue
224 if memo.time > END_OF_TODAY:
225 break
226 all += 1
227 if memo.hidden:
228 hidden += 1
229 return (all, hidden)
231 def choose_visible(self):
232 "Return a list of Memos which should be made/kept visible."
233 now = time.time()
234 A_DAY = 60 * 60 * 24
236 VISIBLE_REGION = A_DAY * 31 * max_future.int_value
238 out = []
239 for iter in self:
240 memo = self.get_value(iter, MEMO)
242 if memo.hidden:
243 continue
245 if memo.time > now + VISIBLE_REGION:
246 break # Way too far ahead
248 if memo.time > now + A_DAY:
249 # Skip future memos if the list is too long
250 if len(out) >= max_visible.int_value:
251 break
253 out.append(memo)
255 return out
257 def update_visible(self):
258 # Find what was in the visible list
259 old_vis = {}
260 for iter in self.visible:
261 old_vis[self.visible.get_value(iter, MEMO)] = None
263 new_vis = self.choose_visible()
265 # Find what has changed
266 to_hide = []
267 to_show = []
268 for memo in new_vis:
269 if memo in old_vis:
270 del old_vis[memo]
271 else:
272 to_show.append(memo)
274 # Anything remaining was visible but isn't now
275 for memo in old_vis:
276 to_hide.append(memo)
278 if not to_show and not to_hide:
279 return
281 # Apply changes
282 for m in to_hide:
283 self.visible.delete(m, update = 0)
284 for m in to_show:
285 self.visible.add(m, update = 0)
286 self.visible.notify_changed()
288 def warn_if_not_visible(self, memo):
289 if memo.hidden:
290 return
291 for iter in self.visible:
292 m = self.visible.get_value(iter, MEMO)
293 if m is memo:
294 return
295 rox.info("This memo has been added, but is not shown in the main "
296 "window. Use Show All from the popup menu to see it.\n\n"
297 "You can use the Options window to control when memos "
298 "are shown in the main window.")