183 else: |
185 else: |
184 title=status.name |
186 title=status.name |
185 |
187 |
186 return title, (status.state, status.errors), this_refresh_time |
188 return title, (status.state, status.errors), this_refresh_time |
187 |
189 |
|
190 class EventTrackingTimer(rumps.Timer): |
|
191 """ |
|
192 Variant of rumps.Timer changed to |
|
193 a) use *both* NSEventTrackingRunLoopMode (for when menu open) and |
|
194 NSDefaultRunLoopMode (for when menu closed), |
|
195 b) in the mainRunLoop, to allow creating timers there from other threads. |
|
196 """ |
|
197 def start(self): |
|
198 """Start the timer thread loop.""" |
|
199 if not self._status: |
|
200 self._nsdate = NSDate.date() |
|
201 self._nstimer = NSTimer.alloc().initWithFireDate_interval_target_selector_userInfo_repeats_( |
|
202 self._nsdate, self._interval, self, 'callback:', None, True) |
|
203 NSRunLoop.mainRunLoop().addTimer_forMode_(self._nstimer, NSEventTrackingRunLoopMode) |
|
204 NSRunLoop.mainRunLoop().addTimer_forMode_(self._nstimer, NSDefaultRunLoopMode) |
|
205 #rumps._TIMERS[self] = None |
|
206 self._status = True |
|
207 |
188 class BorgendTray(rumps.App): |
208 class BorgendTray(rumps.App): |
189 def __init__(self, backups): |
209 def __init__(self, backups): |
190 self.lock=Lock() |
210 self.lock=Lock() |
191 self.backups=backups |
211 self.backups=backups |
192 self.refresh_timer=None |
212 self.refresh_timer=EventTrackingTimer(self.__refresh_callback, refresh_interval) |
193 self.refresh_timer_time=None |
213 self.updated_recently=False |
194 self.statuses=[None]*len(backups) |
214 self.statuses=[None]*len(backups) |
195 |
215 |
196 for index in range(len(backups)): |
216 for index in range(len(backups)): |
197 self.statuses[index]=backups[index].status() |
217 self.statuses[index]=backups[index].status() |
198 |
218 |
199 menu, title=self.build_menu_and_timer() |
219 itemlist, title=self.build_menu_and_timer() |
200 |
220 |
201 super().__init__(title, menu=menu, quit_button=None) |
221 super().__init__(title, menu=itemlist, quit_button=None) |
202 |
222 |
203 for index in range(len(backups)): |
223 for index in range(len(backups)): |
204 # Python closures suck dog's balls; hence the _index=index hack |
224 # Python closures suck dog's balls; hence the _index=index hack |
205 # See also http://math.andrej.com/2009/04/09/pythons-lambda-is-broken/ |
225 # See also http://math.andrej.com/2009/04/09/pythons-lambda-is-broken/ |
206 cb=(lambda status, errorlog=None, _index=index: |
226 cb=(lambda status, errorlog=None, _index=index: |
207 self.__status_callback(_index, status, errorlog=errorlog)) |
227 self.__status_callback(_index, status, errorlog=errorlog)) |
208 backups[index].set_status_update_callback(cb) |
228 backups[index].set_status_update_callback(cb) |
209 |
229 |
210 dreamtime.add_callback(self, self.__sleepwake_callback) |
230 dreamtime.add_callback(self, self.__sleepwake_callback) |
211 |
231 |
212 def __rebuild_menu(self): |
232 def __rebuild_menu(self, update): |
213 menu=[] |
233 if not update: |
|
234 self.itemlist=[] |
|
235 |
|
236 itemlist=self.itemlist |
|
237 |
214 state=(backup.State.INACTIVE, backup.Errors.OK) |
238 state=(backup.State.INACTIVE, backup.Errors.OK) |
215 refresh_time=None |
239 refresh_time=None |
216 all_paused=True |
240 all_paused=True |
217 |
241 |
218 for index in range(len(self.backups)): |
242 for index in range(len(self.backups)): |
220 title, this_state, this_refresh_time=make_title(self.statuses[index]) |
244 title, this_state, this_refresh_time=make_title(self.statuses[index]) |
221 # Python closures suck dog's balls... |
245 # Python closures suck dog's balls... |
222 # first and the last program I write in Python until somebody |
246 # first and the last program I write in Python until somebody |
223 # fixes this brain damage |
247 # fixes this brain damage |
224 cbm=lambda sender, _b=b: self.__menu_select_backup(sender, _b) |
248 cbm=lambda sender, _b=b: self.__menu_select_backup(sender, _b) |
225 item=rumps.MenuItem(title, callback=cbm) |
249 itemstate=0 |
226 if not this_state[1].ok(): |
250 if not this_state[1].ok(): |
227 item.state=-1 |
251 itemstate=-1 |
228 elif this_state[0]==backup.State.SCHEDULED or this_state[0]==backup.State.QUEUED: |
252 elif this_state[0]==backup.State.SCHEDULED or this_state[0]==backup.State.QUEUED: |
229 item.state=1 |
253 itemstate=1 |
230 menu.append(item) |
254 |
|
255 if update: |
|
256 item=itemlist[index] |
|
257 if item.title!=title: |
|
258 item.title=title |
|
259 if item.state!=itemstate: |
|
260 item.state=itemstate |
|
261 else: |
|
262 item=rumps.MenuItem(title, callback=cbm) |
|
263 item.state=itemstate |
|
264 itemlist.append(item) |
|
265 |
231 state=combine_state(state, this_state) |
266 state=combine_state(state, this_state) |
232 |
267 |
233 all_paused=(all_paused and this_state[0]==backup.State.PAUSED) |
268 all_paused=(all_paused and this_state[0]==backup.State.PAUSED) |
234 |
269 |
235 # Do we have to automatically update menu display? |
270 # Do we have to automatically update menu display? |
236 if not refresh_time: |
271 if not refresh_time: |
237 refresh_time=this_refresh_time |
272 refresh_time=this_refresh_time |
238 elif this_refresh_time: |
273 elif this_refresh_time: |
239 refresh_time=min(refresh_time, this_refresh_time) |
274 refresh_time=min(refresh_time, this_refresh_time) |
240 |
275 |
241 menu_log=rumps.MenuItem("Show log", callback=lambda _: showlog()) |
276 index = len(self.backups) |
242 menu.append(menu_log) |
|
243 |
|
244 if all_paused: |
277 if all_paused: |
245 pausename='Resume all' |
278 pausename='Resume all' |
246 else: |
279 else: |
247 pausename="Pause all" |
280 pausename="Pause all" |
248 menu_pause=rumps.MenuItem(pausename, callback=lambda _: self.pause_resume_all()) |
281 if update: |
249 menu.append(menu_pause) |
282 item=itemlist[index] |
250 |
283 if item.title!=pausename: |
251 if not settings['no_quit_menu_entry']: |
284 item.title=pausename |
252 menu_quit=rumps.MenuItem("Quit...", callback=lambda _: self.quit()) |
285 else: |
253 menu.append(menu_quit) |
286 menu_pause=rumps.MenuItem(pausename, callback=lambda _: self.pause_resume_all()) |
254 |
287 itemlist.append(menu_pause) |
255 return menu, state, refresh_time |
288 |
256 |
289 if not update: |
257 def build_menu_and_timer(self): |
290 menu_log=rumps.MenuItem("Show log", callback=lambda _: showlog()) |
258 if self.refresh_timer: |
291 itemlist.append(menu_log) |
259 self.refresh_timer.cancel() |
292 if not settings['no_quit_menu_entry']: |
260 self.refresh_timer=None |
293 menu_quit=rumps.MenuItem("Quit...", callback=lambda _: self.quit()) |
261 self.refresh_timer_time=None |
294 itemlist.append(menu_quit) |
262 |
295 |
|
296 return itemlist, state, refresh_time |
|
297 |
|
298 def build_menu_and_timer(self, update=False): |
263 logger.debug('Rebuilding menu') |
299 logger.debug('Rebuilding menu') |
264 menu, state, refresh_time=self.__rebuild_menu() |
300 itemlist, state, refresh_time=self.__rebuild_menu(update) |
265 title=trayname(state) |
301 title=trayname(state) |
266 |
302 |
267 if refresh_time: |
303 if update and self.title!=title: |
|
304 logger.debug("set title %s" % title) |
|
305 self.title=title |
|
306 |
|
307 if not self.updated_recently and not refresh_time: |
|
308 self.refresh_timer.stop() |
|
309 elif self.updated_recently: |
|
310 self.updated_recently=False |
|
311 if self.refresh_timer.interval>refresh_interval: |
|
312 self.refresh_timer.stop() |
|
313 self.refresh_timer.interval=refresh_interval |
|
314 self.refresh_timer.start() |
|
315 else: |
268 # Need to time a refresh due to content display changing, |
316 # Need to time a refresh due to content display changing, |
269 # e.g., 'tomorrow' changing to a more specific hour. |
317 # e.g., 'tomorrow' changing to a more specific hour. |
270 when=time.mktime(refresh_time.timetuple()) |
318 when=time.mktime(refresh_time.timetuple()) |
271 delay=when-time.time() |
319 delay=max(when-time.time(), refresh_interval) |
272 if delay>0: |
320 logger.debug('Timing menu refresh in %s seconds' % delay) |
273 logger.debug('Timing menu refresh in %s seconds' % delay) |
321 self.refresh_timer.stop() |
274 self.refresh_timer=Timer(delay, self.refresh_ui) |
322 self.refresh_timer.interval=delay |
275 self.refresh_timer_time=refresh_time |
323 self.refresh_timer.start() |
276 self.refresh_timer.start() |
324 |
277 |
325 return itemlist, title |
278 return menu, title |
|
279 |
326 |
280 # Callbacks -- exception-protected to get any indications of errors |
327 # Callbacks -- exception-protected to get any indications of errors |
281 |
328 |
282 @protect_noreturn |
329 @protect_noreturn |
283 def refresh_ui(self): |
330 def __refresh_callback(self, timer): |
284 with self.lock: |
331 with self.lock: |
285 menu, title=self.build_menu_and_timer() |
332 self.build_menu_and_timer(True) |
286 self.menu.clear() |
333 |
287 self.menu.update(menu) |
|
288 self.title=title |
|
289 |
|
290 @protect_noreturn |
334 @protect_noreturn |
291 def __status_callback(self, index, status, errorlog=None): |
335 def __status_callback(self, index, status, errorlog=None): |
292 logger.debug("Tray status callback") |
336 logger.debug("Tray status callback") |
293 with self.lock: |
337 with self.lock: |
294 self.statuses[index]=status |
338 self.statuses[index]=status |
|
339 self.updated_recently=True |
295 # Time the refresh if it has not been timed, or if the timer |
340 # Time the refresh if it has not been timed, or if the timer |
296 # is timing for the "long-term" (refresh_timer_time set) |
341 # is timing for the "long-term" (refresh_timer_time set) |
297 if not self.refresh_timer or self.refresh_timer_time: |
342 if self.refresh_timer.interval>refresh_interval: |
298 # logger.debug("Timing refresh") |
343 self.refresh_timer.stop() |
299 self.refresh_timer=Timer(refresh_interval, self.refresh_ui) |
344 self.refresh_timer.interval=refresh_interval |
300 # refresh_timer_time is only set for "long-term timers" |
345 self.refresh_timer.start() |
301 self.refresh_timer_time=None |
|
302 self.refresh_timer.start() |
|
303 |
346 |
304 # if errorlog: |
347 # if errorlog: |
305 # if 'msgid' not in errorlog or not isinstance(errorlog['msgid'], str): |
348 # if 'msgid' not in errorlog or not isinstance(errorlog['msgid'], str): |
306 # msgid='UnknownError' |
349 # msgid='UnknownError' |
307 # else: |
350 # else: |