524b0068a255bcaf2c8e73c478443e3d661b7e14
[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 \r
15 \r
16 class Menu (wx.Menu):\r
17     """Popup menu for selecting playlist :class:`Tree` actions.\r
18     """\r
19     def __init__(self, on_delete, *args, **kwargs):\r
20         super(Menu, self).__init__(*args, **kwargs)\r
21         self._c = {\r
22             'delete': self.Append(id=wx.ID_ANY, text='Delete'),\r
23             }\r
24         self.Bind(wx.EVT_MENU, on_delete)\r
25 \r
26 \r
27 class Tree (wx.TreeCtrl):\r
28     """:class:`wx.TreeCtrl` subclass handling playlist and curve selection.\r
29     """\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
38         self.image = {\r
39             'root': 0,\r
40             'playlist': 0,\r
41             'curve': 1,\r
42             }\r
43         self._c = {\r
44             'menu': Menu(self._on_delete),\r
45             'root': self.AddRoot(text='Playlists', image=self.image['root'])\r
46             }\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
51 \r
52         self.config = config\r
53         self._callbacks = callbacks\r
54         self._setup_playlists()\r
55 \r
56     def _setup_playlists(self):\r
57         self._playlists = {}    # {name: hooke.playlist.Playlist()}\r
58 \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
64 \r
65     def _name(self, name):\r
66         """Cleanup names according to configured preferences.\r
67         """\r
68         if self.config['hide extensions'] == 'True':  # HACK: config should decode\r
69             name,ext = os.path.splitext(name)\r
70         return name\r
71 \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
74         """\r
75         # Playlist names are strings, Curve names are tuples.\r
76         # See ._setup_playlists().\r
77         return not isinstance(name, types.StringTypes)\r
78 \r
79     def _canonical_id(self, _id):\r
80         """Return a canonical form of `_id` suitable for accessing `._name_for_id`.\r
81 \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
89         """\r
90         for c_id in self._name_for_id.keys():\r
91             if c_id == _id:\r
92                 return c_id\r
93         return _id\r
94 \r
95     def _on_curve_select(self, event):\r
96         """Act on playlist/curve selection.\r
97 \r
98         Currently just a hook for a potential callback.\r
99         """\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
106 \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
115             else:\r
116                 self.set_selected_playlist(name)\r
117         event.Skip()\r
118 \r
119     def _on_left_doubleclick(self, event):\r
120         playlist.index = index\r
121         event.Skip()\r
122 \r
123     def _on_context_menu(self, event):\r
124         """Launch a popup :class:`Menu` with per-playlist/curve activities.\r
125         """\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
129             self.PopupMenu(\r
130                 Menu(self._on_delete), event.GetPoint())\r
131             menu.Destroy()\r
132 \r
133     def _on_delete(self, event):\r
134         """Handler for :class:`Menu`'s `Delete` button.\r
135 \r
136         Determines the clicked item and calls the appropriate\r
137         `.delete_*()` method on it.\r
138         """\r
139         #if hasattr(self, '_hit_id'):  # called via ._c['menu']\r
140         _id = self._hit_id\r
141         del(self._hit_id)\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
145         else:\r
146             self.delete_playlist(name)\r
147 \r
148     def add_playlist(self, playlist):\r
149         """Add a :class:`hooke.playlist.Playlist` to the tree.\r
150         """\r
151         if playlist.name not in self._playlists:\r
152             pass\r
153         else:\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
161 \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
165 \r
166         for curve in playlist:\r
167             self.add_curve(playlist.name, curve)\r
168 \r
169         # restore the add_curve callback\r
170         self._callbacks['add_curve'] = acc\r
171 \r
172         in_callback(self, playlist)\r
173 \r
174     def add_curve(self, playlist_name, curve):\r
175         """Add a :class:`hooke.curve.Curve` to a curently loaded playlist.\r
176         """\r
177         p = self._playlists[playlist_name]\r
178         p.append(curve)\r
179         c_id = AppendItem(\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
185 \r
186     def delete_playlist(self, name):\r
187         """Delete a :class:`hooke.playlist.Playlist` by name.\r
188         """\r
189         _id = self._id_for_name.pop(name)\r
190         self.Delete(_id)\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
197 \r
198     def delete_curve(self, playlist_name, name):\r
199         """Delete a :class:`hooke.curve.Curve` by name.\r
200         """\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
208             else:\r
209                 file_name = self.GetItemText(item)\r
210                 playlist.delete_file(file_name)\r
211                 self.Delete(item)\r
212                 self.Parent.UpdatePlaylistsTreeSelection()\r
213         in_callback(self, playlist, curve)\r
214 \r
215     def get_selected_playlist(self):\r
216         """Return the selected :class:`hooke.playlist.Playlist`.\r
217         """\r
218         _id = self.GetSelection()\r
219         name = self._name_for_id[self._canonical_id(_id)]\r
220         if self._is_curve(name):\r
221             name = name[0]\r
222         return self._playlists[name]\r
223 \r
224     def get_selected_curve(self):\r
225         """Return the selected :class:`hooke.curve.Curve`.\r
226         """\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
234         else:\r
235             playlist = self._playlists[name]\r
236         return playlist.current()\r
237 \r
238     def set_selected_playlist(self, name):\r
239         """Set the selected :class:`hooke.playlist.Playlist` by name.\r
240         """\r
241         playlist = self._playlists[name]\r
242         curve = playlist.current()\r
243         self.set_selected_curve(playlist.name, curve.name)\r
244 \r
245     def set_selected_curve(self, playlist_name, name):\r
246         """Set the selected :class:`hooke.curve.Curve` by name.\r
247         """\r
248         playlist = self._playlists[playlist.name]\r
249         for i,curve in enumerate(playlist):\r
250             if curve.name == name:\r
251                 playlist.jump(i)\r
252                 break\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
258 \r
259     @callback\r
260     def generate_new_playlist(self):\r
261         pass\r
262 \r
263     def _GetUniquePlaylistName(self, name):\r
264         playlist_name = name\r
265         count = 1\r
266         while playlist_name in self.playlists:\r
267             playlist_name = ''.join([name, str(count)])\r
268             count += 1\r
269         return playlist_name\r
270 \r
271 \r
272 class Playlist (wx.Panel):\r
273     """:class:`wx.Panel` subclass wrapper for :class:`Tree`.\r
274     """\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
279 \r
280         self._c = {\r
281             'tree': Tree(\r
282                 config=config,\r
283                 callbacks=callbacks,\r
284                 parent=self,\r
285                 size=wx.Size(160, 250),\r
286                 style=wx.TR_DEFAULT_STYLE | wx.NO_BORDER | wx.TR_HIDE_ROOT),\r
287             }\r
288 \r
289         sizer = wx.BoxSizer(wx.VERTICAL)\r
290         sizer.Add(self._c['tree'], 1, wx.EXPAND)\r
291         self.SetSizer(sizer)\r
292         sizer.Fit(self)\r
293 \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