X-Git-Url: http://git.tremily.us/?p=hooke.git;a=blobdiff_plain;f=hooke%2Fui%2Fgui%2Fpanel%2Fplaylist.py;h=2982744990dce7c182b6ec7255c1f5c09bcf227f;hp=022950d15f0b9852ed579439aa8e8cde2f9017c3;hb=a41d447c6f897c82b94bc6a962d57dc2652a2058;hpb=246feda8324f79709af3aa2619ae15844c80fa3d diff --git a/hooke/ui/gui/panel/playlist.py b/hooke/ui/gui/panel/playlist.py index 022950d..2982744 100644 --- a/hooke/ui/gui/panel/playlist.py +++ b/hooke/ui/gui/panel/playlist.py @@ -1,78 +1,286 @@ # Copyright """Playlist panel for Hooke. + +Provides a nice GUI interface to the +:class:`~hooke.plugin.playlist.PlaylistPlugin`. """ +import types + import wx -class Playlist(wx.Panel): +from ....util.callback import callback, in_callback - def __init__(self, parent): - # Use the WANTS_CHARS style so the panel doesn't eat the Return key. - wx.Panel.__init__(self, parent, -1, style=wx.WANTS_CHARS|wx.NO_BORDER, size=(160, 200)) - self.PlaylistsTree = wx.TreeCtrl(self, -1, wx.Point(0, 0), wx.Size(160, 250), wx.TR_DEFAULT_STYLE | wx.NO_BORDER | wx.TR_HIDE_ROOT) - imglist = wx.ImageList(16, 16, True, 2) - imglist.Add(wx.ArtProvider.GetBitmap(wx.ART_FOLDER, wx.ART_OTHER, wx.Size(16, 16))) - imglist.Add(wx.ArtProvider.GetBitmap(wx.ART_NORMAL_FILE, wx.ART_OTHER, wx.Size(16, 16))) - self.PlaylistsTree.AssignImageList(imglist) - self.PlaylistsTree.AddRoot('Playlists', 0) - self.PlaylistsTree.Bind(wx.EVT_RIGHT_DOWN , self.OnContextMenu) +class Menu (wx.Menu): + """Popup menu for selecting playlist :class:`Tree` actions. + """ + def __init__(self, on_delete, *args, **kwargs): + super(Menu, self).__init__(*args, **kwargs) + self._c = { + 'delete': self.Append(id=wx.ID_ANY, text='Delete'), + } + self.Bind(wx.EVT_MENU, on_delete) - self.Playlists = {} - sizer = wx.BoxSizer(wx.VERTICAL) - sizer.Add(self.PlaylistsTree, 1, wx.EXPAND) - self.SetSizer(sizer) - sizer.Fit(self) +class Tree (wx.TreeCtrl): + """:class:`wx.TreeCtrl` subclass handling playlist and curve selection. + """ + def __init__(self, config, callbacks, *args, **kwargs): + super(Tree, self).__init__(*args, **kwargs) + imglist = wx.ImageList(width=16, height=16, mask=True, initialCount=2) + imglist.Add(wx.ArtProvider.GetBitmap( + wx.ART_FOLDER, wx.ART_OTHER, wx.Size(16, 16))) + imglist.Add(wx.ArtProvider.GetBitmap( + wx.ART_NORMAL_FILE, wx.ART_OTHER, wx.Size(16, 16))) + self.AssignImageList(imglist) + self.image = { + 'root': 0, + 'playlist': 0, + 'curve': 1, + } + self._c = { + 'menu': Menu(self._on_delete), + 'root': self.AddRoot('Playlists', self.image['root']) + } + self.Bind(wx.EVT_RIGHT_DOWN, self._on_context_menu) + self.Bind(wx.EVT_TREE_SEL_CHANGED, self._on_curve_select) + self.Bind(wx.EVT_LEFT_DOWN, self._on_left_down) + self.Bind(wx.EVT_LEFT_DCLICK, self._on_left_doubleclick) + + self.config = config + self._callbacks = callbacks + self._setup_playlists() + + def _setup_playlists(self): + self._playlists = {} # {name: hooke.playlist.Playlist()} + + # In both of the following dicts, curve names are + # (playlist.name, curve.name) to avoid cross-playlist + # collisions. See ._is_curve(). + self._id_for_name = {} # {name: id} + self._name_for_id = {} # {id: name} + + def _name(self, name): + """Cleanup names according to configured preferences. + """ + if self.config['hide extensions'] == 'True': # HACK: config should decode + name,ext = os.path.splitext(name) + return name + + def _is_curve(self, name): # name from ._id_for_name / ._name_for_id + """Return `True` if `name` corresponds to a :class:`hooke.curve.Curve`. + """ + # Playlist names are strings, Curve names are tuples. + # See ._setup_playlists(). + return not isinstance(name, types.StringTypes) - def OnContextMenu(self, event): - hit_item, hit_flags = self.PlaylistsTree.HitTest(event.GetPosition()) + def _on_curve_select(self, event): + """Act on playlist/curve selection. + + Currently just a hook for a potential callback. + """ + _id = self.GetSelection() + name = self._name_for_id(_id) + if self._is_curve(name): + playlist = self._playlists[name[0]] + curve = playlist.current() + in_callback(self, playlist, curve) + + def _on_left_down(self, event): + """Select the clicked-on curve/playlist. + """ # TODO: dup with _on_curve_select? + hit_id, hit_flags = self.HitTest(event.GetPosition()) + if (hit_flags & wx.TREE_HITTEST_ONITEM) != 0: + name = self._name_for_id[hit_id] + if self._is_curve(name): + self.set_selected_curve(name[0], name[1]) + else: + self.set_selected_playlist(name) + event.Skip() + + def _on_left_doubleclick(self, event): + playlist.index = index + event.Skip() + + def _on_context_menu(self, event): + """Launch a popup :class:`Menu` with per-playlist/curve activities. + """ + hit_id,hit_flags = self.HitTest(event.GetPosition()) if (hit_flags & wx.TREE_HITTEST_ONITEM) != 0: - self.PlaylistsTree.SelectItem(hit_item) - # only do this part the first time so the events are only bound once - # Yet another alternate way to do IDs. Some prefer them up top to - # avoid clutter, some prefer them close to the object of interest - # for clarity. - if not hasattr(self, 'ID_popupAdd'): - #self.ID_popupAdd = wx.NewId() - self.ID_popupDelete = wx.NewId() - #self.Bind(wx.EVT_MENU, self.OnPopupAdd, id=self.ID_popupAdd) - self.Bind(wx.EVT_MENU, self.OnPopupDelete, id=self.ID_popupDelete) - # make a menu - menu = wx.Menu() - #items = [['Add', self.ID_popupAdd] , ['Delete', self.ID_popupDelete]] - items = [['Delete', self.ID_popupDelete]] - for item in items: - menu.Append(item[1], item[0]) - # Popup the menu. If an item is selected then its handler - # will be called before PopupMenu returns. - self.PopupMenu(menu) + self._hit_id = hit_id # store for the callbacks + self.PopupMenu( + Menu(self._on_delete), event.GetPoint()) menu.Destroy() - def OnPopupAdd(self, event): - pass + def _on_delete(self, event): + """Handler for :class:`Menu`'s `Delete` button. - def OnPopupDelete(self, event): - item = self.PlaylistsTree.GetSelection() - playlist = self.Parent.GetActivePlaylist() - if self.PlaylistsTree.ItemHasChildren(item): - playlist_name = self.PlaylistsTree.GetItemText(item) - notebook = self.Parent.plotNotebook - index = self.Parent._GetPlaylistTab(playlist_name) - notebook.SetSelection(index) - notebook.DeletePage(notebook.GetSelection()) - self.Parent.DeleteFromPlaylists(playlist_name) + Determines the clicked item and calls the appropriate + `.delete_*()` method on it. + """ + if hasattr(self, '_hit_id'): # called via ._c['menu'] + _id = self._hit_id + del(self._hit_id) + name = self._name_for_id[_id] + if self._is_curve(name): + self.delete_curve(playlist_name=name[0], name=name[1]) else: - if playlist is not None: - if playlist.count == 1: - notebook = self.Parent.plotNotebook - index = self.Parent._GetPlaylistTab(playlist.name) - notebook.SetSelection(index) - notebook.DeletePage(notebook.GetSelection()) - self.Parent.DeleteFromPlaylists(playlist.name) - else: - file_name = self.PlaylistsTree.GetItemText(item) - playlist.delete_file(file_name) - self.PlaylistsTree.Delete(item) - self.Parent.UpdatePlaylistsTreeSelection() + self.delete_playlist(name) + + def add_playlist(self, playlist): + """Add a :class:`hooke.playlist.Playlist` to the tree. + """ + if playlist.name not in self._playlists: + pass + else: + raise ValueError('duplicate playlist: %s' % playlist.name) + self._playlists[playlist.name] = playlist + p_id = self.AppendItem( + parent=self._c['root'], + text=self._name(playlist.name), + image=self.image['playlist']) + self._id_for_name[playlist.name] = p_id + + # temporarily disable any add_curve callbacks + acc = self._callbacks.get('add_curve', None) + self._callbacks['add_curve'] = None + + for curve in playlist: + self.add_curve(playlist.name, curve) + + # restore the add_curve callback + self._callbacks['add_curve'] = acc + + in_callback(self, playlist) + + def add_curve(self, playlist_name, curve): + """Add a :class:`hooke.curve.Curve` to a curently loaded playlist. + """ + p = self._playlists[playlist_name] + p.append(curve) + c_id = AppendItem( + parent=self._id_for_name[playlist_name], + text=self._name(curve.name), + image=self.image['curve']) + self._id_for_name[(playlist.name, curve.name)] = c_id + in_callback(self, p, curve) + + def delete_playlist(self, name): + """Delete a :class:`hooke.playlist.Playlist` by name. + """ + _id = self._id_for_name.pop(name) + self.Delete(_id) + playlist = self._playlists.pop(name) + del(self._name_for_id[_id]) + for curve in playlist: + _id = self._id_for_name.pop((name, curve.name)) + del(self._name_for_id[_id]) + in_callback(self, playlist) + + def delete_curve(self, playlist_name, name): + """Delete a :class:`hooke.curve.Curve` by name. + """ + if playlist is not None: + if playlist.count == 1: + notebook = self.Parent.plotNotebook + index = self.Parent._GetPlaylistTab(playlist.name) + notebook.SetSelection(index) + notebook.DeletePage(notebook.GetSelection()) + self.Parent.DeleteFromPlaylists(playlist.name) + else: + file_name = self.GetItemText(item) + playlist.delete_file(file_name) + self.Delete(item) + self.Parent.UpdatePlaylistsTreeSelection() + in_callback(self, playlist, curve) + + def get_selected_playlist(self): + """Return the selected :class:`hooke.playlist.Playlist`. + """ + _id = self.GetSelection() + name = self._name_for_id(_id) + if self._is_curve(name): + name = name[0] + return self._playlists[name] + + def get_selected_curve(self): + """Return the selected :class:`hooke.curve.Curve`. + """ + _id = self.GetSelection() + name = self._name_for_id(_id) + if self._is_curve(name): + p_name,c_name = name + playlist = self._playlists[p_name] + index = [i for i,c in enumerate(playlist) if c.name == c_name] + playlist.jump(index) + else: + playlist = self._playlists[name] + return playlist.current() + + def set_selected_playlist(self, name): + """Set the selected :class:`hooke.playlist.Playlist` by name. + """ + playlist = self._playlists[name] + curve = playlist.current() + self.set_selected_curve(playlist.name, curve.name) + + def set_selected_curve(self, playlist_name, name): + """Set the selected :class:`hooke.curve.Curve` by name. + """ + playlist = self._playlists[playlist.name] + for i,curve in enumerate(playlist): + if curve.name == name: + playlist.jump(i) + break + curve = playlist.current() + _id = self._id_for_name[(playlist.name, curve.name)] + self.Expand(self._id_for_name[playlist.name]) + self.SelectItem(_id) + in_callback(self, playlist, curve) # TODO: dup callback with _on_curve_select + + @callback + def generate_new_playlist(self): + pass + + def _GetUniquePlaylistName(self, name): + playlist_name = name + count = 1 + while playlist_name in self.playlists: + playlist_name = ''.join([name, str(count)]) + count += 1 + return playlist_name + + +class Playlist (wx.Panel): + """:class:`wx.Panel` subclass wrapper for :class:`Tree`. + """ + def __init__(self, config, callbacks, *args, **kwargs): + # Use the WANTS_CHARS style so the panel doesn't eat the Return key. + super(Playlist, self).__init__(*args, **kwargs) + self.name = 'playlist panel' + + self._c = { + 'tree': Tree( + config=config, + callbacks=callbacks, + parent=self, + size=wx.Size(160, 250), + style=wx.TR_DEFAULT_STYLE | wx.NO_BORDER | wx.TR_HIDE_ROOT), + } + + sizer = wx.BoxSizer(wx.VERTICAL) + sizer.Add(self._c['tree'], 1, wx.EXPAND) + self.SetSizer(sizer) + sizer.Fit(self) + + # Expose all Tree's public curve/playlist methods directly. + for attribute_name in dir(self._c['tree']): + if (attribute_name.startswith('_') + or 'playlist' not in attribute_name + or 'curve' not in attribute_name): + continue # not an attribute we're interested in + attr = getattr(self._c['tree'], attribute_name) + if hasattr(attr, '__call__'): # attr is a function / method + setattr(self, attribute_name, attr) # expose it