Added hooke.plugin.playlist.ApplyCommandStack and related changes.
[hooke.git] / hooke / plugin / playlist.py
1 # Copyright (C) 2010 W. Trevor King <wking@drexel.edu>
2 #
3 # This file is part of Hooke.
4 #
5 # Hooke is free software: you can redistribute it and/or modify it
6 # under the terms of the GNU Lesser General Public License as
7 # published by the Free Software Foundation, either version 3 of the
8 # License, or (at your option) any later version.
9 #
10 # Hooke is distributed in the hope that it will be useful, but WITHOUT
11 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
12 # or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General
13 # Public License for more details.
14 #
15 # You should have received a copy of the GNU Lesser General Public
16 # License along with Hooke.  If not, see
17 # <http://www.gnu.org/licenses/>.
18
19 """The ``playlist`` module provides :class:`PlaylistPlugin` and
20 several associated :class:`hooke.command.Command`\s for handling
21 :mod:`hooke.playlist` classes.
22 """
23
24 import glob
25 import os.path
26
27 from ..command import Command, Argument, Failure
28 from ..playlist import FilePlaylist
29 from . import Builtin
30
31
32 class PlaylistPlugin (Builtin):
33     def __init__(self):
34         super(PlaylistPlugin, self).__init__(name='playlist')
35         self._commands = [
36             NextCommand(self), PreviousCommand(self), JumpCommand(self),
37             GetCommand(self), IndexCommand(self), CurveListCommand(self),
38             SaveCommand(self), LoadCommand(self),
39             AddCommand(self), AddGlobCommand(self),
40             RemoveCommand(self), ApplyCommandStack(self),
41             FilterCommand(self), NoteFilterCommand(self),
42             ]
43
44
45 # Define common or complicated arguments
46
47 def current_playlist_callback(hooke, command, argument, value):
48     if value != None:
49         return value
50     playlist = hooke.playlists.current()
51     if playlist == None:
52         raise Failure('No playlists loaded')
53     return playlist
54
55 PlaylistArgument = Argument(
56     name='playlist', type='playlist', callback=current_playlist_callback,
57     help="""
58 :class:`hooke.playlist.Playlist` to act on.  Defaults to the current
59 playlist.
60 """.strip())
61
62 def playlist_name_callback(hooke, command, argument, value):
63     if value != None:
64         return value
65     i = 0
66     names = [p.name for p in hooke.playlists]
67     while True:
68         name = 'playlist-%d' % i
69         if name not in names:
70             return name
71         i += 1
72
73 PlaylistNameArgument = Argument(
74     name='output playlist', type='string', optional=True,
75     callback=playlist_name_callback,
76     help="""
77 Name of the new playlist (defaults to an auto-generated name).
78 """.strip())
79
80 def all_drivers_callback(hooke, command, argument, value):
81     return hooke.drivers
82
83
84 # Define useful command subclasses
85
86 class PlaylistCommand (Command):
87     """A :class:`~hooke.command.Command` operating on a
88     :class:`~hooke.playlist.Playlist`.
89     """
90     def __init__(self, **kwargs):
91         if 'arguments' in kwargs:
92             kwargs['arguments'].insert(0, PlaylistArgument)
93         else:
94             kwargs['arguments'] = [PlaylistArgument]
95         super(PlaylistCommand, self).__init__(**kwargs)
96
97     def _playlist(self, hooke, params):
98         """Get the selected playlist.
99
100         Notes
101         -----
102         `hooke` is intended to attach the selected playlist to the
103         local hooke instance; the returned playlist should not be
104         effected by the state of `hooke`.
105         """
106         # HACK? rely on params['playlist'] being bound to the local
107         # hooke (i.e. not a copy, as you would get by passing a
108         # playlist through the queue).  Ugh.  Stupid queues.  As an
109         # alternative, we could pass lookup information through the
110         # queue...
111         return params['playlist']
112
113
114 class PlaylistAddingCommand (Command):
115     """A :class:`~hooke.command.Command` adding a
116     :class:`~hooke.playlist.Playlist`.
117     """
118     def __init__(self, **kwargs):
119         if 'arguments' in kwargs:
120             kwargs['arguments'].insert(0, PlaylistNameArgument)
121         else:
122             kwargs['arguments'] = [PlaylistNameArgument]
123         super(PlaylistAddingCommand, self).__init__(**kwargs)
124
125     def _set_playlist(self, hooke, params, playlist):
126         """Attach a new playlist.
127         """
128         playlist.name = params['output playlist']
129         hooke.playlists.append(playlist)
130
131
132 # Define commands
133
134 class NextCommand (PlaylistCommand):
135     """Move playlist to the next curve.
136     """
137     def __init__(self, plugin):
138         super(NextCommand, self).__init__(
139             name='next curve', help=self.__doc__, plugin=plugin)
140
141     def _run(self, hooke, inqueue, outqueue, params):
142         self._playlist(hooke, params).next()
143
144
145 class PreviousCommand (PlaylistCommand):
146     """Move playlist to the previous curve.
147     """
148     def __init__(self, plugin):
149         super(PreviousCommand, self).__init__(
150             name='previous curve', help=self.__doc__, plugin=plugin)
151
152     def _run(self, hooke, inqueue, outqueue, params):
153         self._playlist(hooke, params).previous()
154
155
156 class JumpCommand (PlaylistCommand):
157     """Move playlist to a given curve.
158     """
159     def __init__(self, plugin):
160         super(JumpCommand, self).__init__(
161             name='jump to curve',
162             arguments=[
163                 Argument(name='index', type='int', optional=False, help="""
164 Index of target curve.
165 """.strip()),
166                 ],
167             help=self.__doc__, plugin=plugin)
168
169     def _run(self, hooke, inqueue, outqueue, params):
170         self._playlist(hooke, params).jump(params['index'])
171
172
173 class IndexCommand (PlaylistCommand):
174     """Print the index of the current curve.
175
176     The first curve has index 0.
177     """
178     def __init__(self, plugin):
179         super(IndexCommand, self).__init__(
180             name='curve index', help=self.__doc__, plugin=plugin)
181
182     def _run(self, hooke, inqueue, outqueue, params):
183         outqueue.put(self._playlist(hooke, params).index())
184
185
186 class GetCommand (PlaylistCommand):
187     """Return a :class:`hooke.playlist.Playlist`.
188     """
189     def __init__(self, plugin):
190         super(GetCommand, self).__init__(
191             name='get playlist', help=self.__doc__, plugin=plugin)
192
193     def _run(self, hooke, inqueue, outqueue, params):
194         outqueue.put(self._playlist(hooke, params))
195
196
197 class CurveListCommand (PlaylistCommand):
198     """Get the curves in a playlist.
199     """
200     def __init__(self, plugin):
201         super(CurveListCommand, self).__init__(
202             name='playlist curves', help=self.__doc__, plugin=plugin)
203
204     def _run(self, hooke, inqueue, outqueue, params):
205         outqueue.put(list(self._playlist(hooke, params)))
206
207
208 class SaveCommand (PlaylistCommand):
209     """Save a playlist.
210     """
211     def __init__(self, plugin):
212         super(SaveCommand, self).__init__(
213             name='save playlist',
214             arguments=[
215                 PlaylistArgument,
216                 Argument(name='output', type='file',
217                          help="""
218 File name for the output playlist.  Defaults to overwriting the input
219 playlist.  If the playlist does not have an input file (e.g. it was
220 created from scratch with 'new playlist'), this option is required.
221 """.strip()),
222                 ],
223             help=self.__doc__, plugin=plugin)
224
225     def _run(self, hooke, inqueue, outqueue, params):
226         self._playlist(hooke, params).save(params['output'])
227
228
229 class LoadCommand (PlaylistAddingCommand):
230     """Load a playlist.
231     """
232     def __init__(self, plugin):
233         super(LoadCommand, self).__init__(
234             name='load playlist',
235             arguments=[
236                 Argument(name='input', type='file', optional=False,
237                          help="""
238 File name for the input playlist.
239 """.strip()),
240                 Argument(name='drivers', type='driver', optional=True,
241                          count=-1, callback=all_drivers_callback,
242                          help="""
243 Drivers for loading curves.
244 """.strip()),
245                 ],
246             help=self.__doc__, plugin=plugin)
247
248     def _run(self, hooke, inqueue, outqueue, params):
249         p = FilePlaylist(drivers=params['drivers'], path=params['input'])
250         p.load(hooke=hooke)
251         self._set_playlist(hooke, params, p)
252         outqueue.put(p)
253
254
255 class AddCommand (PlaylistCommand):
256     """Add a curve to a playlist.
257     """
258     def __init__(self, plugin):
259         super(AddCommand, self).__init__(
260             name='add curve to playlist',
261             arguments=[
262                 Argument(name='input', type='file', optional=False,
263                          help="""
264 File name for the input :class:`hooke.curve.Curve`.
265 """.strip()),
266                 Argument(name='info', type='dict', optional=True,
267                          help="""
268 Additional information for the input :class:`hooke.curve.Curve`.
269 """.strip()),
270                 ],
271             help=self.__doc__, plugin=plugin)
272
273     def _run(self, hooke, inqueue, outqueue, params):
274         self._playlist(hooke, params).append_curve_by_path(
275             params['input'], params['info'], hooke=hooke)
276
277
278 class AddGlobCommand (PlaylistCommand):
279     """Add curves to a playlist with file globbing.
280
281     Adding lots of files one at a time can be tedious.  With this
282     command you can use globs (`data/curves/*.dat`) to add curves
283     for all matching files at once.
284     """
285     def __init__(self, plugin):
286         super(AddGlobCommand, self).__init__(
287             name='glob curves to playlist',
288             arguments=[
289                 Argument(name='input', type='string', optional=False,
290                          help="""
291 File name glob for the input :class:`hooke.curve.Curve`.
292 """.strip()),
293                 Argument(name='info', type='dict', optional=True,
294                          help="""
295 Additional information for the input :class:`hooke.curve.Curve`.
296 """.strip()),
297                 ],
298             help=self.__doc__, plugin=plugin)
299
300     def _run(self, hooke, inqueue, outqueue, params):
301         for path in sorted(glob.glob(params['input'])):
302             self._playlist(hooke, params).append_curve_by_path(
303                 path, params['info'], hooke=hooke)
304
305
306 class RemoveCommand (PlaylistCommand):
307     """Remove a curve from a playlist.
308     """
309     def __init__(self, plugin):
310         super(RemoveCommand, self).__init__(
311             name='remove curve from playlist',
312             arguments=[
313                 Argument(name='index', type='int', optional=False, help="""
314 Index of target curve.
315 """.strip()),
316                 ],
317             help=self.__doc__, plugin=plugin)
318
319     def _run(self, hooke, inqueue, outqueue, params):
320         self._playlist(hooke, params).pop(params['index'])
321         self._playlist(hooke, params).jump(params.index())
322
323
324 class ApplyCommandStack (PlaylistCommand):
325     """Apply a :class:`~hooke.command_stack.CommandStack` to each
326     curve in a playlist.
327
328     TODO: discuss `evaluate`.
329     """
330     def __init__(self, plugin):
331         super(ApplyCommandStack, self).__init__(
332             name='apply command stack',
333             arguments=[
334                 Argument(name='commands', type='command stack', optional=False,
335                          help="""
336 Command stack to apply to each curve.
337 """.strip()),
338                 Argument(name='evaluate', type='bool', default=False,
339                          help="""
340 Evaluate the applied command stack immediately.
341 """.strip()),
342                 ],
343             help=self.__doc__, plugin=plugin)
344
345     def _run(self, hooke, inqueue, outqueue, params):
346         if len(params['commands']) == 0:
347             return
348         p = self._playlist(hooke, params)
349         if params['evaluate'] == True:
350             for curve in p.items():
351                 for command in params['commands']:
352                     curve.command_stack.execute_command(hooke, command)
353                     curve.command_stack.append(command)
354         else:
355             for curve in p:
356                 curve.command_stack.extend(params['commands'])
357                 curve.unload()  # force command stack execution on next access.
358
359
360 class FilterCommand (PlaylistAddingCommand, PlaylistCommand):
361     """Create a subset playlist via a selection function.
362
363     Removing lots of curves one at a time can be tedious.  With this
364     command you can use a function `filter` to select the curves you
365     wish to keep.
366
367     Notes
368     -----
369     There are issues with pickling functions bound to class
370     attributes, because the pickle module doesn't know where those
371     functions were originally defined (where it should point the
372     loader).  Because of this, subclasses with hard-coded filter
373     functions are encouraged to define their filter function as a
374     method of their subclass.  See, for example,
375     :meth:`NoteFilterCommand.filter`.
376     """
377     def __init__(self, plugin, name='filter playlist'):
378         super(FilterCommand, self).__init__(
379             name=name, help=self.__doc__, plugin=plugin)
380         if not hasattr(self, 'filter'):
381             self.arguments.append(
382                 Argument(name='filter', type='function', optional=False,
383                          help="""
384 Function returning `True` for "good" curves.
385 `filter(curve, hooke, inqueue, outqueue, params) -> True/False`.
386 """.strip()))
387
388     def _run(self, hooke, inqueue, outqueue, params):
389         if not hasattr(self, 'filter'):
390             filter_fn = params['filter']
391         else:
392             filter_fn = self.filter
393         p = self._playlist(hooke, params).filter(filter_fn,
394             hooke=hooke, inqueue=inqueue, outqueue=outqueue, params=params)
395         p.name = params['name']
396         if hasattr(p, 'path') and p.path != None:
397             p.set_path(os.path.join(os.path.dirname(p.path), p.name))
398         self._set_playlist(hooke, params, p)
399         outqueue.put(p)
400
401
402 class NoteFilterCommand (FilterCommand):
403     """Create a subset playlist of curves with `.info['note'] != None`.
404     """
405     def __init__(self, plugin):
406         super(NoteFilterCommand, self).__init__(
407             plugin, name='note filter playlist')
408
409     def filter(self, curve, hooke, inqueue, outqueue, params):
410         return 'note' in curve.info and curve.info['note'] != None