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