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