hooke.ui.gui was getting complicated, so I stripped it down for a moment.
[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         playlist.index = index\r
122         event.Skip()\r
123 \r
124     def _on_context_menu(self, event):\r
125         """Launch a popup :class:`Menu` with per-playlist/curve activities.\r
126         """\r
127         hit_id,hit_flags = self.HitTest(event.GetPosition())\r
128         if (hit_flags & wx.TREE_HITTEST_ONITEM) != 0:\r
129             self._hit_id = self._canonical_id(hit_id)  # store for callbacks\r
130             self.PopupMenu(\r
131                 Menu(self._on_delete), event.GetPoint())\r
132             menu.Destroy()\r
133 \r
134     def _on_delete(self, event):\r
135         """Handler for :class:`Menu`'s `Delete` button.\r
136 \r
137         Determines the clicked item and calls the appropriate\r
138         `.delete_*()` method on it.\r
139         """\r
140         #if hasattr(self, '_hit_id'):  # called via ._c['menu']\r
141         _id = self._hit_id\r
142         del(self._hit_id)\r
143         name = self._name_for_id[_id]\r
144         if self._is_curve(name):\r
145             self.delete_curve(playlist_name=name[0], name=name[1])\r
146         else:\r
147             self.delete_playlist(name)\r
148 \r
149     def add_playlist(self, playlist):\r
150         """Add a :class:`hooke.playlist.Playlist` to the tree.\r
151         """\r
152         if playlist.name not in self._playlists:\r
153             pass\r
154         else:\r
155             raise ValueError('duplicate playlist: %s' % playlist.name)\r
156         self._playlists[playlist.name] = playlist\r
157         p_id = self.AppendItem(\r
158             parent=self._c['root'],\r
159             text=self._name(playlist.name),\r
160             image=self.image['playlist'])\r
161         self._id_for_name[playlist.name] = p_id\r
162 \r
163         # temporarily disable any add_curve callbacks\r
164         acc = self._callbacks.get('add_curve', None)\r
165         self._callbacks['add_curve'] = None\r
166 \r
167         for curve in playlist:\r
168             self.add_curve(playlist.name, curve)\r
169 \r
170         # restore the add_curve callback\r
171         self._callbacks['add_curve'] = acc\r
172 \r
173         in_callback(self, playlist)\r
174 \r
175     def add_curve(self, playlist_name, curve):\r
176         """Add a :class:`hooke.curve.Curve` to a curently loaded playlist.\r
177         """\r
178         p = self._playlists[playlist_name]\r
179         p.append(curve)\r
180         c_id = AppendItem(\r
181             parent=self._id_for_name[playlist_name],\r
182             text=self._name(curve.name),\r
183             image=self.image['curve'])\r
184         self._id_for_name[(playlist.name, curve.name)] = c_id\r
185         in_callback(self, p, curve)\r
186 \r
187     def delete_playlist(self, name):\r
188         """Delete a :class:`hooke.playlist.Playlist` by name.\r
189         """\r
190         _id = self._id_for_name.pop(name)\r
191         self.Delete(_id)\r
192         playlist = self._playlists.pop(name)\r
193         del(self._name_for_id[_id])\r
194         for curve in playlist:\r
195             _id = self._id_for_name.pop((name, curve.name))\r
196             del(self._name_for_id[_id])\r
197         in_callback(self, playlist)\r
198 \r
199     def delete_curve(self, playlist_name, name):\r
200         """Delete a :class:`hooke.curve.Curve` by name.\r
201         """\r
202         if playlist is not None:\r
203             if playlist.count == 1:\r
204                 notebook = self.Parent.plotNotebook\r
205                 index = self.Parent._GetPlaylistTab(playlist.name)\r
206                 notebook.SetSelection(index)\r
207                 notebook.DeletePage(notebook.GetSelection())\r
208                 self.Parent.DeleteFromPlaylists(playlist.name)\r
209             else:\r
210                 file_name = self.GetItemText(item)\r
211                 playlist.delete_file(file_name)\r
212                 self.Delete(item)\r
213                 self.Parent.UpdatePlaylistsTreeSelection()\r
214         in_callback(self, playlist, curve)\r
215 \r
216     def get_selected_playlist(self):\r
217         """Return the selected :class:`hooke.playlist.Playlist`.\r
218         """\r
219         _id = self.GetSelection()\r
220         name = self._name_for_id[self._canonical_id(_id)]\r
221         if self._is_curve(name):\r
222             name = name[0]\r
223         return self._playlists[name]\r
224 \r
225     def get_selected_curve(self):\r
226         """Return the selected :class:`hooke.curve.Curve`.\r
227         """\r
228         _id = self.GetSelection()\r
229         name = self._name_for_id[self._canonical_id(_id)]\r
230         if self._is_curve(name):\r
231             p_name,c_name = name\r
232             playlist = self._playlists[p_name]\r
233             index = [i for i,c in enumerate(playlist) if c.name == c_name]\r
234             playlist.jump(index)\r
235         else:\r
236             playlist = self._playlists[name]\r
237         return playlist.current()\r
238 \r
239     def set_selected_playlist(self, name):\r
240         """Set the selected :class:`hooke.playlist.Playlist` by name.\r
241         """\r
242         playlist = self._playlists[name]\r
243         curve = playlist.current()\r
244         self.set_selected_curve(playlist.name, curve.name)\r
245 \r
246     def set_selected_curve(self, playlist_name, name):\r
247         """Set the selected :class:`hooke.curve.Curve` by name.\r
248         """\r
249         playlist = self._playlists[playlist.name]\r
250         for i,curve in enumerate(playlist):\r
251             if curve.name == name:\r
252                 playlist.jump(i)\r
253                 break\r
254         curve = playlist.current()\r
255         _id = self._id_for_name[(playlist.name, curve.name)]\r
256         self.Expand(self._id_for_name[playlist.name])\r
257         self.SelectItem(_id)\r
258         in_callback(self, playlist, curve) # TODO: dup callback with _on_curve_select\r
259 \r
260     @callback\r
261     def generate_new_playlist(self):\r
262         pass\r
263 \r
264     def _GetUniquePlaylistName(self, name):\r
265         playlist_name = name\r
266         count = 1\r
267         while playlist_name in self.playlists:\r
268             playlist_name = ''.join([name, str(count)])\r
269             count += 1\r
270         return playlist_name\r
271 \r
272 \r
273 class Playlist (Panel, wx.Panel):\r
274     """:class:`wx.Panel` subclass wrapper for :class:`Tree`.\r
275     """\r
276     def __init__(self, config, callbacks, *args, **kwargs):\r
277         # Use the WANTS_CHARS style so the panel doesn't eat the Return key.\r
278         super(Playlist, self).__init__(*args, **kwargs)\r
279         self.name = 'playlist panel'\r
280 \r
281         self._c = {\r
282             'tree': Tree(\r
283                 config=config,\r
284                 callbacks=callbacks,\r
285                 parent=self,\r
286                 size=wx.Size(160, 250),\r
287                 style=wx.TR_DEFAULT_STYLE | wx.NO_BORDER | wx.TR_HIDE_ROOT),\r
288             }\r
289 \r
290         sizer = wx.BoxSizer(wx.VERTICAL)\r
291         sizer.Add(self._c['tree'], 1, wx.EXPAND)\r
292         self.SetSizer(sizer)\r
293         sizer.Fit(self)\r
294 \r
295         # Expose all Tree's public curve/playlist methods directly.\r
296         # Following DRY and the LoD.\r
297         for attribute_name in dir(self._c['tree']):\r
298             if (attribute_name.startswith('_')\r
299                 or 'playlist' not in attribute_name\r
300                 or 'curve' not in attribute_name):\r
301                 continue  # not an attribute we're interested in\r
302             attr = getattr(self._c['tree'], attribute_name)\r
303             if hasattr(attr, '__call__'):  # attr is a function / method\r
304                 setattr(self, attribute_name, attr)  # expose it\r