3 """Playlist panel for Hooke.
\r
5 Provides a nice GUI interface to the
\r
6 :class:`~hooke.plugin.playlist.PlaylistPlugin`.
\r
14 from ....util.callback import callback, in_callback
\r
18 class Menu (wx.Menu):
\r
19 """Popup menu for selecting playlist :class:`Tree` actions.
\r
21 def __init__(self, on_delete, *args, **kwargs):
\r
22 super(Menu, self).__init__(*args, **kwargs)
\r
24 'delete': self.Append(id=wx.ID_ANY, text='Delete'),
\r
26 self.Bind(wx.EVT_MENU, on_delete)
\r
29 class Tree (wx.TreeCtrl):
\r
30 """:class:`wx.TreeCtrl` subclass handling playlist and curve selection.
\r
32 def __init__(self, *args, **kwargs):
\r
33 self.log = logging.getLogger('hooke')
\r
34 self._panel = kwargs['parent']
\r
35 self._callbacks = self._panel._callbacks # TODO: CallbackClass.set_callback{,s}()
\r
36 super(Tree, self).__init__(*args, **kwargs)
\r
37 imglist = wx.ImageList(width=16, height=16, mask=True, initialCount=2)
\r
38 imglist.Add(wx.ArtProvider.GetBitmap(
\r
39 wx.ART_FOLDER, wx.ART_OTHER, wx.Size(16, 16)))
\r
40 imglist.Add(wx.ArtProvider.GetBitmap(
\r
41 wx.ART_NORMAL_FILE, wx.ART_OTHER, wx.Size(16, 16)))
\r
42 self.AssignImageList(imglist)
\r
49 'menu': Menu(self._on_delete),
\r
50 'root': self.AddRoot(text='Playlists', image=self.image['root'])
\r
52 self.Bind(wx.EVT_RIGHT_DOWN, self._on_context_menu)
\r
53 self.Bind(wx.EVT_TREE_SEL_CHANGED, self._on_select)
\r
55 self._setup_playlists()
\r
57 def _setup_playlists(self):
\r
58 self._playlists = {} # {name: hooke.playlist.Playlist()}
\r
60 # In both of the following dicts, curve names are
\r
61 # (playlist.name, curve.name) to avoid cross-playlist
\r
62 # collisions. See ._is_curve().
\r
63 self._id_for_name = {} # {name: id}
\r
64 self._name_for_id = {} # {id: name}
\r
66 def _is_curve(self, name): # name from ._id_for_name / ._name_for_id
\r
67 """Return `True` if `name` corresponds to a :class:`hooke.curve.Curve`.
\r
69 # Playlist names are strings, Curve names are tuples.
\r
70 # See ._setup_playlists().
\r
71 return not isinstance(name, types.StringTypes)
\r
73 def _canonical_id(self, _id):
\r
74 """Return a canonical form of `_id` suitable for accessing `._name_for_id`.
\r
76 For some reason, `.GetSelection()`, etc. return items that
\r
77 hash differently than the original `.AppendItem()`-returned
\r
78 IDs. This means that `._name_for_id[self.GetSelection()]`
\r
79 will raise `KeyError`, even if there is an id `X` in
\r
80 `._name_for_id` for which `X == self.GetSelection()` will
\r
81 return `True`. This method "canonicalizes" IDs so that the
\r
82 hashing is consistent.
\r
84 for c_id in self._name_for_id.keys():
\r
92 def _on_context_menu(self, event):
\r
93 """Launch a popup :class:`Menu` with per-playlist/curve activities.
\r
95 hit_id,hit_flags = self.HitTest(event.GetPosition())
\r
96 if (hit_flags & wx.TREE_HITTEST_ONITEM) != 0:
\r
97 self._hit_id = self._canonical_id(hit_id) # store for callbacks
\r
98 menu = Menu(self._on_delete)
\r
99 self.PopupMenu(menu, event.GetPosition())
\r
103 # add_* called directly by HookeFrame
\r
104 # _add_* called on every addition
\r
106 def add_playlist(self, playlist):
\r
107 """Add a :class:`hooke.playlist.Playlist` to the tree.
\r
109 Calls :meth:`_add_playlist` and triggers a callback.
\r
111 self._add_playlist(playlist)
\r
112 in_callback(self, playlist)
\r
114 def _add_playlist(self, playlist):
\r
115 """Add a class:`hooke.playlist.Playlist` to the tree.
\r
117 No callback triggered.
\r
119 if playlist.name not in self._playlists:
\r
122 raise ValueError('duplicate playlist: %s' % playlist.name)
\r
123 self._playlists[playlist.name] = playlist
\r
124 p_id = self.AppendItem(
\r
125 parent=self._c['root'],
\r
126 text=self._panel._hooke_frame._file_name(playlist.name),
\r
127 image=self.image['playlist'])
\r
128 self._id_for_name[playlist.name] = p_id
\r
129 self._name_for_id[p_id] = playlist.name
\r
130 for curve in playlist:
\r
131 self._add_curve(playlist.name, curve)
\r
133 def add_curve(self, playlist_name, curve):
\r
134 """Add a :class:`hooke.curve.Curve` to a curently loaded playlist.
\r
136 Calls :meth:`_add_curve` and triggers a callback.
\r
138 self._add_curve(playlist_name, curve)
\r
139 playlist = self._playlists[playlist_name]
\r
140 in_callback(self, playlist, curve)
\r
142 def _add_curve(self, playlist_name, curve):
\r
143 """Add a class:`hooke.curve.Curve` to the tree.
\r
145 No callback triggered.
\r
147 p = self._playlists[playlist_name]
\r
150 c_id = self.AppendItem(
\r
151 parent=self._id_for_name[playlist_name],
\r
152 text=self._panel._hooke_frame._file_name(curve.name),
\r
153 image=self.image['curve'])
\r
154 self._id_for_name[(p.name, curve.name)] = c_id
\r
155 self._name_for_id[c_id] = (p.name, curve.name)
\r
158 def generate_new_playlist(self):
\r
161 def _GetUniquePlaylistName(self, name): # TODO
\r
162 playlist_name = name
\r
164 while playlist_name in self.playlists:
\r
165 playlist_name = ''.join([name, str(count)])
\r
167 return playlist_name
\r
170 # delete_* called by _on_delete handler (user click) or HookeFrame
\r
171 # _delete_* called on every deletion
\r
173 def _on_delete(self, event):
\r
174 """Handler for :class:`Menu`'s `Delete` button.
\r
176 Determines the clicked item and calls the appropriate
\r
177 `.delete_*()` method on it.
\r
179 #if hasattr(self, '_hit_id'): # called via ._c['menu']
\r
182 name = self._name_for_id[_id]
\r
183 if self._is_curve(name):
\r
184 self.delete_curve(playlist_name=name[0], name=name[1])
\r
186 self.delete_playlist(name)
\r
188 def delete_playlist(self, name):
\r
189 """Delete a :class:`hooke.playlist.Playlist` by name.
\r
191 Called by the :meth:`_on_delete` handler.
\r
193 Removes the playlist and its curves from the tree, then calls
\r
194 :meth:`_delete_playlist`.
\r
196 _id = self._id_for_name[name]
\r
198 playlist = self._playlists[name]
\r
199 self._delete_playlist(playlist)
\r
200 in_callback(self, playlist)
\r
202 def _delete_playlist(self, playlist):
\r
203 """Adjust name/id caches for the playlist and its curves.
\r
205 Called on *every* playlist deletion.
\r
207 self._playlists.pop(playlist.name)
\r
208 _id = self._id_for_name.pop(playlist.name)
\r
209 del(self._name_for_id[_id])
\r
210 for curve in playlist:
\r
211 self._delete_curve(playlist, curve)
\r
212 in_callback(self, playlist)
\r
214 def delete_curve(self, playlist_name, name):
\r
215 """Delete a :class:`hooke.curve.Curve` by name.
\r
217 Called by the :meth:`_on_delete` handler.
\r
219 Removes the curve from the tree, then calls
\r
220 :meth:`_delete_curve`.
\r
222 _id = self._id_for_name[(playlist_name, name)]
\r
224 playlist = self._playlists[playlist_name]
\r
226 for i,c in enumerate(playlist):
\r
230 self._delete_curve(playlist, curve)
\r
231 in_callback(self, playlist, curve)
\r
233 def _delete_curve(self, playlist, curve):
\r
234 """Adjust name/id caches.
\r
236 Called on _every_ curve deletion.
\r
238 _id = self._id_for_name.pop((playlist.name, curve.name))
\r
239 del(self._name_for_id[_id])
\r
240 in_callback(self, playlist, curve)
\r
244 def get_selected_playlist(self):
\r
245 """Return the selected :class:`hooke.playlist.Playlist`.
\r
247 _id = self.GetSelection()
\r
249 _id = self._canonical_id(_id)
\r
250 except KeyError: # no playlist selected
\r
252 name = self._name_for_id[_id]
\r
253 if self._is_curve(name):
\r
255 return self._playlists[name]
\r
257 def get_selected_curve(self):
\r
258 """Return the selected :class:`hooke.curve.Curve`.
\r
260 _id = self.GetSelection()
\r
261 name = self._name_for_id[self._canonical_id(_id)]
\r
262 if self._is_curve(name):
\r
263 p_name,c_name = name
\r
264 playlist = self._playlists[p_name]
\r
265 c = playlist.current()
\r
266 assert c.name == c_name, '%s != %s' % (c.name, c_name)
\r
268 playlist = self._playlists[name]
\r
269 return playlist.current()
\r
271 # Set selection (via user interaction with this panel)
\r
273 # These are hooks for HookeFrame callbacks which will send
\r
274 # the results back via 'get curve' calling 'set_selected_curve'.
\r
276 def _on_select(self, event):
\r
277 """Select the clicked-on curve/playlist.
\r
279 _id = self.GetSelection()
\r
280 name = self._name_for_id[self._canonical_id(_id)]
\r
281 if self._is_curve(name):
\r
282 p_name,c_name = name
\r
283 self._on_set_selected_curve(p_name, c_name)
\r
285 self._on_set_selected_playlist(name)
\r
287 def _on_set_selected_playlist(self, name):
\r
288 in_callback(self, self._playlists[name])
\r
290 def _on_set_selected_curve(self, playlist_name, name):
\r
291 playlist = self._playlists[playlist_name]
\r
293 for i,c in enumerate(playlist):
\r
298 raise ValueError(name)
\r
299 in_callback(self, playlist, curve)
\r
301 # Set selection (from the HookeFrame)
\r
303 def set_selected_curve(self, playlist, curve):
\r
304 """Make the curve the playlist's current curve.
\r
306 self.log.debug('playlist tree expanding %s' % playlist.name)
\r
307 self.Expand(self._id_for_name[playlist.name])
\r
308 self.Unbind(wx.EVT_TREE_SEL_CHANGED)
\r
309 self.log.debug('playlist tree selecting %s' % curve.name)
\r
310 self.SelectItem(self._id_for_name[(playlist.name, curve.name)])
\r
311 self.Bind(wx.EVT_TREE_SEL_CHANGED, self._on_select)
\r
313 def update_playlist(self, playlist):
\r
314 """Absorb changed `._index`, etc.
\r
316 self._playlists[playlist.name] = playlist
\r
319 class Playlist (Panel, wx.Panel):
\r
320 """:class:`wx.Panel` subclass wrapper for :class:`Tree`.
\r
322 def __init__(self, callbacks=None, **kwargs):
\r
323 # Use the WANTS_CHARS style so the panel doesn't eat the Return key.
\r
324 super(Playlist, self).__init__(
\r
325 name='playlist', callbacks=callbacks, **kwargs)
\r
329 size=wx.Size(160, 250),
\r
330 style=wx.TR_DEFAULT_STYLE | wx.NO_BORDER | wx.TR_HIDE_ROOT),
\r
333 sizer = wx.BoxSizer(wx.VERTICAL)
\r
334 sizer.Add(self._c['tree'], 1, wx.EXPAND)
\r
335 self.SetSizer(sizer)
\r
338 # Expose all Tree's public curve/playlist methods directly.
\r
339 # Following DRY and the LoD.
\r
340 for attribute_name in dir(self._c['tree']):
\r
341 if (attribute_name.startswith('_')
\r
342 or 'playlist' not in attribute_name
\r
343 or 'curve' not in attribute_name):
\r
344 continue # not an attribute we're interested in
\r
345 attr = getattr(self._c['tree'], attribute_name)
\r
346 if hasattr(attr, '__call__'): # attr is a function / method
\r
347 setattr(self, attribute_name, attr) # expose it
\r