borgend/ui.py

changeset 133
ec8014a2ee7a
parent 114
ad9fb3dd9fec
child 141
a1c97bc1789e
equal deleted inserted replaced
132:8fe3cf6487f8 133:ec8014a2ee7a
8 import time 8 import time
9 import datetime 9 import datetime
10 import logging 10 import logging
11 import objc 11 import objc
12 import math 12 import math
13 from threading import Lock, Timer 13 from threading import Lock
14 from Foundation import (NSDate, NSTimer, NSRunLoop, NSDefaultRunLoopMode)
15 from AppKit import (NSEventTrackingRunLoopMode)
14 16
15 from . import backup 17 from . import backup
16 from . import dreamtime 18 from . import dreamtime
17 from . import branding 19 from . import branding
18 from . import loggers 20 from . import loggers
46 return traynames_ok[state] 48 return traynames_ok[state]
47 49
48 def combine_state(a, b): 50 def combine_state(a, b):
49 return (max(a[0], b[0]), max(a[1], b[1])) 51 return (max(a[0], b[0]), max(a[1], b[1]))
50 52
51 # Refresh the menu at most once a second to reduce flicker 53 # Refresh the menu at most twice a second
52 refresh_interval=1.0 54 refresh_interval=0.5
53 55
54 # Workaround to rumps brokenness; 56 # Workaround to rumps brokenness;
55 # see https://github.com/jaredks/rumps/issues/59 57 # see https://github.com/jaredks/rumps/issues/59
56 def notification_workaround(title, subtitle, message): 58 def notification_workaround(title, subtitle, message):
57 try: 59 try:
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:

mercurial