Cope with deleting a memo twice.
[memo.git] / memos.py
blob8ad03c552e0fd79f18b56260b62cbf3a7c04e3ee
1 from __future__ import generators
3 import rox, gobject
4 from rox import g, choices, app_options, options
6 import time
7 import os
9 from Memo import Memo, memo_from_node
11 max_visible = options.Option('max_visible', 5)
12 app_options.register(max_visible)
14 max_future = options.Option('max_future', 6)
15 app_options.register(max_future)
17 # Columns
18 TIME = 0
19 BRIEF = 1
20 MEMO = 2
21 HIDDEN = 3
23 class MemoList(g.ListStore):
24 def __init__(self):
25 g.ListStore.__init__(self, gobject.TYPE_STRING, # Time
26 gobject.TYPE_STRING, # Brief
27 gobject.TYPE_OBJECT, # Memo
28 gobject.TYPE_BOOLEAN) # Deleted
29 self.watchers = []
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 if not self.iter_next(iter):
37 iter = None
39 def delete(self, memo, update = 1):
40 for iter in self:
41 m = self.get_value(iter, MEMO)
42 if m is memo:
43 self.remove(iter)
44 if update:
45 self.notify_changed()
46 return
47 # Not found. That's OK.
49 def add(self, memo, update = 1):
50 assert isinstance(memo, Memo)
52 for iter in self:
53 m = self.get_value(iter, MEMO)
54 if m.comes_after(memo):
55 break
56 else:
57 iter = None
59 if iter:
60 new = self.insert_before(iter)
61 else:
62 # PyGtk bug
63 new = self.append()
65 self.set(new,
66 TIME, memo.str_when(),
67 BRIEF, memo.brief,
68 MEMO, memo,
69 HIDDEN, memo.hidden)
71 if update:
72 self.notify_changed()
74 def notify_changed(self):
75 "Called after a Memo is added, removed or updated."
76 map(apply, self.watchers)
78 def get_memo_by_path(self, path):
79 iter = self.get_iter(path)
80 return self.get_memo_by_iter(iter)
82 def get_memo_by_iter(self, iter):
83 return self.get_value(iter, MEMO)
85 def catch_up(self):
86 "Returns a list of alarms to go off, and the time until the "
87 "next alarm (in seconds) or None."
89 missed = []
90 now = time.time()
92 for iter in self:
93 m = self.get_value(iter, MEMO)
95 if m.silent or m.hidden or not m.at:
96 continue
98 delay = m.time - now
100 if delay <= 0:
101 missed.append(m)
102 else:
103 return (missed, delay)
104 return (missed, None)
106 class MasterList(MemoList):
107 def __init__(self):
108 MemoList.__init__(self)
110 self.visible = MemoList()
112 path = choices.load('Memo', 'Entries')
113 if path:
114 try:
115 from xml.dom import minidom, Node
116 doc = minidom.parse(path)
117 except:
118 rox.report_exception()
120 errors = 0
121 root = doc.documentElement
122 for node in root.getElementsByTagName('memo'):
123 try:
124 memo = memo_from_node(node)
125 self.add(memo, update = 0)
126 except:
127 if not errors:
128 rox.report_exception()
129 errors = 1
130 self.update_visible()
131 app_options.add_notify(self.update_visible)
133 def toggle_hidden(self, path):
134 iter = self.get_iter_first()
135 self.get_iter_from_string(iter, path)
136 memo = self.get_memo_by_iter(iter)
137 self.set_hidden(memo, not memo.hidden)
139 def set_hidden(self, memo, hidden):
140 self.delete(memo, update = 0)
141 memo.set_hidden(hidden)
142 self.add(memo)
144 def save(self):
145 path = choices.save('Memo', 'Entries.new')
146 if not path:
147 sys.stderr.write(
148 "Memo: Saving disabled by CHOICESPATH\n")
149 return
150 try:
151 f = os.open(path, os.O_CREAT | os.O_WRONLY, 0600)
152 self.save_to_stream(os.fdopen(f, 'w'))
154 real_path = choices.save('Memo', 'Entries')
155 os.rename(path, real_path)
156 except:
157 rox.report_exception()
159 def save_to_stream(self, stream):
160 from xml.dom import minidom
161 doc = minidom.Document()
163 root = doc.createElement('memos')
164 doc.appendChild(root)
165 for iter in self:
166 m = self.get_value(iter, MEMO)
167 m.save(root)
168 root.appendChild(doc.createTextNode('\n'))
169 doc.writexml(stream)
171 def notify_changed(self):
172 "Called after a Memo is added, removed or updated."
173 MemoList.notify_changed(self)
174 self.update_visible()
175 self.save()
177 def choose_visible(self):
178 "Return a list of Memos which should be made/kept visible."
179 now = time.time()
180 A_DAY = 60 * 60 * 24
182 VISIBLE_REGION = A_DAY * 31 * max_future.int_value
184 out = []
185 for iter in self:
186 memo = self.get_value(iter, MEMO)
188 if memo.hidden:
189 continue
191 if memo.time > now + VISIBLE_REGION:
192 break # Way too far ahead
194 if memo.time > now + A_DAY:
195 # Skip future memos if the list is too long
196 if len(out) >= max_visible.int_value:
197 break
199 out.append(memo)
201 return out
203 def update_visible(self):
204 # Find what was in the visible list
205 old_vis = {}
206 for iter in self.visible:
207 old_vis[self.visible.get_value(iter, MEMO)] = None
209 new_vis = self.choose_visible()
211 # Find what has changed
212 to_hide = []
213 to_show = []
214 for memo in new_vis:
215 if memo in old_vis:
216 del old_vis[memo]
217 else:
218 to_show.append(memo)
220 # Anything remaining was visible but isn't now
221 for memo in old_vis:
222 to_hide.append(memo)
224 if not to_show and not to_hide:
225 return
227 # Apply changes
228 for m in to_hide:
229 self.visible.delete(m, update = 0)
230 for m in to_show:
231 self.visible.add(m, update = 0)
232 self.visible.notify_changed()
234 def warn_if_not_visible(self, memo):
235 if memo.hidden:
236 return
237 for iter in self.visible:
238 m = self.visible.get_value(iter, MEMO)
239 if m is memo:
240 return
241 rox.info("This memo has been added, but is not shown in the main "
242 "window. Use Show All from the popup menu to see it.\n\n"
243 "You can use the Options window to control when memos "
244 "are shown in the main window.")