1# Copyright 2010 Steven Robertson 2# 3# This program is free software; you can redistribute it and/or modify 4# it under the terms of the GNU General Public License as published by 5# the Free Software Foundation; either version 2 of the License, or 6# (at your option) any later version. 7 8""" 9This module will provide a unified notification area for informational 10messages and active tasks. This will eventually handle interactions with 11active tasks (e.g. pausing a copooled task), and provide shortcuts for 12copooling or threading a task with a status notification. It will also provide 13the UI for the planned global undo feature. 14 15Of course, right now it does none of these things. 16""" 17 18# This module is still experimental and may change or be removed. 19 20# TODO: Make copooling things with notifications easier (optional) 21# TODO: Make Ex Falso use this 22# TODO: Port WaitLoadWindow to use this (and not block) 23# TODO: Port Media browser to use this 24# TODO: Port Download Manager to use this 25# TODO: Add basic notification support 26# TODO: Add notification history 27# TODO: Add notification button/callback support (prereq for global undo) 28# TODO: Optimize performance (deferred signals, etc) 29 30from gi.repository import Gtk, GLib, Pango 31 32from quodlibet import _ 33from quodlibet.util import copool 34from quodlibet.qltk.x import SmallImageToggleButton, SmallImageButton, Align 35from quodlibet.qltk import Icons 36 37 38class ParentProperty(object): 39 """ 40 A property which provides a thin layer of protection against accidental 41 reparenting: you must first 'unparent' an instance by setting this 42 property to 'None' before you can set a new parent. 43 """ 44 def __get__(self, inst, owner): 45 return getattr(inst, '_parent', None) 46 47 def __set__(self, inst, value): 48 if getattr(inst, '_parent', None) is not None and value is not None: 49 raise ValueError("Cannot set parent property without first " 50 "setting it to 'None'.") 51 inst._parent = value 52 53 54class Task(object): 55 def __init__(self, source, desc, known_length=True, controller=None, 56 pause=None, stop=None): 57 self.source = source 58 self.desc = desc 59 if known_length: 60 self.frac = 0. 61 else: 62 self.frac = None 63 if controller: 64 self.controller = controller 65 else: 66 self.controller = TaskController.default_instance 67 self._pause = pause 68 self._stop = stop 69 self.pausable = bool(pause) 70 self.stoppable = bool(stop) 71 self._paused = False 72 self.controller.add_task(self) 73 74 def update(self, frac): 75 """ 76 Update a task's progress. 77 """ 78 self.frac = frac 79 self.controller.update() 80 81 def pulse(self): 82 """ 83 Indicate progress on a task of unknown length. 84 """ 85 self.update(None) 86 87 def finish(self): 88 """ 89 Mark a task as finished, and remove it from the list of active tasks. 90 """ 91 self.frac = 1.0 92 self.controller.finish(self) 93 94 @property 95 def paused(self): 96 return self._paused 97 98 @paused.setter 99 def paused(self, value): 100 if self.pausable: 101 self._pause(value) 102 self._paused = value 103 104 def stop(self): 105 if self._stop: 106 self._stop() 107 self.finish() 108 109 def gen(self, gen): 110 """ 111 Act as a generator pass-through, updating and finishing the task's 112 progress automatically. If 'gen' has a __len__ property, it will be 113 used to set the fraction accordingly. 114 """ 115 try: 116 if hasattr(gen, '__len__'): 117 for i, x in enumerate(gen): 118 self.update(float(i) / len(gen)) 119 yield x 120 else: 121 for x in gen: 122 yield x 123 finally: 124 self.finish() 125 126 def list(self, l): 127 """ 128 Evaluates the iterable argument before passing to 'gen'. 129 """ 130 return self.gen(list(l)) 131 132 def copool(self, funcid, pause=True, stop=True): 133 """ 134 Convenience function: set the Task's 'pause' and 'stop' callbacks to 135 act upon the copool with the given funcid. 136 """ 137 if pause: 138 def pause_func(state): 139 if state != self._paused: 140 if state: 141 copool.pause(funcid) 142 else: 143 copool.resume(funcid) 144 self._pause = pause_func 145 self.pausable = True 146 if stop: 147 self._stop = lambda: copool.remove(funcid) 148 self.stoppable = True 149 150 # Support context managers: 151 # >>> with Task(...) as t: 152 def __enter__(self): 153 return self 154 155 def __exit__(self, exc_type, exc_val, exc_tb): 156 self.finish() 157 return False 158 159 160class TaskController(object): 161 """ 162 Controller logic for displaying and managing a list of Tasks. Also 163 implements the full Task interface to act as a pass-through or summary of 164 all tasks in flight on this controller. 165 """ 166 parent = ParentProperty() 167 default_instance = None 168 169 def __init__(self): 170 self.active_tasks = [] 171 self._parent = None 172 self.update() 173 174 def add_task(self, task): 175 self.active_tasks.append(task) 176 self.update() 177 178 @property 179 def source(self): 180 if len(self.active_tasks) == 1: 181 return self.active_tasks[0].source 182 return _("Active tasks") 183 184 @property 185 def desc(self): 186 if len(self.active_tasks) == 1: 187 return self.active_tasks[0].desc 188 return _("%d tasks running") % len(self.active_tasks) 189 190 @property 191 def frac(self): 192 fracs = [t.frac for t in self.active_tasks if t.frac is not None] 193 if fracs: 194 return sum(fracs) / len(self.active_tasks) 195 return None 196 197 @property 198 def paused(self): 199 pausable = [t for t in self.active_tasks if t.pausable] 200 if not pausable: 201 return False 202 return not [t for t in pausable if not t.paused] 203 204 @paused.setter 205 def paused(self, val): 206 for t in self.active_tasks: 207 if t.pausable: 208 t.paused = val 209 210 def stop(self): 211 [t.stop() for t in self.active_tasks if t.stoppable] 212 213 @property 214 def pausable(self): 215 return [t for t in self.active_tasks if t.pausable] 216 217 @property 218 def stoppable(self): 219 return [t for t in self.active_tasks if t.stoppable] 220 221 def update(self): 222 if self._parent is not None: 223 self._parent.update() 224 225 def finish(self, finished_task): 226 self.active_tasks = list(filter(lambda t: t is not finished_task, 227 self.active_tasks)) 228 self.update() 229 230# Oh so deliciously hacky. 231TaskController.default_instance = TaskController() 232 233 234class TaskWidget(Gtk.HBox): 235 """ 236 Displays a task. 237 """ 238 def __init__(self, task): 239 super(TaskWidget, self).__init__(spacing=2) 240 self.task = task 241 self.label = Gtk.Label() 242 self.label.set_alignment(1.0, 0.5) 243 self.label.set_ellipsize(Pango.EllipsizeMode.END) 244 self.pack_start(self.label, True, True, 12) 245 self.progress = Gtk.ProgressBar() 246 self.progress.set_size_request(100, -1) 247 self.pack_start(self.progress, True, True, 0) 248 self.pause = SmallImageToggleButton() 249 self.pause.add( 250 Gtk.Image.new_from_icon_name(Icons.MEDIA_PLAYBACK_PAUSE, 251 Gtk.IconSize.MENU)) 252 self.pause.connect('toggled', self.__pause_toggled) 253 self.pack_start(self.pause, False, True, 0) 254 self.stop = SmallImageButton() 255 self.stop.add( 256 Gtk.Image.new_from_icon_name(Icons.MEDIA_PLAYBACK_STOP, 257 Gtk.IconSize.MENU)) 258 self.stop.connect('clicked', self.__stop_clicked) 259 self.pack_start(self.stop, False, True, 0) 260 261 def __pause_toggled(self, btn): 262 if self.task.pausable: 263 self.task.paused = btn.props.active 264 265 def __stop_clicked(self, btn): 266 if self.task.stoppable: 267 self.task.stop() 268 269 def update(self): 270 formatted_label = "<small><b>%s</b>\n%s</small>" % (self.task.source, 271 self.task.desc) 272 self.label.set_markup(formatted_label) 273 if self.task.frac is not None: 274 self.progress.set_fraction(self.task.frac) 275 else: 276 self.progress.pulse() 277 if self.pause.props.sensitive != self.task.pausable: 278 self.pause.props.sensitive = self.task.pausable 279 show_as_active = (self.task.pausable and self.task.paused) 280 if self.pause.props.active != show_as_active: 281 self.pause.props.active = show_as_active 282 if self.stop.props.sensitive != self.task.stoppable: 283 self.stop.props.sensitive = self.task.stoppable 284 285 286class StatusBar(Gtk.HBox): 287 def __init__(self, task_controller): 288 super(StatusBar, self).__init__() 289 self.__dirty = False 290 self.set_spacing(12) 291 self.task_controller = task_controller 292 self.task_controller.parent = self 293 294 self.default_label = Gtk.Label(selectable=True) 295 self.default_label.set_ellipsize(Pango.EllipsizeMode.END) 296 self.pack_start( 297 Align(self.default_label, halign=Gtk.Align.END), 298 True, True, 0) 299 self.task_widget = TaskWidget(task_controller) 300 self.pack_start(self.task_widget, True, True, 0) 301 # The history button will eventually hold the full list of running 302 # tasks, as well as the list of previous notifications. 303 #self.history_btn = Gtk.Button() 304 #self.pack_start(self.history_btn, False, True, 0) 305 306 self.show_all() 307 self.set_no_show_all(True) 308 self.__set_shown('default') 309 self.connect("destroy", self.__destroy) 310 311 def __destroy(self, *args): 312 self.task_controller.parent = None 313 314 def __set_shown(self, type): 315 if type == 'default': 316 self.default_label.show() 317 else: 318 self.default_label.hide() 319 if type == 'task': 320 self.task_widget.show() 321 else: 322 self.task_widget.hide() 323 324 def set_default_text(self, text): 325 self.default_label.set_text(text) 326 327 def __update(self): 328 self.__dirty = False 329 if self.task_controller.active_tasks: 330 self.__set_shown('task') 331 self.task_widget.update() 332 else: 333 self.__set_shown('default') 334 335 def update(self): 336 if not self.__dirty: 337 self.__dirty = True 338 GLib.idle_add(self.__update) 339