3 """Playlist panel for Hooke.
\r
5 Provides a nice GUI interface to the
\r
6 :class:`~hooke.plugin.playlist.PlaylistPlugin`.
\r
13 from ....util.callback import callback, in_callback
\r
16 class Menu (wx.Menu):
\r
17 """Popup menu for selecting playlist :class:`Tree` actions.
\r
19 def __init__(self, on_delete, *args, **kwargs):
\r
20 super(Menu, self).__init__(*args, **kwargs)
\r
22 'delete': self.Append(id=wx.ID_ANY, text='Delete'),
\r
24 self.Bind(wx.EVT_MENU, on_delete)
\r
27 class Tree (wx.TreeCtrl):
\r
28 """:class:`wx.TreeCtrl` subclass handling playlist and curve selection.
\r
30 def __init__(self, config, callbacks, *args, **kwargs):
\r
31 super(Tree, self).__init__(*args, **kwargs)
\r
32 imglist = wx.ImageList(width=16, height=16, mask=True, initialCount=2)
\r
33 imglist.Add(wx.ArtProvider.GetBitmap(
\r
34 wx.ART_FOLDER, wx.ART_OTHER, wx.Size(16, 16)))
\r
35 imglist.Add(wx.ArtProvider.GetBitmap(
\r
36 wx.ART_NORMAL_FILE, wx.ART_OTHER, wx.Size(16, 16)))
\r
37 self.AssignImageList(imglist)
\r
44 'menu': Menu(self._on_delete),
\r
45 'root': self.AddRoot(text='Playlists', image=self.image['root'])
\r
47 self.Bind(wx.EVT_RIGHT_DOWN, self._on_context_menu)
\r
48 self.Bind(wx.EVT_TREE_SEL_CHANGED, self._on_curve_select)
\r
49 self.Bind(wx.EVT_LEFT_DOWN, self._on_left_down)
\r
50 self.Bind(wx.EVT_LEFT_DCLICK, self._on_left_doubleclick)
\r
52 self.config = config
\r
53 self._callbacks = callbacks
\r
54 self._setup_playlists()
\r
56 def _setup_playlists(self):
\r
57 self._playlists = {} # {name: hooke.playlist.Playlist()}
\r
59 # In both of the following dicts, curve names are
\r
60 # (playlist.name, curve.name) to avoid cross-playlist
\r
61 # collisions. See ._is_curve().
\r
62 self._id_for_name = {} # {name: id}
\r
63 self._name_for_id = {} # {id: name}
\r
65 def _name(self, name):
\r
66 """Cleanup names according to configured preferences.
\r
68 if self.config['hide extensions'] == 'True': # HACK: config should decode
\r
69 name,ext = os.path.splitext(name)
\r
72 def _is_curve(self, name): # name from ._id_for_name / ._name_for_id
\r
73 """Return `True` if `name` corresponds to a :class:`hooke.curve.Curve`.
\r
75 # Playlist names are strings, Curve names are tuples.
\r
76 # See ._setup_playlists().
\r
77 return not isinstance(name, types.StringTypes)
\r
79 def _canonical_id(self, _id):
\r
80 """Return a canonical form of `_id` suitable for accessing `._name_for_id`.
\r
82 For some reason, `.GetSelection()`, etc. return items that
\r
83 hash differently than the original `.AppendItem()`-returned
\r
84 IDs. This means that `._name_for_id[self.GetSelection()]`
\r
85 will raise `KeyError`, even if there is an id `X` in
\r
86 `._name_for_id` for which `X == self.GetSelection()` will
\r
87 return `True`. This method "canonicalizes" IDs so that the
\r
88 hashing is consistent.
\r
90 for c_id in self._name_for_id.keys():
\r
95 def _on_curve_select(self, event):
\r
96 """Act on playlist/curve selection.
\r
98 Currently just a hook for a potential callback.
\r
100 _id = self.GetSelection()
\r
101 name = self._name_for_id[self._canonical_id(_id)]
\r
102 if self._is_curve(name):
\r
103 playlist = self._playlists[name[0]]
\r
104 curve = playlist.current()
\r
105 in_callback(self, playlist, curve)
\r
107 def _on_left_down(self, event):
\r
108 """Select the clicked-on curve/playlist.
\r
109 """ # TODO: dup with _on_curve_select?
\r
110 hit_id, hit_flags = self.HitTest(event.GetPosition())
\r
111 if (hit_flags & wx.TREE_HITTEST_ONITEM) != 0:
\r
112 name = self._name_for_id[self._canonical_id(hit_id)]
\r
113 if self._is_curve(name):
\r
114 self.set_selected_curve(name[0], name[1])
\r
116 self.set_selected_playlist(name)
\r
119 def _on_left_doubleclick(self, event):
\r
120 playlist.index = index
\r
123 def _on_context_menu(self, event):
\r
124 """Launch a popup :class:`Menu` with per-playlist/curve activities.
\r
126 hit_id,hit_flags = self.HitTest(event.GetPosition())
\r
127 if (hit_flags & wx.TREE_HITTEST_ONITEM) != 0:
\r
128 self._hit_id = self._canonical_id(hit_id) # store for callbacks
\r
130 Menu(self._on_delete), event.GetPoint())
\r
133 def _on_delete(self, event):
\r
134 """Handler for :class:`Menu`'s `Delete` button.
\r
136 Determines the clicked item and calls the appropriate
\r
137 `.delete_*()` method on it.
\r
139 #if hasattr(self, '_hit_id'): # called via ._c['menu']
\r
142 name = self._name_for_id[_id]
\r
143 if self._is_curve(name):
\r
144 self.delete_curve(playlist_name=name[0], name=name[1])
\r
146 self.delete_playlist(name)
\r
148 def add_playlist(self, playlist):
\r
149 """Add a :class:`hooke.playlist.Playlist` to the tree.
\r
151 if playlist.name not in self._playlists:
\r
154 raise ValueError('duplicate playlist: %s' % playlist.name)
\r
155 self._playlists[playlist.name] = playlist
\r
156 p_id = self.AppendItem(
\r
157 parent=self._c['root'],
\r
158 text=self._name(playlist.name),
\r
159 image=self.image['playlist'])
\r
160 self._id_for_name[playlist.name] = p_id
\r
162 # temporarily disable any add_curve callbacks
\r
163 acc = self._callbacks.get('add_curve', None)
\r
164 self._callbacks['add_curve'] = None
\r
166 for curve in playlist:
\r
167 self.add_curve(playlist.name, curve)
\r
169 # restore the add_curve callback
\r
170 self._callbacks['add_curve'] = acc
\r
172 in_callback(self, playlist)
\r
174 def add_curve(self, playlist_name, curve):
\r
175 """Add a :class:`hooke.curve.Curve` to a curently loaded playlist.
\r
177 p = self._playlists[playlist_name]
\r
180 parent=self._id_for_name[playlist_name],
\r
181 text=self._name(curve.name),
\r
182 image=self.image['curve'])
\r
183 self._id_for_name[(playlist.name, curve.name)] = c_id
\r
184 in_callback(self, p, curve)
\r
186 def delete_playlist(self, name):
\r
187 """Delete a :class:`hooke.playlist.Playlist` by name.
\r
189 _id = self._id_for_name.pop(name)
\r
191 playlist = self._playlists.pop(name)
\r
192 del(self._name_for_id[_id])
\r
193 for curve in playlist:
\r
194 _id = self._id_for_name.pop((name, curve.name))
\r
195 del(self._name_for_id[_id])
\r
196 in_callback(self, playlist)
\r
198 def delete_curve(self, playlist_name, name):
\r
199 """Delete a :class:`hooke.curve.Curve` by name.
\r
201 if playlist is not None:
\r
202 if playlist.count == 1:
\r
203 notebook = self.Parent.plotNotebook
\r
204 index = self.Parent._GetPlaylistTab(playlist.name)
\r
205 notebook.SetSelection(index)
\r
206 notebook.DeletePage(notebook.GetSelection())
\r
207 self.Parent.DeleteFromPlaylists(playlist.name)
\r
209 file_name = self.GetItemText(item)
\r
210 playlist.delete_file(file_name)
\r
212 self.Parent.UpdatePlaylistsTreeSelection()
\r
213 in_callback(self, playlist, curve)
\r
215 def get_selected_playlist(self):
\r
216 """Return the selected :class:`hooke.playlist.Playlist`.
\r
218 _id = self.GetSelection()
\r
219 name = self._name_for_id[self._canonical_id(_id)]
\r
220 if self._is_curve(name):
\r
222 return self._playlists[name]
\r
224 def get_selected_curve(self):
\r
225 """Return the selected :class:`hooke.curve.Curve`.
\r
227 _id = self.GetSelection()
\r
228 name = self._name_for_id[self._canonical_id(_id)]
\r
229 if self._is_curve(name):
\r
230 p_name,c_name = name
\r
231 playlist = self._playlists[p_name]
\r
232 index = [i for i,c in enumerate(playlist) if c.name == c_name]
\r
233 playlist.jump(index)
\r
235 playlist = self._playlists[name]
\r
236 return playlist.current()
\r
238 def set_selected_playlist(self, name):
\r
239 """Set the selected :class:`hooke.playlist.Playlist` by name.
\r
241 playlist = self._playlists[name]
\r
242 curve = playlist.current()
\r
243 self.set_selected_curve(playlist.name, curve.name)
\r
245 def set_selected_curve(self, playlist_name, name):
\r
246 """Set the selected :class:`hooke.curve.Curve` by name.
\r
248 playlist = self._playlists[playlist.name]
\r
249 for i,curve in enumerate(playlist):
\r
250 if curve.name == name:
\r
253 curve = playlist.current()
\r
254 _id = self._id_for_name[(playlist.name, curve.name)]
\r
255 self.Expand(self._id_for_name[playlist.name])
\r
256 self.SelectItem(_id)
\r
257 in_callback(self, playlist, curve) # TODO: dup callback with _on_curve_select
\r
260 def generate_new_playlist(self):
\r
263 def _GetUniquePlaylistName(self, name):
\r
264 playlist_name = name
\r
266 while playlist_name in self.playlists:
\r
267 playlist_name = ''.join([name, str(count)])
\r
269 return playlist_name
\r
272 class Playlist (wx.Panel):
\r
273 """:class:`wx.Panel` subclass wrapper for :class:`Tree`.
\r
275 def __init__(self, config, callbacks, *args, **kwargs):
\r
276 # Use the WANTS_CHARS style so the panel doesn't eat the Return key.
\r
277 super(Playlist, self).__init__(*args, **kwargs)
\r
278 self.name = 'playlist panel'
\r
283 callbacks=callbacks,
\r
285 size=wx.Size(160, 250),
\r
286 style=wx.TR_DEFAULT_STYLE | wx.NO_BORDER | wx.TR_HIDE_ROOT),
\r
289 sizer = wx.BoxSizer(wx.VERTICAL)
\r
290 sizer.Add(self._c['tree'], 1, wx.EXPAND)
\r
291 self.SetSizer(sizer)
\r
294 # Expose all Tree's public curve/playlist methods directly.
\r
295 for attribute_name in dir(self._c['tree']):
\r
296 if (attribute_name.startswith('_')
\r
297 or 'playlist' not in attribute_name
\r
298 or 'curve' not in attribute_name):
\r
299 continue # not an attribute we're interested in
\r
300 attr = getattr(self._c['tree'], attribute_name)
\r
301 if hasattr(attr, '__call__'): # attr is a function / method
\r
302 setattr(self, attribute_name, attr) # expose it
\r