=== modified file 'xlgui/__init__.py' --- xlgui/__init__.py 2010-07-26 20:26:14 +0000 +++ xlgui/__init__.py 2010-08-10 22:30:44 +0000 @@ -61,7 +61,7 @@ @param exaile: The Exaile instance """ from xlgui import icons, main, panel, tray, progress - from xlgui.panel import collection, radio, playlists, files + from xlgui.panel import collection, radio, playlists, files, playlist2 gtk.gdk.set_program_class("Exaile") @@ -102,8 +102,9 @@ self.panels['playlists'] = playlists.PlaylistsPanel(self.main.window, exaile.playlists, exaile.smart_playlists, exaile.collection) self.panels['files'] = files.FilesPanel(self.main.window, exaile.collection) + self.panels['test'] = playlist2.TestPanel(self.main.window) - for panel in ('collection', 'radio', 'playlists', 'files'): + for panel in ('collection', 'radio', 'playlists', 'files', 'test'): self.add_panel(*self.panels[panel].get_panel()) # add the device panels === added file 'xlgui/panel/playlist2.py' --- xlgui/panel/playlist2.py 1970-01-01 00:00:00 +0000 +++ xlgui/panel/playlist2.py 2010-08-13 01:29:37 +0000 @@ -0,0 +1,352 @@ +# Copyright (C) 2008-2010 Adam Olsen +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2, or (at your option) +# any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +# +# +# The developers of the Exaile media player hereby grant permission +# for non-GPL compatible GStreamer and Exaile plugins to be used and +# distributed together with GStreamer and Exaile. This permission is +# above and beyond the permissions granted by the GPL license by which +# Exaile is covered. If you modify this code, you may extend this +# exception to your version of the code, but you are not obligated to +# do so. If you do not wish to do so, delete this exception statement +# from your version. + +import glib +import gobject +import gtk +import pango + +from xl import common, settings + +from xlgui import panel, icons + +import logging +logger = logging.getLogger(__name__) + +class BaseFolderTreeNode(gobject.GObject): + __gsignals__ = { + 'activated': ( + gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + tuple() + ), + 'children-changed': ( + gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + tuple() + ), + 'icon-changed': ( + gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + (gtk.gdk.Pixbuf,) + ), + 'title-changed': ( + gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + (str,) + ) + } + + def __init__(self, icon, title, data): + """ + icon: icon for the node + title: display name for the node + data: arbitrary data associated with the node + """ + gobject.GObject.__init__(self) + self._icon = icon + self._title = title + self.data = data + self.draggable = False + self.expand_by_default = False + self.context_menu = None + + def get_icon(self): + return self._icon + + def set_icon(self, icon): + self._icon = icon + self.emit('icon-changed', icon) + + icon = property(get_icon, set_icon) + + def get_title(self): + return self._title + + def set_title(self, title): + self._title = title + self.emit('title-changed', title) + + title = property(get_title, set_title) + + def has_children(self): + raise NotImplementedError + + def get_children(self): + raise NotImplementedError + + def activate(self): + self.emit('activated') + +class FolderTreeNode(BaseFolderTreeNode): + def __init__(self, icon, title, data): + BaseFolderTreeNode.__init__(self, icon, title, data) + self._children = [] + + def append_child(self, child): + self._children.append(child) + self.emit('children-changed') + + def insert_child(self, index, child): + self._children.insert(index, child) + self.emit('children-changed') + + def remove_child(self, child): + self._children.remove(child) + self.emit('children-changed') + + def has_children(self): + return len(self._children) > 0 + + def get_children(self): + return self._children[:] + +class TracksFolderTreeNode(FolderTreeNode): + def __init__(self, icon, title, data): + FolderTreeNode.__init__(self, icon, title, data) + + def get_tracks(self): + raise NotImplementedError + +class FolderTree(gtk.TreeView): + store_columns = (gtk.gdk.Pixbuf, gobject.TYPE_STRING, object) + def __init__(self): + gtk.TreeView.__init__(self) + self.model = None + self.root = FolderTreeNode(None, None, None) + self.rebuild() + + pb = gtk.CellRendererPixbuf() + cell = gtk.CellRendererText() + if settings.get_option('gui/ellipsize_text_in_panels', False): + cell.set_property('ellipsize-set', True) + cell.set_property('ellipsize', pango.ELLIPSIZE_END) + col = gtk.TreeViewColumn('Text') + col.pack_start(pb, False) + col.pack_start(cell, True) + col.set_attributes(pb, pixbuf=0) + col.set_attributes(cell, text=1) + self.append_column(col) + + self.set_headers_visible(False) + self.connect('row-expanded', self.on_row_expanded) + self.connect('row-activated', self.on_row_activated) + + gobject.add_emission_hook(BaseFolderTreeNode, 'title-changed', + self.on_node_title_changed) + gobject.add_emission_hook(BaseFolderTreeNode, 'icon-changed', + self.on_node_icon_changed) + gobject.add_emission_hook(BaseFolderTreeNode, 'children-changed', + self.on_node_children_changed) + + self.loading_icon = icons.MANAGER.pixbuf_from_icon_name('reload') + self.error_icon = icons.MANAGER.pixbuf_from_icon_name('error') + + def rebuild(self): + """ + Rebuilds the TreeStore from the root node. + """ + model = gtk.TreeStore(*self.store_columns) + self.model = model + self.set_model(model) + self._add_children(model, self.root, None) + + + def _add_children(self, model, node, parent): + """ + Adds the children of the given node to the TreeStore, + removing placeholders as needed. + + Generally should not be called directly - on_row_expanded + will call this automatically as needed. + """ + if parent: + parent_path = model.get_path(parent) + else: + parent_path = None + current_child = model.iter_children(parent) + if current_child is not None: + if model.get_value(current_child, 2) is not None: + raise Exception, "Cannot add children to node %s: it already has children." % repr(node) + model.set_value(current_child, 0, self.loading_icon) + model.set_value(current_child, 1, "Loading...") + model.set_value(current_child, 2, "Dummy string so we don't load twice") + + self.__add_children_getter(model, node, parent_path) + + @common.threaded + def __add_children_getter(self, model, node, parent_path): + try: + children = node.get_children() + except: + # TODO: allow get_children() to raise a particular type of + # exception that contains a message we can display to the + # user, in the same place where Loading... is. + common.log_exception(log=logger) + if parent_path: + parent = model.get_iter(parent_path) + else: + parent = None + current_child = model.iter_children(parent) + if current_child is not None: + model.set_value(current_child, 0, self.error_icon) + model.set_value(current_child, 1, "Failed to load.") + model.set_value(current_child, 2, None) + else: + glib.idle_add(self.__add_children, model, node, parent_path, children) + + def __add_children(self, model, node, parent_path, children): + if parent_path: + parent = model.get_iter(parent_path) + else: + parent = None + prev = None + remove_first = False + current_child = model.iter_children(parent) + if current_child is not None: + prev = current_child + remove_first = True + for child in children: + iter = model.insert_after(parent, prev, + row=[child.icon, child.title, child]) + if child.has_children(): + model.insert(iter, 0, row=[None, "", None]) + if child.expand_by_default: + self.expand_row(model.get_path(iter), open_all=False) + prev = iter + if remove_first: + model.remove(model.iter_children(parent)) + + def __get_node_iter_filter(self, model, path, iter, res): + if model.get_value(iter, 2) == res[0]: + res.append(iter) + return True + + # FIXME: this is really inefficient - if we keep parent refs in + # Nodes, then we can fix this to walk up the Node tree, then back + # down the Gtk tree, which should be rather faster on large trees. + def get_node_iter(self, node): + res = [node] + self.model.foreach(self.__get_node_iter_filter, res) + if len(res) == 2: + return res[1] + return None + + def on_node_title_changed(self, node, title): + iter = self.get_node_iter(node) + if iter: + self.model.set_value(iter, 1, title) + return True + + def on_node_icon_changed(self, node, icon): + iter = self.get_node_iter(node) + if iter: + self.model.set_value(iter, 0, icon) + return True + + def on_node_children_changed(self, node): + if node == self.root: + self.rebuild() + iter = self.get_node_iter(node) + if iter: + child = self.model.iter_children(iter) + if child is not None: + while self.model.remove(child): + pass + self._add_children(self.model, node, iter) + return True + + def on_row_expanded(self, view, iter, path): + """ + Called when a row is expanded in the tree, used to lazily + load additional nodes into the tree. + """ + model = self.model + child = model.iter_children(iter) + if model.get_value(child, 2) == None: + parent_node = model.get_value(iter, 2) + self._add_children(model, parent_node, iter) + + def on_row_activated(self, view, path, column): + """ + Called when a row is activated in the tree. + """ + iter = self.model.get_iter(path) + node = self.model.get_value(iter, 2) + if not isinstance(node, BaseFolderTreeNode): + return + node.activate() + + +class TestPanel(panel.Panel): + ui_info = ('playlists_panel.ui', 'PlaylistsPanelWindow') + def __init__(self, parent): + panel.Panel.__init__(self, parent) + self.box = self.builder.get_object('playlists_box') + self.tree = FolderTree() + + + foldericon = icons.MANAGER.pixbuf_from_icon_name('folder') + documenticon = icons.MANAGER.pixbuf_from_icon_name('txt') + t1 = FolderTreeNode(foldericon, "This is a test", None) + t1.expand_by_default = True + t1c1 = FolderTreeNode(documenticon, "Activate test.", None) + def activate_cb(node): + node.title = "Passed!" + node.icon = icons.MANAGER.pixbuf_from_icon_name('gtk-apply') + if node.get_children(): + node.remove_child(node.get_children()[0]) + else: + t1c1c1 = FolderTreeNode(documenticon, "Passed!", None) + node.append_child(t1c1c1) + t1c1.connect('activated', activate_cb) + t1.append_child(t1c1) + t1c2 = FolderTreeNode(foldericon, "Asyncronous Load Test", None) + t1c2c1 = FolderTreeNode(documenticon, "Passed!", None) + t1c2.append_child(t1c2c1) + getter = t1c2.get_children + def delayed_getter(*args): + import time + time.sleep(2) + return getter(*args) + t1c2.get_children = delayed_getter + t1.append_child(t1c2) + t1c3 = FolderTreeNode(foldericon, "Load Failure Test", None) + t1c3c1 = FolderTreeNode(documenticon, "Failed!", None) + t1c3.append_child(t1c3c1) + def error_getter(*args): + raise ValueError, "Test!" + t1c3.get_children = error_getter + t1.append_child(t1c3) + self.tree.root.append_child(t1) + t2 = FolderTreeNode(documenticon, "Test part 2", None) + self.tree.root.append_child(t2) + + + self.box.pack_start(self.tree) + self.box.show_all() + +