Restored the playlist panel + cleanups now that I can load stuff into it ;).
[hooke.git] / hooke / ui / gui / panel / playlist.py
1 # Copyright\r
2 \r
3 """Playlist panel for Hooke.\r
4 \r
5 Provides a nice GUI interface to the\r
6 :class:`~hooke.plugin.playlist.PlaylistPlugin`.\r
7 """\r
8 \r
9 import types\r
10 \r
11 import wx\r
12 \r
13 from ....util.callback import callback, in_callback\r
14 from . import Panel\r
15 \r
16 \r
17 class Menu (wx.Menu):\r
18     """Popup menu for selecting playlist :class:`Tree` actions.\r
19     """\r
20     def __init__(self, on_delete, *args, **kwargs):\r
21         super(Menu, self).__init__(*args, **kwargs)\r
22         self._c = {\r
23             'delete': self.Append(id=wx.ID_ANY, text='Delete'),\r
24             }\r
25         self.Bind(wx.EVT_MENU, on_delete)\r
26 \r
27 \r
28 class Tree (wx.TreeCtrl):\r
29     """:class:`wx.TreeCtrl` subclass handling playlist and curve selection.\r
30     """\r
31     def __init__(self, config, callbacks, *args, **kwargs):\r
32         super(Tree, self).__init__(*args, **kwargs)\r
33         imglist = wx.ImageList(width=16, height=16, mask=True, initialCount=2)\r
34         imglist.Add(wx.ArtProvider.GetBitmap(\r
35                 wx.ART_FOLDER, wx.ART_OTHER, wx.Size(16, 16)))\r
36         imglist.Add(wx.ArtProvider.GetBitmap(\r
37                 wx.ART_NORMAL_FILE, wx.ART_OTHER, wx.Size(16, 16)))\r
38         self.AssignImageList(imglist)\r
39         self.image = {\r
40             'root': 0,\r
41             'playlist': 0,\r
42             'curve': 1,\r
43             }\r
44         self._c = {\r
45             'menu': Menu(self._on_delete),\r
46             'root': self.AddRoot(text='Playlists', image=self.image['root'])\r
47             }\r
48         self.Bind(wx.EVT_RIGHT_DOWN, self._on_context_menu)\r
49         self.Bind(wx.EVT_TREE_SEL_CHANGED, self._on_curve_select)\r
50         self.Bind(wx.EVT_LEFT_DOWN, self._on_left_down)\r
51         self.Bind(wx.EVT_LEFT_DCLICK, self._on_left_doubleclick)\r
52 \r
53         self.config = config\r
54         self._callbacks = callbacks\r
55         self._setup_playlists()\r
56 \r
57     def _setup_playlists(self):\r
58         self._playlists = {}    # {name: hooke.playlist.Playlist()}\r
59 \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
65 \r
66     def _name(self, name):\r
67         """Cleanup names according to configured preferences.\r
68         """\r
69         if self.config['hide extensions'] == 'True':  # HACK: config should decode\r
70             name,ext = os.path.splitext(name)\r
71         return name\r
72 \r
73     def _is_curve(self, name):  # name from ._id_for_name / ._name_for_id\r
74         """Return `True` if `name` corresponds to a :class:`hooke.curve.Curve`.\r
75         """\r
76         # Playlist names are strings, Curve names are tuples.\r
77         # See ._setup_playlists().\r
78         return not isinstance(name, types.StringTypes)\r
79 \r
80     def _canonical_id(self, _id):\r
81         """Return a canonical form of `_id` suitable for accessing `._name_for_id`.\r
82 \r
83         For some reason, `.GetSelection()`, etc. return items that\r
84         hash differently than the original `.AppendItem()`-returned\r
85         IDs.  This means that `._name_for_id[self.GetSelection()]`\r
86         will raise `KeyError`, even if there is an id `X` in\r
87         `._name_for_id` for which `X == self.GetSelection()` will\r
88         return `True`.  This method "canonicalizes" IDs so that the\r
89         hashing is consistent.\r
90         """\r
91         for c_id in self._name_for_id.keys():\r
92             if c_id == _id:\r
93                 return c_id\r
94         return _id\r
95 \r
96     def _on_curve_select(self, event):\r
97         """Act on playlist/curve selection.\r
98 \r
99         Currently just a hook for a potential callback.\r
100         """\r
101         _id = self.GetSelection()\r
102         name = self._name_for_id[self._canonical_id(_id)]\r
103         if self._is_curve(name):\r
104             playlist = self._playlists[name[0]]\r
105             curve = playlist.current()\r
106             in_callback(self, playlist, curve)\r
107 \r
108     def _on_left_down(self, event):\r
109         """Select the clicked-on curve/playlist.\r
110         """ # TODO: dup with _on_curve_select?\r
111         hit_id, hit_flags = self.HitTest(event.GetPosition())\r
112         if (hit_flags & wx.TREE_HITTEST_ONITEM) != 0:\r
113             name = self._name_for_id[self._canonical_id(hit_id)]\r
114             if self._is_curve(name):\r
115                 self.set_selected_curve(name[0], name[1])\r
116             else:\r
117                 self.set_selected_playlist(name)\r
118         event.Skip()\r
119 \r
120     def _on_left_doubleclick(self, event):\r
121         pass\r
122         #playlist.index = index\r
123         #event.Skip()\r
124 \r
125     def _on_context_menu(self, event):\r
126         """Launch a popup :class:`Menu` with per-playlist/curve activities.\r
127         """\r
128         hit_id,hit_flags = self.HitTest(event.GetPosition())\r
129         if (hit_flags & wx.TREE_HITTEST_ONITEM) != 0:\r
130             self._hit_id = self._canonical_id(hit_id)  # store for callbacks\r
131             menu = Menu(self._on_delete)\r
132             self.PopupMenu(menu, event.GetPosition())\r
133             menu.Destroy()\r
134 \r
135     def _on_delete(self, event):\r
136         """Handler for :class:`Menu`'s `Delete` button.\r
137 \r
138         Determines the clicked item and calls the appropriate\r
139         `.delete_*()` method on it.\r
140         """\r
141         #if hasattr(self, '_hit_id'):  # called via ._c['menu']\r
142         _id = self._hit_id\r
143         del(self._hit_id)\r
144         name = self._name_for_id[_id]\r
145         if self._is_curve(name):\r
146             self.delete_curve(playlist_name=name[0], name=name[1])\r
147         else:\r
148             self.delete_playlist(name)\r
149 \r
150     def add_playlist(self, playlist):\r
151         """Add a :class:`hooke.playlist.Playlist` to the tree.\r
152         """\r
153         if playlist.name not in self._playlists:\r
154             pass\r
155         else:\r
156             raise ValueError('duplicate playlist: %s' % playlist.name)\r
157         self._playlists[playlist.name] = playlist\r
158         p_id = self.AppendItem(\r
159             parent=self._c['root'],\r
160             text=self._name(playlist.name),\r
161             image=self.image['playlist'])\r
162         self._id_for_name[playlist.name] = p_id\r
163 \r
164         # temporarily disable any add_curve callbacks\r
165         acc = self._callbacks.get('add_curve', None)\r
166         self._callbacks['add_curve'] = None\r
167 \r
168         for curve in playlist:\r
169             self.add_curve(playlist.name, curve)\r
170 \r
171         # restore the add_curve callback\r
172         self._callbacks['add_curve'] = acc\r
173 \r
174         in_callback(self, playlist)\r
175 \r
176     def add_curve(self, playlist_name, curve):\r
177         """Add a :class:`hooke.curve.Curve` to a curently loaded playlist.\r
178         """\r
179         p = self._playlists[playlist_name]\r
180         if curve not in p:\r
181             p.append(curve)\r
182         c_id = self.AppendItem(\r
183             parent=self._id_for_name[playlist_name],\r
184             text=self._name(curve.name),\r
185             image=self.image['curve'])\r
186         self._id_for_name[(p.name, curve.name)] = c_id\r
187         in_callback(self, p, curve)\r
188 \r
189     def delete_playlist(self, name):\r
190         """Delete a :class:`hooke.playlist.Playlist` by name.\r
191         """\r
192         _id = self._id_for_name.pop(name)\r
193         self.Delete(_id)\r
194         playlist = self._playlists.pop(name)\r
195         del(self._name_for_id[_id])\r
196         for curve in playlist:\r
197             _id = self._id_for_name.pop((name, curve.name))\r
198             del(self._name_for_id[_id])\r
199         in_callback(self, playlist)\r
200 \r
201     def delete_curve(self, playlist_name, name):\r
202         """Delete a :class:`hooke.curve.Curve` by name.\r
203         """\r
204         if playlist is not None:\r
205             if playlist.count == 1:\r
206                 notebook = self.Parent.plotNotebook\r
207                 index = self.Parent._GetPlaylistTab(playlist.name)\r
208                 notebook.SetSelection(index)\r
209                 notebook.DeletePage(notebook.GetSelection())\r
210                 self.Parent.DeleteFromPlaylists(playlist.name)\r
211             else:\r
212                 file_name = self.GetItemText(item)\r
213                 playlist.delete_file(file_name)\r
214                 self.Delete(item)\r
215                 self.Parent.UpdatePlaylistsTreeSelection()\r
216         in_callback(self, playlist, curve)\r
217 \r
218     def get_selected_playlist(self):\r
219         """Return the selected :class:`hooke.playlist.Playlist`.\r
220         """\r
221         _id = self.GetSelection()\r
222         name = self._name_for_id[self._canonical_id(_id)]\r
223         if self._is_curve(name):\r
224             name = name[0]\r
225         return self._playlists[name]\r
226 \r
227     def get_selected_curve(self):\r
228         """Return the selected :class:`hooke.curve.Curve`.\r
229         """\r
230         _id = self.GetSelection()\r
231         name = self._name_for_id[self._canonical_id(_id)]\r
232         if self._is_curve(name):\r
233             p_name,c_name = name\r
234             playlist = self._playlists[p_name]\r
235             index = [i for i,c in enumerate(playlist) if c.name == c_name]\r
236             playlist.jump(index)\r
237         else:\r
238             playlist = self._playlists[name]\r
239         return playlist.current()\r
240 \r
241     def set_selected_playlist(self, name):\r
242         """Set the selected :class:`hooke.playlist.Playlist` by name.\r
243         """\r
244         playlist = self._playlists[name]\r
245         curve = playlist.current()\r
246         self.set_selected_curve(playlist.name, curve.name)\r
247 \r
248     def set_selected_curve(self, playlist_name, name):\r
249         """Set the selected :class:`hooke.curve.Curve` by name.\r
250         """\r
251         playlist = self._playlists[playlist.name]\r
252         for i,curve in enumerate(playlist):\r
253             if curve.name == name:\r
254                 playlist.jump(i)\r
255                 break\r
256         curve = playlist.current()\r
257         _id = self._id_for_name[(playlist.name, curve.name)]\r
258         self.Expand(self._id_for_name[playlist.name])\r
259         self.SelectItem(_id)\r
260         in_callback(self, playlist, curve) # TODO: dup callback with _on_curve_select\r
261 \r
262     @callback\r
263     def generate_new_playlist(self):\r
264         pass\r
265 \r
266     def _GetUniquePlaylistName(self, name):\r
267         playlist_name = name\r
268         count = 1\r
269         while playlist_name in self.playlists:\r
270             playlist_name = ''.join([name, str(count)])\r
271             count += 1\r
272         return playlist_name\r
273 \r
274 \r
275 class Playlist (Panel, wx.Panel):\r
276     """:class:`wx.Panel` subclass wrapper for :class:`Tree`.\r
277     """\r
278     def __init__(self, config, callbacks, *args, **kwargs):\r
279         # Use the WANTS_CHARS style so the panel doesn't eat the Return key.\r
280         super(Playlist, self).__init__(*args, **kwargs)\r
281         self.name = 'playlist panel'\r
282 \r
283         self._c = {\r
284             'tree': Tree(\r
285                 config=config,\r
286                 callbacks=callbacks,\r
287                 parent=self,\r
288                 size=wx.Size(160, 250),\r
289                 style=wx.TR_DEFAULT_STYLE | wx.NO_BORDER | wx.TR_HIDE_ROOT),\r
290             }\r
291 \r
292         sizer = wx.BoxSizer(wx.VERTICAL)\r
293         sizer.Add(self._c['tree'], 1, wx.EXPAND)\r
294         self.SetSizer(sizer)\r
295         sizer.Fit(self)\r
296 \r
297         # Expose all Tree's public curve/playlist methods directly.\r
298         # Following DRY and the LoD.\r
299         for attribute_name in dir(self._c['tree']):\r
300             if (attribute_name.startswith('_')\r
301                 or 'playlist' not in attribute_name\r
302                 or 'curve' not in attribute_name):\r
303                 continue  # not an attribute we're interested in\r
304             attr = getattr(self._c['tree'], attribute_name)\r
305             if hasattr(attr, '__call__'):  # attr is a function / method\r
306                 setattr(self, attribute_name, attr)  # expose it\r