liblarch-2.1.0/0000775000175000017500000000000012046771461013616 5ustar ploumploum00000000000000liblarch-2.1.0/AUTHORS0000664000175000017500000000034012003453426014652 0ustar ploumploum00000000000000AUTHORS ======== Liblarch team: -------------- * Lionel Dricot * Izidor Matušov Contributors: ------------- * Antonio Roquentin liblarch-2.1.0/PKG-INFO0000664000175000017500000000064712046771461014722 0ustar ploumploum00000000000000Metadata-Version: 1.0 Name: liblarch Version: 2.1.0 Summary: Liblarch is a python library built to easily handle data structures such as lists, trees and directed acyclic graphs and represent them as GTK TreeWidget or in other forms. Home-page: https://live.gnome.org/liblarch Author: Lionel Dricot & Izidor Matušov Author-email: gtg-contributors@lists.launchpad.net License: LGPLv3 Description: UNKNOWN Platform: UNKNOWN liblarch-2.1.0/liblarch/0000775000175000017500000000000012046771461015376 5ustar ploumploum00000000000000liblarch-2.1.0/liblarch/filters_bank.py0000664000175000017500000001027112040743752020410 0ustar ploumploum00000000000000# -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- # Liblarch - a library to handle directed acyclic graphs # Copyright (c) 2011-2012 - Lionel Dricot & Izidor Matušov # # This program is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, 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 Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . # ----------------------------------------------------------------------------- """ filters_bank stores all of GTG's filters in centralized place """ class Filter: def __init__(self, func, req): self.func = func self.dic = {} self.tree = req def set_parameters(self, dic): if dic: self.dic = dic def is_displayed(self, node_id): if self.tree.has_node(node_id): task = self.tree.get_node(node_id) else: return False if self.dic: value = self.func(task, parameters=self.dic) else: value = self.func(task) if 'negate' in self.dic and self.dic['negate']: value = not value return value def get_parameters(self, param): return self.dic.get(param, None) def is_flat(self): """ Should be the final list flat """ return self.get_parameters('flat') class FiltersBank: """ Stores filter objects in a centralized place. """ def __init__(self,tree): """ Create several stock filters: workview - Tasks that are active, workable, and started active - Tasks of status Active closed - Tasks of status closed or dismissed notag - Tasks with no tags """ self.tree = tree self.available_filters = {} self.custom_filters = {} ########################################## def get_filter(self,filter_name): """ Get the filter object for a given name """ if self.available_filters.has_key(filter_name): return self.available_filters[filter_name] elif self.custom_filters.has_key(filter_name): return self.custom_filters[filter_name] else: return None def has_filter(self,filter_name): return self.available_filters.has_key(filter_name) \ or self.custom_filters.has_key(filter_name) def list_filters(self): """ List, by name, all available filters """ liste = self.available_filters.keys() liste += self.custom_filters.keys() return liste def add_filter(self,filter_name,filter_func,parameters=None): """ Adds a filter to the filter bank Return True if the filter was added Return False if the filter_name was already in the bank """ if filter_name not in self.list_filters(): negate = False if filter_name.startswith('!'): negate = True filter_name = filter_name[1:] else: filter_obj = Filter(filter_func,self.tree) filter_obj.set_parameters(parameters) self.custom_filters[filter_name] = filter_obj return True else: return False def remove_filter(self,filter_name): """ Remove a filter from the bank. Only custom filters that were added here can be removed Return False if the filter was not removed """ if not self.available_filters.has_key(filter_name): if self.custom_filters.has_key(filter_name): self.custom_filters.pop(filter_name) return True else: return False else: return False liblarch-2.1.0/liblarch/__init__.py0000664000175000017500000001517512043230440017500 0ustar ploumploum00000000000000# -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- # Liblarch - a library to handle directed acyclic graphs # Copyright (c) 2011-2012 - Lionel Dricot & Izidor Matušov # # This program is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, 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 Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . # ----------------------------------------------------------------------------- import functools from liblarch.tree import MainTree from liblarch.treenode import _Node from liblarch.filteredtree import FilteredTree from liblarch.filters_bank import FiltersBank from liblarch.viewtree import ViewTree from liblarch.viewcount import ViewCount # API version of liblarch. # Your application is compatible if the major version number match liblarch's # one and if your minor version number is inferior to liblarch's one. # # The major number is incremented if an existing method is removed or modified # The minor number is incremented when a method is added to the API API = "2.1" def is_compatible(request): major, minor = [int(i) for i in request.split(".")] current_ma, current_mi = [int(i) for i in API.split(".")] return major == current_ma and minor <= current_mi class TreeNode(_Node): """ The public interface for TreeNode """ def __init__(self, node_id, parent=None): _Node.__init__(self,node_id,parent) def _set_tree(tree): print "_set_tree is not part of the API" class Tree: """ A thin wrapper to MainTree that adds filtering capabilities. It also provides a few methods to operate complex operation on the MainTree (e.g, move_node) """ def __init__(self): """ Creates MainTree which wraps and a main view without filters """ self.__tree = MainTree() self.__fbank = FiltersBank(self.__tree) self.__views = {} self.__viewscount = {} self.__views['main'] = ViewTree(self, self.__tree, self.__fbank, static=True) ##### HANDLE NODES ############################################################ def get_node(self, node_id): """ Returns the object of node. If the node does not exists, a ValueError is raised. """ return self.__tree.get_node(node_id) def has_node(self, node_id): """ Does the node exists in this tree? """ return self.__tree.has_node(node_id) def add_node(self, node, parent_id=None, priority="low"): """ Add a node to tree. If parent_id is set, put the node as a child of this node, otherwise put it as a child of the root node.""" self.__tree.add_node(node, parent_id, priority) def del_node(self, node_id, recursive=False): """ Remove node from tree and return whether it was successful or not """ return self.__tree.remove_node(node_id, recursive) def refresh_node(self, node_id, priority="low"): """ Send a request for updating the node """ self.__tree.modify_node(node_id, priority) def refresh_all(self): """ Refresh all nodes """ self.__tree.refresh_all() def move_node(self, node_id, new_parent_id=None): """ Move the node to a new parent (dismissing all other parents) use pid None to move it to the root """ if self.has_node(node_id): node = self.get_node(node_id) node.set_parent(new_parent_id) toreturn = True else: toreturn = False return toreturn def add_parent(self, node_id, new_parent_id=None): """ Add the node to a new parent. Return whether operation was successful or not. If the node does not exists, return False """ if self.has_node(node_id): node = self.get_node(node_id) return node.add_parent(new_parent_id) else: return False ##### VIEWS ################################################################### def get_main_view(self): """ Return the special view "main" which is without any filters on it.""" return self.__views['main'] def get_viewtree(self, name=None, refresh=True): """ Returns a viewtree by the name: * a viewtree with that name exists => return it * a viewtree with that name does not exist => create a new one and return it * name is None => create an anonymous tree (do not remember it) If refresh is False, the view is not initialized. This is useful as an optimization if you plan to apply a filter. """ if name is not None and self.__views.has_key(name): view_tree = self.__views[name] else: view_tree = ViewTree(self,self.__tree,self.__fbank, name = name, refresh = refresh) if name is not None: self.__views[name] = view_tree return view_tree def get_viewcount(self,name=None, refresh=True): if name is not None and self.__viewscount.has_key(name): view_count = self.__viewscount[name] else: view_count = ViewCount(self.__tree,self.__fbank, name = name, refresh = refresh) if name is not None: self.__viewscount[name] = view_count return view_count ##### FILTERS ################################################################## def list_filters(self): """ Return a list of all available filters by name """ return self.__fbank.list_filters() def add_filter(self, filter_name, filter_func, parameters=None): """ Adds a filter to the filter bank. @filter_name : name to give to the filter @filter_func : the function that will filter the nodes @parameters : some default parameters fot that filter Return True if the filter was added Return False if the filter_name was already in the bank """ return self.__fbank.add_filter(filter_name, filter_func, parameters) def remove_filter(self,filter_name): """ Remove a filter from the bank. Only custom filters that were added here can be removed. Return False if the filter was not removed. """ return self.__fbank.remove_filter(filter_name) liblarch-2.1.0/liblarch/viewtree.py0000664000175000017500000002702112040743752017600 0ustar ploumploum00000000000000# -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- # Liblarch - a library to handle directed acyclic graphs # Copyright (c) 2011-2012 - Lionel Dricot & Izidor Matušov # # This program is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, 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 Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . # ----------------------------------------------------------------------------- import functools from liblarch.tree import MainTree from liblarch.treenode import _Node from liblarch.filteredtree import FilteredTree from liblarch.filters_bank import FiltersBank # There should be two classes: for static and for dynamic mode # There are many conditions, and also we would prevent unallowed modes class ViewTree: def __init__(self, maininterface, maintree, filters_bank,\ name = None, refresh = True, static = False): """A ViewTree is the interface that should be used to display Tree(s). In static mode, FilteredTree layer is not created. (There is no need) We connect to MainTree or FilteredTree to get informed about changes. If FilteredTree is used, it is connected to MainTree to handle changes and then send id to ViewTree if it applies. @param maintree: a Tree object, cointaining all the nodes @param filters_bank: a FiltersBank object. Filters can be added dinamically to that. @param refresh: if True, this ViewTree is automatically refreshed after applying a filter. @param static: if True, this is the view of the complete maintree. Filters cannot be added to such a view. """ self.maininterface = maininterface self.__maintree = maintree self.__cllbcks = {} self.__fbank = filters_bank self.static = static if self.static: self._tree = self.__maintree self.__ft = None self.__maintree.register_callback('node-added', \ functools.partial(self.__emit, 'node-added')) self.__maintree.register_callback('node-deleted', \ functools.partial(self.__emit, 'node-deleted')) self.__maintree.register_callback('node-modified', \ functools.partial(self.__emit, 'node-modified')) else: self.__ft = FilteredTree(maintree, filters_bank, name = name, refresh = refresh) self._tree = self.__ft self.__ft.set_callback('added', \ functools.partial(self.__emit, 'node-added-inview')) self.__ft.set_callback('deleted', \ functools.partial(self.__emit, 'node-deleted-inview')) self.__ft.set_callback('modified', \ functools.partial(self.__emit, 'node-modified-inview')) self.__ft.set_callback('reordered', \ functools.partial(self.__emit, 'node-children-reordered')) def queue_action(self, node_id,func,param=None): self.__ft.set_callback('runonce',func,node_id=node_id,param=param) def get_basetree(self): """ Return Tree object """ return self.maininterface def register_cllbck(self, event, func): """ Store function and return unique key which can be used to unregister the callback later """ if not self.__cllbcks.has_key(event): self.__cllbcks[event] = {} callbacks = self.__cllbcks[event] key = 0 while callbacks.has_key(key): key += 1 callbacks[key] = func return key def deregister_cllbck(self, event, key): """ Remove the callback identifed by key (from register_cllbck) """ try: del self.__cllbcks[event][key] except KeyError: pass def __emit(self, event, node_id, path=None, neworder=None): """ Handle a new event from MainTree or FilteredTree by passing it to other objects, e.g. TreeWidget """ callbacks = dict(self.__cllbcks.get(event, {})) # print "ViewTree __emit for %s" %str(node_id) for func in callbacks.itervalues(): # print " -> func = %s - %s" %(func,str(path)) if neworder: func(node_id, path, neworder) else: func(node_id,path) def get_node(self, node_id): """ Get a node from MainTree """ return self.__maintree.get_node(node_id) #FIXME Remove this method from public interface def get_root(self): return self.__maintree.get_root() #FIXME Remove this method from public interface def refresh_all(self): self.__maintree.refresh_all() def get_current_state(self): """ Request current state to be send by signals/callbacks. This allow LibLarch widget to connect on fly (e.g. after FilteredTree is up and has some nodes). """ if self.static: self.__maintree.refresh_all() else: self.__ft.get_current_state() def print_tree(self, string=None): """ Print the shown tree, i.e. MainTree or FilteredTree """ return self._tree.print_tree(string) def get_all_nodes(self): """ Return list of node_id of displayed nodes """ return self._tree.get_all_nodes() def get_n_nodes(self, withfilters=[]): """ Returns quantity of displayed nodes in this tree @withfilters => Additional filters are applied before counting, i.e. the currently applied filters are also taken into account """ if not self.__ft: self.__ft = FilteredTree(self.__maintree, self.__fbank, refresh = True) return self.__ft.get_n_nodes(withfilters=withfilters) def get_nodes(self, withfilters=[]): """ Returns displayed nodes in this tree @withfilters => Additional filters are applied before counting, i.e. the currently applied filters are also taken into account """ if not self.__ft: self.__ft = FilteredTree(self.__maintree, self.__fbank, refresh = True) return self.__ft.get_nodes(withfilters=withfilters) def get_node_for_path(self, path): """ Convert path to node_id. I am not sure what this is for... """ return self._tree.get_node_for_path(path) def get_paths_for_node(self, node_id=None): """ If node_id is none, return root path *Almost* reverse function to get_node_for_path (1 node can have many paths, 1:M) """ return self._tree.get_paths_for_node(node_id) # FIXME change pid => parent_id def next_node(self, node_id, pid=None): """ Return the next node to node_id. @parent_id => identify which instance of node_id to work. If None, random instance is used """ return self._tree.next_node(node_id, pid) def node_has_child(self, node_id): """ Has the node at least one child? """ if self.static: return self.__maintree.get_node(node_id).has_child() else: return self.__ft.node_has_child(node_id) def node_all_children(self, node_id=None): """ Return children of a node """ if self.static: if not node_id or self.__maintree.has_node(node_id): return self.__maintree.get_node(node_id).get_children() else: return [] else: return self._tree.node_all_children(node_id) def node_n_children(self, node_id=None, recursive=False): """ Return quantity of children of node_id. If node_id is None, use the root node. Every instance of node has the same children""" if not self.__ft: self.__ft = FilteredTree(self.__maintree, self.__fbank, refresh = True) return self.__ft.node_n_children(node_id,recursive) def node_nth_child(self, node_id, n): """ Return nth child of the node. """ if self.static: if not node_id or node_id == 'root': node = self.__maintree.get_root() else: node = self.__maintree.get_node(node_id) if node and node.get_n_children() > n: return node.get_nth_child(n) else: raise ValueError("node %s has less than %s nodes" %(node_id, n)) else: realn = self.__ft.node_n_children(node_id) if realn <= n: raise ValueError("viewtree has %s nodes, no node %s" %(realn, n)) return self.__ft.node_nth_child(node_id, n) def node_has_parent(self, node_id): """ Has node parents? Is it child of root? """ return len(self.node_parents(node_id)) > 0 def node_parents(self, node_id): """ Returns displayed parents of the given node, or [] if there is no parent (such as if the node is a child of the virtual root), or if the parent is not displayable. Doesn't check wheter node node_id is displayed or not. (we only care about parents) """ if self.static: return self.__maintree.get_node(node_id).get_parents() else: return self.__ft.node_parents(node_id) def is_displayed(self, node_id): """ Is the node displayed? """ if self.static: return self.__maintree.has_node(node_id) else: return self.__ft.is_displayed(node_id) ####### FILTERS ############################################################### def list_applied_filters(self): return self.__ft.list_applied_filters() def apply_filter(self, filter_name, parameters=None, \ reset=False, refresh=True): """ Applies a new filter to the tree. @param filter_name: The name of an already registered filter to apply @param parameters: Optional parameters to pass to the filter @param reset : optional boolean. Should we remove other filters? @param refresh : should we refresh after applying this filter ? """ if self.static: raise Exception("WARNING: filters cannot be applied" + \ "to a static tree\n") self.__ft.apply_filter(filter_name, parameters, reset, refresh) def unapply_filter(self,filter_name,refresh=True): """ Removes a filter from the tree. @param filter_name: The name of filter to remove """ if self.static: raise Exception("WARNING: filters cannot be unapplied" +\ "from a static tree\n") self.__ft.unapply_filter(filter_name, refresh) def reset_filters(self, refresh=True): """ Remove all filters currently set on the tree. """ if self.static: raise Exception("WARNING: filters cannot be reset" +\ "on a static tree\n") else: self.__ft.reset_filters(refresh) liblarch-2.1.0/liblarch/viewcount.py0000664000175000017500000000740012041520566017765 0ustar ploumploum00000000000000# -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- # Liblarch - a library to handle directed acyclic graphs # Copyright (c) 2011-2012 - Lionel Dricot & Izidor Matušov # # This program is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, 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 Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . # --------------------------------------------------------------------------- class ViewCount: def __init__(self,tree,fbank, name = None, refresh = True): self.initialized = False self.ncount = {} self.tree = tree self.tree.register_callback("node-added", self.__modify) self.tree.register_callback("node-modified", self.__modify) self.tree.register_callback("node-deleted", self.__delete) self.fbank = fbank self.name = name self.applied_filters = [] self.nodes = [] self.cllbcks = [] if refresh: self.__refresh() def __refresh(self): for node in self.tree.get_all_nodes(): self.__modify(node) self.initialized = True def apply_filter(self,filter_name,refresh=True): if self.fbank.has_filter(filter_name): if filter_name not in self.applied_filters: self.applied_filters.append(filter_name) if refresh: #If we are not initialized, we need to refresh with all existing nodes if self.initialized: for n in list(self.nodes): self.__modify(n) else: self.__refresh() else: #FIXME: raise proper error print "There's no filter called %s" %filter_name def unapply_filter(self,filter_name): if filter_name in self.applied_filters: self.applied_filters.remove(filter_name) for node in self.tree.get_all_nodes(): self.__modify(node) #there's only one callback: "modified" def register_cllbck(self, func): if func not in self.cllbcks: self.cllbcks.append(func) def unregister_cllbck(self,func): if func in self.cllbacks: self.cllbacks.remove(func) def get_n_nodes(self): return len(self.nodes) #Allow external update of a given node def modify(self,nid): self.__modify(nid) def __modify(self,nid): displayed = True for filtname in self.applied_filters: filt = self.fbank.get_filter(filtname) displayed &= filt.is_displayed(nid) if displayed: self.__add(nid) else: self.__delete(nid) def __delete(self,nid): if nid in self.nodes: # print "node %s removed from viewcount %s"%(nid,self.name) self.nodes.remove(nid) self.__callback() def __add(self,nid): if nid not in self.nodes: # print "node %s added to viewcount %s"%(nid,self.name) self.nodes.append(nid) self.__callback() def __callback(self): for c in self.cllbcks: c() liblarch-2.1.0/liblarch/tree.py0000664000175000017500000003562112041500643016701 0ustar ploumploum00000000000000# -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- # Liblarch - a library to handle directed acyclic graphs # Copyright (c) 2011-2012 - Lionel Dricot & Izidor Matušov # # This program is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, 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 Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . # ----------------------------------------------------------------------------- import processqueue from liblarch.treenode import _Node class MainTree: """ Tree which stores and handle all requests """ def __init__(self): """ Initialize MainTree. @param root - the "root" node which contains all nodes """ self.nodes = {} self.pending_relationships = [] self.__cllbcks = {} self.root_id = 'root' self.root = _Node(self.root_id) _Node._set_tree(self.root,self) self._queue = processqueue.SyncQueue() def __str__(self): return "" % self.root def get_root(self): """ Return root node """ return self.root ####### INTERFACE FOR CALLBACKS ############################################### def register_callback(self, event, func): """ Store function and return unique key which can be used to unregister the callback later """ if not self.__cllbcks.has_key(event): self.__cllbcks[event] = {} callbacks = self.__cllbcks[event] key = 0 while callbacks.has_key(key): key += 1 callbacks[key] = func return key def deregister_callback(self, event, key): """ Remove the callback identifed by key (from register_cllbck) """ try: del self.__cllbcks[event][key] except KeyError: pass def _callback(self, event, node_id): """ Inform others about the event """ #We copy the dict to not loop on it while it could be modified dic = dict(self.__cllbcks.get(event, {})) for func in dic.itervalues(): func(node_id) ####### INTERFACE FOR HANDLING REQUESTS ####################################### def add_node(self, node, parent_id=None, priority="low"): self._queue.push(self._add_node, node, parent_id, priority=priority) def remove_node(self, node_id, recursive=False): self._queue.push(self._remove_node, node_id, recursive) def modify_node(self, node_id, priority="low"): self._queue.push(self._modify_node, node_id, priority=priority) def new_relationship(self, parent_id, child_id): self._queue.push(self._new_relationship, parent_id, child_id) def break_relationship(self, parent_id, child_id): self._queue.push(self._break_relationship, parent_id, child_id) def refresh_all(self): """ Refresh all nodes """ for node_id in self.nodes.keys(): self.modify_node(node_id) ####### IMPLEMENTATION OF HANDLING REQUESTS ################################### def _create_relationship(self, parent_id, child_id): """ Create relationship without any checks """ parent = self.nodes[parent_id] child = self.nodes[child_id] if child_id not in parent.children: parent.children.append(child_id) if parent_id not in child.parents: child.parents.append(parent_id) if child_id in self.root.children: self.root.children.remove(child_id) def _destroy_relationship(self, parent_id, child_id): """ Destroy relationship without any checks """ parent = self.nodes[parent_id] child = self.nodes[child_id] if child_id in parent.children: parent.children.remove(child_id) if parent_id in child.parents: child.parents.remove(parent_id) def _is_circular_relation(self, parent_id, child_id): """ Would the new relation be circular? Go over every possible ancestors. If one of them is child_id, this would be circular relation. """ visited = [] ancestors = [parent_id] while ancestors != []: node_id = ancestors.pop(0) if node_id == child_id: return True if node_id not in self.nodes: continue for ancestor_id in self.nodes[node_id].parents: if ancestor_id not in visited: ancestors.append(ancestor_id) return False def _add_node(self, node, parent_id): """ Add a node to the tree @param node - node to be added @param parent_id - parent to add or it will be add to root """ node_id = node.get_id() if node_id in self.nodes: print "Error: Node '%s' already exists" % node_id return False _Node._set_tree(node,self) for relationship in node.pending_relationships: if relationship not in self.pending_relationships: self.pending_relationships.append(relationship) node.pending_relationships = [] self.nodes[node_id] = node add_to_root = True parents_to_refresh = [] children_to_refresh = [] # Build pending relationships for rel_parent_id, rel_child_id in list(self.pending_relationships): # Adding as a child if rel_child_id == node_id and rel_parent_id in self.nodes: if not self._is_circular_relation(rel_parent_id, node_id): self._create_relationship(rel_parent_id, node_id) add_to_root = False parents_to_refresh.append(rel_parent_id) else: print "Error: Detected pending circular relationship", \ rel_parent_id, rel_child_id self.pending_relationships.remove((rel_parent_id, rel_child_id)) # Adding as a parent if rel_parent_id == node_id and rel_child_id in self.nodes: if not self._is_circular_relation(node_id, rel_child_id): self._create_relationship(node_id, rel_child_id) children_to_refresh.append(rel_child_id) else: print "Error: Detected pending circular relationship", \ rel_parent_id, rel_child_id self.pending_relationships.remove((rel_parent_id, rel_child_id)) # Build relationship with given parent if parent_id is not None: if self._is_circular_relation(parent_id, node_id): raise Exception('Creating circular relationship between %s and %s' % \ (parent_id, node_id)) if parent_id in self.nodes: self._create_relationship(parent_id, node_id) add_to_root = False parents_to_refresh.append(parent_id) else: self.pending_relationships.append((parent_id, node_id)) # Add at least to root if add_to_root: self.root.children.append(node_id) # Send callbacks #updating the parent and the children is handled by the FT self._callback("node-added", node_id) # #The following callback is only needed in case we have a # #Flat filter applied. # for parent_id in parents_to_refresh: # self._callback("node-modified", parent_id) #this callback is really fast. No problem # for child_id in children_to_refresh: # #FIXME: why parent_id? this should be a bug! # #removing this doesn't affect the tests. Why is it useful? # self._callback("node-modified", child_id) def _remove_node(self, node_id, recursive=False): """ Remove node from tree """ if node_id not in self.nodes: print "*** Warning *** Trying to remove a non-existing node" return # Do not remove root node if node_id is None: return # Remove pending relationships with this node for relation in list(self.pending_relationships): if node_id in relation: self.pending_relationships.remove(relation) node = self.nodes[node_id] # Handle parents for parent_id in node.parents: self._destroy_relationship(parent_id, node_id) self._callback('node-modified', parent_id) # Handle children for child_id in list(node.children): if recursive: self._remove_node(child_id, True) else: self._destroy_relationship(node_id, child_id) self._callback('node-modified', child_id) if self.nodes[child_id].parents == []: self.root.children.append(child_id) if node_id in self.root.children: self.root.children.remove(node_id) self.nodes.pop(node_id) self._callback('node-deleted', node_id) def _modify_node(self, node_id): """ Force update of a node """ if node_id != self.root_id and node_id in self.nodes: self._callback('node-modified', node_id) def _new_relationship(self, parent_id, child_id): """ Creates a new relationship This method is used mainly from TreeNode""" if (parent_id, child_id) in self.pending_relationships: self.pending_relationships.remove((parent_id, child_id)) if not parent_id or not child_id or parent_id == child_id: return False if parent_id not in self.nodes or child_id not in self.nodes: self.pending_relationships.append((parent_id, child_id)) return True if self._is_circular_relation(parent_id, child_id): self._destroy_relationship(parent_id, child_id) raise Exception('Cannot build circular relationship between %s and %s' % (parent_id, child_id)) self._create_relationship(parent_id, child_id) # Remove from root when having a new relationship if child_id in self.root.children: self.root.children.remove(child_id) self._callback('node-modified', parent_id) self._callback('node-modified', child_id) def _break_relationship(self, parent_id, child_id): """ Remove a relationship This method is used mainly from TreeNode """ for rel_parent, rel_child in list(self.pending_relationships): if rel_parent == parent_id and rel_child == child_id: self.pending_relationships.remove((rel_parent, rel_child)) if not parent_id or not child_id or parent_id == child_id: return False if parent_id not in self.nodes or child_id not in self.nodes: return False self._destroy_relationship(parent_id, child_id) # Move to root if beak the last parent if self.nodes[child_id].get_parents() == []: self.root.add_child(child_id) self._callback('node-modified', parent_id) self._callback('node-modified', child_id) ####### INTERFACE FOR READING STATE OF TREE ################################### def has_node(self, node_id): """ Is this node_id in this tree? """ return node_id in self.nodes def get_node(self, node_id=None): """ Return node of tree or root node of this tree """ if node_id in self.nodes: return self.nodes[node_id] elif node_id == self.root_id or node_id is None: return self.root else: raise ValueError("Node %s is not in the tree" % node_id) def get_node_for_path(self, path): """ Convert path into node_id @return node_id if path is valid, None otherwise """ if not path or path == (): return None node_id = path[-1] if path in self.get_paths_for_node(node_id): return node_id else: return None return node_id def get_paths_for_node(self, node_id): """ Get all paths for node_id """ if not node_id or node_id == self.root_id: return [()] elif node_id in self.nodes: node = self.nodes[node_id] if node.has_parent(): paths = [] for parent_id in node.get_parents(): if parent_id not in self.nodes: continue for path in self.get_paths_for_node(parent_id): paths.append(path + (node_id,)) return paths else: return [(node_id,)] else: raise ValueError("Cannot get path for non existing node %s" % node_id) def get_all_nodes(self): """ Return list of all nodes in this tree """ return self.nodes.keys() def next_node(self, node_id, parent_id=None): """ Return the next sibling node or None if there is none @param node_id - we look for siblings of this node @param parent_id - specify which siblings should be used, if task has more parents. If None, random parent will be used """ if node_id is None: raise ValueError('node_id should be different than None') node = self.get_node(node_id) parents_id = node.get_parents() if len(parents_id) == 0: parid = self.root_id elif parent_id in parents_id: parid = parent_id else: parid = parents_id[0] parent = self.get_node(parid) if not parent: raise ValueError('Parent does not exist') index = parent.get_child_index(node_id) if index == None: error = 'children are : %s\n' %parent.get_children() error += 'node %s is not a child of %s' %(node_id,parid) raise IndexError(error) if parent.get_n_children() > index+1: return parent.get_nth_child(index+1) else: return None def print_tree(self, string=False): output = self.root_id + "\n" stack = [(" ", child_id) for child_id in reversed(self.root.children)] while stack != []: prefix, node_id = stack.pop() output += prefix + node_id + "\n" prefix += " " for child_id in reversed(self.nodes[node_id].get_children()): stack.append((prefix, child_id)) if string: return output else: print output, liblarch-2.1.0/liblarch/processqueue.py0000664000175000017500000000762711750775537020520 0ustar ploumploum00000000000000# -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- # Liblarch - a library to handle directed acyclic graphs # Copyright (c) 2011-2012 - Lionel Dricot & Izidor Matušov # # This program is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, 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 Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . # ----------------------------------------------------------------------------- import threading import gobject class SyncQueue: """ Synchronized queue for processing requests""" def __init__(self): """ Initialize synchronized queue. @param callback - function for processing requests""" self._low_queue = [] self._queue = [] self._vip_queue = [] self._handler = None self._lock = threading.Lock() self._origin_thread = threading.current_thread() self.count = 0 def process_queue(self): """ Process requests from queue """ for action in self.process(): func = action[0] func(*action[1:]) # return True to process other requests as well return True def push(self, *element, **kwargs): """ Add a new element to the queue. Process actions from the same thread as the thread which created this queue immediately. What does it mean? When I use liblarch without threads, all actions are processed immediately. In GTG, this queue is created by the main thread which process GUI. When GUI callback is triggered, process those actions immediately because no protection is needed. However, requests from synchronization services are put in the queue. Application can choose which kind of priority should have an update. If the request is not in the queue of selected priority, add it and setup callback. """ if self._origin_thread == threading.current_thread(): func = element[0] func(*element[1:]) return priority = kwargs.get('priority') if priority == 'low': queue = self._low_queue elif priority == 'high': queue = self._vip_queue else: queue = self._queue self._lock.acquire() if element not in queue: queue.append(element) if self._handler is None: self._handler = gobject.idle_add(self.process_queue) self._lock.release() def process(self): """ Return elements to process At the moment, it returns just one element. In the future more elements may be better to return (to speed it up). If there is no request left, disable processing. """ self._lock.acquire() if len(self._vip_queue) > 0: toreturn = [self._vip_queue.pop(0)] elif len(self._queue) > 0: toreturn = [self._queue.pop(0)] elif len(self._low_queue) > 0: toreturn = [self._low_queue.pop(0)] else: toreturn = [] if len(self._queue) == 0 and len(self._vip_queue) == 0 and\ len(self._low_queue) == 0 and\ self._handler is not None: gobject.source_remove(self._handler) self._handler = None self._lock.release() return toreturn liblarch-2.1.0/liblarch/treenode.py0000664000175000017500000001677412041500624017556 0ustar ploumploum00000000000000# -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- # Liblarch - a library to handle directed acyclic graphs # Copyright (c) 2011-2012 - Lionel Dricot & Izidor Matušov # # This program is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, 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 Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . # ----------------------------------------------------------------------------- class _Node: """ Object just for a single node in Tree """ def __init__(self, node_id, parent=None): """ Initializes node @param node_id - unique identifier of node (str) @param parent - node_id of parent """ self.node_id = node_id self.parents_enabled = True self.children_enabled = True self.parents = [] self.children = [] self.tree = None self.pending_relationships = [] if parent: self.add_parent(parent) def __str__(self): return "" % (self.node_id) def get_id(self): """ Return node_id """ return self.node_id def modified(self, priority="low"): """ Force to update node (because it has changed) """ if self.tree: self.tree.modify_node(self.node_id, priority=priority) def _set_tree(self, tree): """ Set tree which is should contain this node. This method should be called only from MainTree. It is not part of public interface. """ self.tree = tree def get_tree(self): """ Return associated tree with this node """ return self.tree ####### Parents ############################################################### def set_parents_enabled(self,bol): if not bol: for p in self.get_parents(): self.remove_parent(p) self.parents_enabled = bol def has_parents_enabled(self): return self.parents_enabled def add_parent(self, parent_id): """ Add a new parent """ if parent_id != self.get_id() and self.parents_enabled \ and parent_id not in self.parents: if not self.tree: self.pending_relationships.append((parent_id, self.get_id())) elif not self.tree.has_node(parent_id): self.tree.pending_relationships.append((parent_id, self.get_id())) else: par = self.tree.get_node(parent_id) if par.has_children_enabled(): self.tree.new_relationship(parent_id, self.node_id) def set_parent(self, parent_id): """ Remove other parents and set this parent as only parent """ if parent_id != self.get_id() and self.parents_enabled: is_already_parent_flag = False if not self.tree: self.pending_relationships.append((parent_id, self.get_id())) elif not self.tree.has_node(parent_id): for p in self.get_parents(): self.tree.break_relationship(p,self.get_id()) self.tree.pending_relationships.append((parent_id, self.get_id())) else: par = self.tree.get_node(parent_id) if par.has_children_enabled(): #First we remove all the other parents for node_id in self.parents: if node_id != parent_id: self.remove_parent(node_id) else: is_already_parent_flag = True if parent_id and not is_already_parent_flag: self.add_parent(parent_id) def remove_parent(self, parent_id): """ Remove parent """ if self.parents_enabled and parent_id in self.parents: self.parents.remove(parent_id) self.tree.break_relationship(parent_id, self.node_id) def has_parent(self, parent_id=None): """ Has parent/parents? @param parent_id - None => has any parent? not None => has this parent? """ if self.parents_enabled: if parent_id: return self.tree.has_node(parent_id) and parent_id in self.parents else: return len(self.parents) > 0 else: return False def get_parents(self): """ Return parents of node """ parents = [] if self.parents_enabled and self.tree: for parent_id in self.parents: if self.tree.has_node(parent_id): parents.append(parent_id) return parents ####### Children ############################################################## def set_children_enabled(self,bol): if not bol: for c in self.get_children(): self.tree.break_relationship(self.get_id(),c) self.children_enabled = bol def has_children_enabled(self): return self.children_enabled def add_child(self, child_id): """ Add a children to node """ if self.children_enabled and child_id != self.get_id(): if child_id not in self.children: if not self.tree: self.pending_relationships.append((self.get_id(), child_id)) elif not self.tree.has_node(child_id): self.tree.pending_relationships.append((self.get_id(), child_id)) else: child = self.tree.get_node(child_id) if child.has_parents_enabled(): self.children.append(child_id) self.tree.new_relationship(self.node_id, child_id) else: print "%s was already in children of %s" % (child_id, self.node_id) def has_child(self, child_id=None): """ Has child/children? @param child_id - None => has any child? not None => has this child? """ if self.children_enabled: if child_id: return child_id in self.children else: return bool(self.children) else: return False def get_children(self): """ Return children of nodes """ children = [] if self.children_enabled and self.tree: for child_id in self.children: if self.tree.has_node(child_id): children.append(child_id) return children def get_n_children(self): """ Return count of children """ if self.children_enabled: return len(self.get_children()) else: return 0 def get_nth_child(self, index): """ Return nth child """ try: return self.children[index] except(IndexError): raise ValueError("Requested non-existing child") def get_child_index(self, node_id): if self.children_enabled and node_id in self.children: return self.children.index(node_id) else: return None liblarch-2.1.0/liblarch/filteredtree.py0000664000175000017500000006147012041462047020426 0ustar ploumploum00000000000000# -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- # Liblarch - a library to handle directed acyclic graphs # Copyright (c) 2011-2012 - Lionel Dricot & Izidor Matušov # # This program is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, 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 Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . # ----------------------------------------------------------------------------- import gobject class FilteredTree(): """ FilteredTree is the most important and also the most buggy part of LibLarch. FilteredTree transforms general changes in tree like creating/removing relationships between nodes and adding/updating/removing nodes into a serie of simple steps which can be for instance by GTK Widget. FilteredTree allows filtering - hiding certain nodes defined by a predicate. The reason of most bugs is that FilteredTree is request to update a node. FilteredTree must update its ancestors and also decestors. You cann't do that by a simple recursion. """ def __init__(self, tree, filtersbank, name=None, refresh=True): """ Construct a layer where filters could by applied @param tree: Original tree to filter. @param filtersbank: Filter bank which stores filters @param refresh: Requests all nodes in the beginning? Additional filters can be added and refresh can be done later _flat defines whether only nodes without children can be shown. For example WorkView filter. """ self.cllbcks = {} # Cache self.nodes = {} # Set root_id by the name of FilteredTree if name is None: self.root_id = "anonymous_root" else: self.root_id = "root_%s" % name self.nodes[self.root_id] = {'parents': [], 'children': []} self.cache_paths = {} self.filter_cache = {} # Connect to signals from MainTree self.tree = tree self.tree.register_callback("node-added", self.__external_modify) self.tree.register_callback("node-modified", self.__external_modify) self.tree.register_callback("node-deleted", self.__external_modify) # Filters self.__flat = False self.applied_filters = [] self.fbank = filtersbank if refresh: self.refilter() def set_callback(self, event, func,node_id=None, param=None): """ Register a callback for an event. It is possible to have just one callback for event. @param event: one of added, modified, deleted, reordered @param func: callback function """ if event == 'runonce': if not node_id: raise Exception('runonce callback should come with a node_id') if self.is_displayed(node_id): #it is essential to idle_add to avoid hard recursion gobject.idle_add(func,param) else: if not self.cllbcks.has_key(node_id): self.cllbcks[node_id] = [] self.cllbcks[node_id].append([func,node_id,param]) else: self.cllbcks[event] = [func,node_id,param] def callback(self, event, node_id, path, neworder=None): """ Run a callback. To call callback, the object must be initialized and function exists. @param event: one of added, modified, deleted, reordered, runonce @param node_id: node_id parameter for callback function @param path: path parameter for callback function @param neworder: neworder parameter for reorder callback function The runonce event is actually only run once, when a given task appears. """ if event == 'added': for func,nid,param in self.cllbcks.get(node_id,[]): if nid and self.is_displayed(nid): func(param) if self.cllbcks.has_key(node_id): self.cllbcks.pop(node_id) else: raise Exception('%s is not displayed but %s was added' %(nid,node_id)) func,nid,param = self.cllbcks.get(event, (None,None,None)) if func: if neworder: func(node_id,path,neworder) else: func(node_id,path) #### EXTERNAL MODIFICATION #################################################### def __external_modify(self, node_id): return self.__update_node(node_id,direction="both") def __update_node(self, node_id, direction): '''update the node node_id and propagate the change in direction (up|down|both) ''' if node_id == self.root_id: return None current_display = self.is_displayed(node_id) new_display = self.__is_displayed(node_id) for fcname in self.filter_cache: if node_id in self.filter_cache[fcname]['nodes']: self.filter_cache[fcname]['nodes'].remove(node_id) self.filter_cache[fcname]['count'] = len(self.filter_cache[fcname]['nodes']) completely_updated = True if not current_display and not new_display: # If a task is not displayed and should not be displayed, we # should still check its parent because he might not be aware # that he has a child if self.tree.has_node(node_id): node = self.tree.get_node(node_id) for parent in node.get_parents(): self.__update_node(parent,"up") return completely_updated elif not current_display and new_display: action = 'added' elif current_display and not new_display: action = 'deleted' else: action = 'modified' # Create node info for new node if action == 'added': self.nodes[node_id] = {'parents':[], 'children':[]} # Make sure parents are okay if we adding or updating if action == 'added' or action == 'modified': current_parents = self.nodes[node_id]['parents'] new_parents = self.__node_parents(node_id) # When using flat filter or a recursive filter, FilteredTree # might not recognize a parent correctly, make sure to check them if action == 'added': node = self.tree.get_node(node_id) for parent_id in node.get_parents(): if parent_id not in new_parents and parent_id not in current_parents: self.__update_node(parent_id, direction="up") # Refresh list of parents after doing checkup once again current_parents = self.nodes[node_id]['parents'] new_parents = self.__node_parents(node_id) self.nodes[node_id]['parents'] = [parent_id for parent_id in new_parents if parent_id in self.nodes] remove_from = list(set(current_parents) - set(new_parents)) add_to = list(set(new_parents) - set(current_parents)) stay = list(set(new_parents) - set(add_to)) #If we are updating a node at the root, we should take care #of the root too if direction == "down" and self.root_id in add_to: direction = "both" for parent_id in remove_from: self.send_remove_tree(node_id, parent_id) self.nodes[parent_id]['children'].remove(node_id) if direction == "both" or direction == "up": self.__update_node(parent_id,direction="up") #there might be some optimization here for parent_id in add_to: if parent_id in self.nodes: self.nodes[parent_id]['children'].append(node_id) self.send_add_tree(node_id, parent_id) if direction == "both" or direction == "up": self.__update_node(parent_id,direction="up") else: completely_updated = False raise Exception("We have a parent not in the ViewTree") #We update all the other parents if direction == "both" or direction == "up": for parent_id in stay: self.__update_node(parent_id,direction="up") #We update the node itself #Why should we call the callback only for modify? if action == 'modified': for path in self.get_paths_for_node(node_id): self.callback(action, node_id, path) #We update the children current_children = self.nodes[node_id]['children'] new_children = self.__node_children(node_id) if direction == "both" or direction == "down": for cid in new_children: if cid not in current_children: self.__update_node(cid,direction="down") elif action == 'deleted': paths = self.get_paths_for_node(node_id) children = list(reversed(self.nodes[node_id]['children'])) for child_id in children: self.send_remove_tree(child_id, node_id) self.nodes[child_id]['parents'].remove(node_id) self.__update_node(child_id,direction="down") for path in paths: self.callback(action, node_id, path) # Remove node from cache for parent_id in self.nodes[node_id]['parents']: self.nodes[parent_id]['children'].remove(node_id) self.__update_node(parent_id,direction="up") self.nodes.pop(node_id) # We update parents who are not displayed # If the node is only hidden and still exists in the tree if self.tree.has_node(node_id): node = self.tree.get_node(node_id) for parent in node.get_parents(): if parent not in self.nodes: self.__update_node(parent, direction="up") return completely_updated def send_add_tree(self, node_id, parent_id): paths = self.get_paths_for_node(parent_id) queue = [(node_id, (node_id, ))] while queue != []: node_id, relative_path = queue.pop(0) for start_path in paths: path = start_path + relative_path self.callback('added', node_id, path) for child_id in self.nodes[node_id]['children']: queue.append((child_id, relative_path + (child_id,))) def send_remove_tree(self, node_id, parent_id): paths = self.get_paths_for_node(parent_id) stack = [(node_id, (node_id, ), True)] while stack != []: node_id, relative_path, first_time = stack.pop() if first_time: stack.append((node_id, relative_path, False)) for child_id in self.nodes[node_id]['children']: stack.append((child_id, relative_path + (child_id,), True)) else: for start_path in paths: path = start_path + relative_path self.callback('deleted', node_id, path) def test_validity(self): for node_id in self.nodes: for parent_id in self.nodes[node_id]['parents']: assert node_id in self.nodes[parent_id]['children'], "Node '%s' is not in children of '%s'" % (node_id, parent_id) if self.nodes[node_id]['parents'] == []: assert node_id == self.root_id, "Node '%s' does not have parents" % (node_id) for parent_id in self.nodes[node_id]['children']: assert node_id in self.nodes[parent_id]['parents'], "Node '%s' is not in parents of '%s'" % (node_id, parent_id) #### OTHER #################################################################### def refilter(self): # Find out it there is at least one flat filter self.filter_cache = {} self.__flat = False for filter_name in self.applied_filters: filt = self.fbank.get_filter(filter_name) if filt and not self.__flat: self.__flat = filt.is_flat() # Clean the tree for node_id in reversed(self.nodes[self.root_id]['children']): self.send_remove_tree(node_id, self.root_id) self.nodes = {} self.nodes[self.root_id] = {'parents': [], 'children': []} # Build tree again root_node = self.tree.get_root() queue = root_node.get_children() while queue != []: node_id = queue.pop(0) # FIXME: decide which is the best direction self.__update_node(node_id, direction="both") node = self.tree.get_node(node_id) for child_id in node.get_children(): queue.append(child_id) def __is_displayed(self, node_id): """ Should be node displayed regardless of its current status? """ if node_id and self.tree.has_node(node_id): for filter_name in self.applied_filters: filt = self.fbank.get_filter(filter_name) if filt: can_be_displayed = filt.is_displayed(node_id) if not can_be_displayed: return False else: return False return True else: return False def is_displayed(self, node_id): """ Is the node displayed at the moment? """ return node_id in self.nodes def __node_children(self, node_id): if node_id == self.root_id: raise Exception("Requesting children for root node") if not self.__flat: if self.tree.has_node(node_id): node = self.tree.get_node(node_id) else: node = None else: node = None if not node: return [] toreturn = [] for child_id in node.get_children(): if self.__is_displayed(child_id): toreturn.append(child_id) return toreturn def __node_parents(self, node_id): """ Returns parents of the given node. If node has no parent or no displyed parent, return the virtual root. """ if node_id == self.root_id: raise ValueError("Requested a parent of the root node") parents_nodes = [] #we return only parents that are not root and displayed if not self.__flat and self.tree.has_node(node_id): node = self.tree.get_node(node_id) for parent_id in node.get_parents(): if parent_id in self.nodes and self.__is_displayed(parent_id): parents_nodes.append(parent_id) # Add to root if it is an orphan if parents_nodes == []: parents_nodes = [self.root_id] return parents_nodes #This is a crude hack which is more performant that other methods def is_path_valid(self,p): # print "is %s valid?" %str(p) valid = True i = 0 if len(p) == 1: valid = False else: while valid and i < len(p) - 1: child = p[i+1] par = p[i] if self.nodes.has_key(par): valid = (child in self.nodes[par]['children']) else: valid = False i += 1 return valid def get_paths_for_node(self, node_id): # cached = self.cache_paths.get(node_id,None) #The cache improves performance a lot for "stairs" #FIXME : the cache cannot detect if a new path has been added # validcache = False # if cached: # validcache = True # for p in cached: # validcache = validcache and self.is_path_valid(p) # if validcache: ## print "the valid cache is : %s" %str(cached) # return cached if node_id == self.root_id or not self.is_displayed(node_id): return [()] else: toreturn = [] for parent_id in self.nodes[node_id]['parents']: if parent_id not in self.nodes: raise Exception("Parent %s does not exists" % parent_id) if node_id not in self.nodes[parent_id]['children']: # Dump also state of FilteredTree => useful for debugging s = "\nCurrent tree:\n" for key in self.nodes: s += key + "\n" s += "\t parents" + str(self.nodes[key]['parents']) + "\n" s += "\t children" + str(self.nodes[key]['children']) + "\n" raise Exception("%s is not children of %s\n%s" % (node_id, parent_id, s)) for parent_path in self.get_paths_for_node(parent_id): mypath = parent_path + (node_id,) toreturn.append(mypath) # #Testing the cache # if validcache and toreturn != cached: # print "We return %s but %s was cached" %(str(toreturn),str(cached)) self.cache_paths[node_id] = toreturn return toreturn def print_tree(self, string=False): """ Representation of tree in FilteredTree @param string: if set, instead of printing, return string for printing. """ stack = [(self.root_id, "")] output = "_"*30 + "\n" + "FilteredTree cache\n" + "_"*30 + "\n" while stack != []: node_id, prefix = stack.pop() output += prefix + str(node_id) + '\n' for child_id in reversed(self.nodes[node_id]['children']): stack.append((child_id, prefix+" ")) output += "_"*30 + "\n" if string: return output else: print output def get_all_nodes(self): nodes = list(self.nodes.keys()) nodes.remove(self.root_id) return nodes def get_n_nodes(self, withfilters=[]): """ returns quantity of displayed nodes in this tree if the withfilters is set, returns the quantity of nodes that will be displayed if we apply those filters to the current tree. It means that the currently applied filters are also taken into account. """ return len(self.get_nodes(withfilters=withfilters)) def get_nodes(self, withfilters=[]): """ returns quantity of displayed nodes in this tree if the withfilters is set, returns the quantity of nodes that will be displayed if we apply those filters to the current tree. It means that the currently applied filters are also taken into account. """ if withfilters == []: # Use current cache return self.get_all_nodes() #FIXME maybe allow caching multiple withfilters... elif len(withfilters) == 1 and withfilters[0] in self.filter_cache: return self.filter_cache[withfilters[0]]['nodes'] # elif withfilters != []: else: # Filter on the current nodes filters = [] for filter_name in withfilters: filt = self.fbank.get_filter(filter_name) if filt: filters.append(filt) nodes = [] for node_id in self.nodes: if node_id == self.root_id: continue displayed = True for filt in filters: displayed = filt.is_displayed(node_id) if not displayed: break if displayed: nodes.append(node_id) return nodes def get_node_for_path(self, path): if not path or path == (): return None node_id = path[-1] #Both "if" should be benchmarked if path in self.get_paths_for_node(node_id): # if self.is_path_valid(path): return node_id else: return None def next_node(self, node_id, parent_id): if node_id == self.root_id: raise Exception("Calling next_node on the root node") if node_id not in self.nodes: raise Exception("Node %s is not displayed" % node_id) parents = self.nodes[node_id]['parents'] if not parent_id: parent_id = parents[0] elif parent_id not in parents: raise Exception("Node %s does not have parent %s" % (node_id, parent_id)) index = self.nodes[parent_id]['children'].index(node_id) if index+1 < len(self.nodes[parent_id]['children']): return self.nodes[parent_id]['children'][index+1] else: return None def node_all_children(self, node_id=None): if node_id is None: node_id = self.root_id return list(self.nodes[node_id]['children']) def node_has_child(self, node_id): return len(self.nodes[node_id]['children']) > 0 def node_n_children(self, node_id, recursive=False): if node_id == None: node_id = self.root_id if not self.nodes.has_key(node_id): return 0 if recursive: total = 0 #We avoid recursion in a loop #because the dict might be updated in the meantime cids = list(self.nodes[node_id]['children']) for cid in cids: total += self.node_n_children(cid,recursive=True) total += 1 #we count the node itself ofcourse return total else: return len(self.nodes[node_id]['children']) def node_nth_child(self, node_id, n): return self.nodes[node_id]['children'][n] def node_parents(self, node_id): if node_id not in self.nodes: raise IndexError('Node %s is not displayed' % node_id) parents = list(self.nodes[node_id]['parents']) if self.root_id in parents: parents.remove(self.root_id) return parents def get_current_state(self): """ Allows to connect LibLarch widget on fly to FilteredTree Sends 'added' signal/callback for every nodes that is currently in FilteredTree. After that, FilteredTree and TreeModel are in the same state """ for node_id in self.nodes[self.root_id]['children']: self.send_add_tree(node_id, self.root_id) #### FILTERS ################################################################## def list_applied_filters(self): return list(self.applied_filters) def apply_filter(self, filter_name, parameters=None, \ reset=None, refresh=None): """ Apply a new filter to the tree. @param filter_name: The name of an registrered filter from filters_bank @param parameters: Optional parameters to pass to the filter @param reset: Should other filters be removed? @param refresh: Should be refereshed the whole tree? (performance optimization) """ if reset: self.applied_filters = [] if parameters: filt = self.fbank.get_filter(filter_name) if filt: filt.set_parameters(parameters) else: raise ValueError("No filter of name %s in the bank" % filter_name) if filter_name not in self.applied_filters: self.applied_filters.append(filter_name) if refresh: self.refilter() toreturn = True else: toreturn = False return toreturn def unapply_filter(self, filter_name, refresh=True): """ Removes a filter from the tree. @param filter_name: The name of an already added filter to remove @param refresh: Should be refereshed the whole tree? (performance optimization) """ if filter_name in self.applied_filters: self.applied_filters.remove(filter_name) if refresh: self.refilter() return True else: return False def reset_filters(self, refresh=True): """ Clears all filters currently set on the tree. Can't be called on the main tree. """ self.applied_filters = [] if refresh: self.refilter() liblarch-2.1.0/setup.py0000664000175000017500000000272112046771147015333 0ustar ploumploum00000000000000#!/usr/bin/python # -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- # Liblarch - a library to handle directed acyclic graphs # Copyright (c) 2011-2012 - Lionel Dricot & Izidor Matušov # # This program is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, 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 Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . # ----------------------------------------------------------------------------- from distutils.core import setup setup( version = '2.1.0', url = 'https://live.gnome.org/liblarch', author = 'Lionel Dricot & Izidor Matušov', author_email = 'gtg-contributors@lists.launchpad.net', license = 'LGPLv3', name = 'liblarch', packages = ['liblarch', 'liblarch_gtk'], description = 'Liblarch is a python library built to easily handle '\ 'data structures such as lists, trees and directed acyclic graphs '\ 'and represent them as GTK TreeWidget or in other forms.', ) liblarch-2.1.0/liblarch_gtk/0000775000175000017500000000000012046771461016243 5ustar ploumploum00000000000000liblarch-2.1.0/liblarch_gtk/__init__.py0000664000175000017500000005407612043230325020352 0ustar ploumploum00000000000000# -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- # Liblarch - a library to handle directed acyclic graphs # Copyright (c) 2011-2012 - Lionel Dricot & Izidor Matušov # # This program is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, 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 Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . # ----------------------------------------------------------------------------- import gtk import gobject from liblarch_gtk.treemodel import TreeModel # Useful for debugging purpose. # Disabling that will disable the TreeModelSort on top of our TreeModel ENABLE_SORTING = True USE_TREEMODELFILTER = False BRITGHTNESS_CACHE = {} def brightness(color_str): """ Compute brightness of a color on scale 0-1 Courtesy to http://stackoverflow.com/questions/596216/formula-to-determine-brightness-of-rgb-color """ global BRITGHTNESS_CACHE if color_str not in BRITGHTNESS_CACHE: c = gtk.gdk.color_parse(color_str) brightness = (0.2126*c.red + 0.7152*c.green + 0.0722*c.blue)/65535.0 BRITGHTNESS_CACHE[color_str] = brightness return BRITGHTNESS_CACHE[color_str] class TreeView(gtk.TreeView): """ Widget which display LibLarch FilteredTree. This widget extends gtk.TreeView by several features: * Drag'n'Drop support * Sorting support * separator rows * background color of a row * selection of multiple rows """ __string_signal__ = (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, (str, )) __gsignals__ = {'node-expanded' : __string_signal__, \ 'node-collapsed': __string_signal__, \ } def __init__(self, tree, description): """ Build the widget @param tree - LibLarch FilteredTree @param description - definition of columns. Parameters of description dictionary for a column: * value => (type of values, function for generating value from a node) * renderer => (renderer_attribute, renderer object) Optional: * order => specify order of column otherwise use natural oreder * expandable => is the column expandable? * resizable => is the column resizable? * visible => is the column visible? * title => title of column * new_colum => do not create a separate column, just continue with the previous one (this can be used to create columns without borders) * sorting => allow default sorting on this column * sorting_func => use special function for sorting on this func Example of columns descriptions: description = { 'title': { 'value': [str, self.task_title_column], 'renderer': ['markup', gtk.CellRendererText()], 'order': 0 }} """ gtk.TreeView.__init__(self) self.columns = {} self.sort_col = None self.sort_order = gtk.SORT_ASCENDING self.bg_color_column = None self.separator_func = None self.dnd_internal_target = '' self.dnd_external_targets = {} # Sort columns self.order_of_column = [] last = 9999 for col_name in description: desc = description[col_name] order = desc.get('order', last) last += 1 self.order_of_column.append((order, col_name)) types = [] sorting_func = [] # Build the first coulumn if user starts with new_colum=False col = gtk.TreeViewColumn() # Build columns according to the order for col_num, (order_num, col_name) in enumerate(sorted(self.order_of_column), 1): desc = description[col_name] types.append(desc['value']) expand = desc.get('expandable', False) resizable = desc.get('resizable', True) visible = desc.get('visible', True) if 'renderer' in desc: rend_attribute, renderer = desc['renderer'] else: rend_attribute = 'markup' renderer = gtk.CellRendererText() # If new_colum=False, do not create new column, use the previous one # It will create columns without borders if desc.get('new_column',True): col = gtk.TreeViewColumn() newcol = True else: newcol = False col.set_visible(visible) if 'title' in desc: col.set_title(desc['title']) col.pack_start(renderer, expand=expand) col.add_attribute(renderer, rend_attribute, col_num) col.set_resizable(resizable) col.set_expand(expand) # Allow to set background color col.set_cell_data_func(renderer, self._celldatafunction) if newcol: self.append_column(col) self.columns[col_name] = (col_num, col) if ENABLE_SORTING: if 'sorting' in desc: # Just allow sorting and use default comparing self.sort_col = desc['sorting'] sort_num, sort_col = self.columns[self.sort_col] col.set_sort_column_id(sort_num) if 'sorting_func' in desc: # Use special funcion for comparing, e.g. dates sorting_func.append((col_num, col, desc['sorting_func'])) self.basetree = tree # Build the model around LibLarch tree self.basetreemodel = TreeModel(tree, types) #Applying an intermediate treemodelfilter, for debugging purpose if USE_TREEMODELFILTER: treemodelfilter = self.basetreemodel.filter_new() else: treemodelfilter = self.basetreemodel # Apply TreeModelSort to be able to sort if ENABLE_SORTING: # self.treemodel = gtk.TreeModelSort(treemodelfilter) self.treemodel = self.basetreemodel for col_num, col, sort_func in sorting_func: self.treemodel.set_sort_func(col_num, self._sort_func, sort_func) col.set_sort_column_id(col_num) else: self.treemodel = treemodelfilter self.set_model(self.treemodel) self.expand_all() self.show() self.collapsed_paths = [] self.connect('row-expanded', self.__emit, 'expanded') self.connect('row-collapsed', self.__emit, 'collapsed') self.treemodel.connect('row-has-child-toggled', self.on_child_toggled) def __emit(self, sender, iter, path, data): """ Emitt expanded/collapsed signal """ node_id = self.treemodel.get_value(iter, 0) #recreating the path of the collapsed node ll_path = () i = 1 while i <= len(path): temp_iter = self.treemodel.get_iter(path[:i]) ll_path += (self.treemodel.get_value(temp_iter,0),) i+=1 if data == 'expanded': self.emit('node-expanded', ll_path) elif data == 'collapsed': self.emit('node-collapsed', ll_path) def on_child_toggled(self, treemodel, path, iter, param=None): """ Expand row """ #is the toggled node in the collapsed paths? collapsed = False nid = treemodel.get_value(iter,0) while iter and not collapsed: for c in self.collapsed_paths: if c[-1] == nid: collapsed = True iter = treemodel.iter_parent(iter) if not self.row_expanded(path) and not collapsed: self.expand_row(path, True) def expand_node(self,llpath): """ Expand the children of a node. This is not recursive """ self.collapse_node(llpath,collapsing_method=self.expand_one_row) def expand_one_row(self,p): #We have to set the "open all" parameters self.expand_row(p,False) def collapse_node(self, llpath,collapsing_method=None): """ Hide children of a node This method is needed for "rember collapsed nodes" feature of GTG. Transform node_id into paths and those paths collapse. By default all children are expanded (see self.expand_all()) @parameter llpath - LibLarch path to the node. Node_id is extracted as the last parameter and then all instances of that node are collapsed. For retro-compatibility, we take llpath instead of node_id directly""" if not collapsing_method: collapsing_method = self.collapse_row node_id = llpath[-1].strip("'") if not node_id: raise Exception('Missing node_id in path %s' % str(llpath)) schedule_next = True for path in self.basetree.get_paths_for_node(node_id): iter = self.basetreemodel.my_get_iter(path) if iter is None: continue target_path = self.basetreemodel.get_path(iter) if self.basetreemodel.get_value(iter, 0) == node_id: collapsing_method(target_path) self.collapsed_paths.append(path) schedule_next = False if schedule_next: self.basetree.queue_action(node_id, collapsing_method, param=llpath) def show(self): """ Shows the TreeView and connect basetreemodel to LibLarch """ gtk.TreeView.show(self) self.basetreemodel.connect_model() def get_columns(self): """ Return the list of columns name """ return self.columns.keys() def set_main_search_column(self, col_name): """ Set search column for GTK integrate search This is just wrapper to use internal representation of columns""" col_num, col = self.columns[col_name] self.set_search_column(col_num) def set_expander_column(self, col_name): """ Set expander column (that which expands through free space) This is just wrapper to use internal representation of columns""" col_num, col = self.columns[col_name] self.set_property("expander-column", col) def set_sort_column(self, col_name, order=gtk.SORT_ASCENDING): """ Select column to sort by it by default """ if ENABLE_SORTING: self.sort_col = col_name self.sort_order = order col_num, col = self.columns[col_name] self.treemodel.set_sort_column_id(col_num, order) def get_sort_column(self): """ Get sort column """ if ENABLE_SORTING: return self.sort_col, self.sort_order def set_col_visible(self, col_name,visible): """ Set visiblity of column. Allow to hide/show certain column """ col_num, col = self.columns[col_name] col.set_visible(visible) def set_col_resizable(self, col_name, resizable): """ Allow/forbid column to be resizable """ col_num, col = self.columns[col_name] col.set_resizable(resizable) def set_bg_color(self, color_func, color_column): """ Set which column and function for generating background color Function should be in format func(node, default_color) """ def closure_default_color(func, column): """ Set default color to the function. Transform function from func(node, default_color) into func(node). Default color is computed based on some GTK style magic. """ default = column.get_tree_view().get_style().base[gtk.STATE_NORMAL] return lambda node: func(node, default) if self.columns.has_key(color_column): self.bg_color_column, column = self.columns[color_column] func = closure_default_color(color_func, column) self.treemodel.set_column_function(self.bg_color_column, func) else: raise ValueError("There is no colum %s to use to set color" % \ color_column) def _sort_func(self, model, iter1, iter2, func=None): """ Sort two iterators by function which gets node objects. This is a simple wrapper which prepares node objects and then call comparing function. In other case return default value -1 """ node_id_a = model.get_value(iter1, 0) node_id_b = model.get_value(iter2, 0) if node_id_a and node_id_b and func: id, order = self.treemodel.get_sort_column_id() node_a = self.basetree.get_node(node_id_a) node_b = self.basetree.get_node(node_id_b) sort = func(node_a, node_b, order) else: sort = -1 return sort def _celldatafunction(self, column, cell, model, myiter): """ Determine background color for cell Requirements: self.bg_color_column must be set (see self.set_bg_color()) Set background color based on a certain column value. """ if self.bg_color_column is None: return if myiter and model.iter_is_valid(myiter): color = model.get_value(myiter, self.bg_color_column) else: color = None if isinstance(cell, gtk.CellRendererText): if color is not None and brightness(color) < 0.5: cell.set_property("foreground", '#FFFFFF') else: # Otherwise unset foreground color cell.set_property("foreground-set", False) cell.set_property("cell-background", color) ######### DRAG-N-DROP functions ##################################### def set_dnd_name(self, dndname): """ Sets Drag'n'Drop name and initialize Drag'n'Drop support If ENABLE_SORTING, drag_drop signal must be handled by this widget.""" self.dnd_internal_target = dndname self.__init_dnd() self.connect('drag_data_get', self.on_drag_data_get) self.connect('drag_data_received', self.on_drag_data_received) def set_dnd_external(self, sourcename, func): """ Add a new external target and initialize Drag'n'Drop support""" i = 1 while self.dnd_external_targets.has_key(i): i += 1 self.dnd_external_targets[i] = [sourcename, func] self.__init_dnd() def __init_dnd(self): """ Initialize Drag'n'Drop support Firstly build list of DND targets: * name * scope - just the same widget / same application * id Enable DND by calling enable_model_drag_dest(), enable_model-drag_source() It didnt use support from gtk.Widget(drag_source_set(), drag_dest_set()). To know difference, look in PyGTK FAQ: http://faq.pygtk.org/index.py?file=faq13.033.htp&req=show """ self.defer_select = False if self.dnd_internal_target == '': error = 'Cannot initialize DND without a valid name\n' error += 'Use set_dnd_name() first' raise Exception(error) dnd_targets = [(self.dnd_internal_target, gtk.TARGET_SAME_WIDGET, 0)] for target in self.dnd_external_targets: name = self.dnd_external_targets[target][0] dnd_targets.append((name, gtk.TARGET_SAME_APP, target)) self.enable_model_drag_source( gtk.gdk.BUTTON1_MASK, dnd_targets, gtk.gdk.ACTION_DEFAULT | gtk.gdk.ACTION_MOVE) self.enable_model_drag_dest(\ dnd_targets, gtk.gdk.ACTION_DEFAULT | gtk.gdk.ACTION_MOVE) def on_drag_data_get(self, treeview, context, selection, info, timestamp): """ Extract data from the source of the DnD operation. Serialize iterators of selected tasks in format ,,..., and set it as parameter of DND """ treeselection = treeview.get_selection() model, paths = treeselection.get_selected_rows() iters = [model.get_iter(path) for path in paths] iter_str = ','.join([model.get_string_from_iter(iter) for iter in iters]) selection.set(self.dnd_internal_target, 0, iter_str) def on_drag_data_received(self, treeview, context, x, y, selection, info,\ timestamp): """ Handle a drop situation. First of all, we need to get id of node which should accept all draged nodes as their new children. If there is no node, drop to root node. Deserialize iterators of dragged nodes (see self.on_drag_data_get()) Info parameter determines which target was used: * info == 0 => internal DND within this TreeView * info > 0 => external DND In case of internal DND we just use Tree.move_node(). In case of external DND we call function associated with that DND set by self.set_dnd_external() """ #TODO: it should be configurable for each TreeView if you want: # 0 : no drag-n-drop at all # 1 : drag-n-drop move the node # 2 : drag-n-drop copy the node model = treeview.get_model() drop_info = treeview.get_dest_row_at_pos(x, y) if drop_info: path, position = drop_info iter = model.get_iter(path) # Must add the task to the parent of the task situated # before/after if position == gtk.TREE_VIEW_DROP_BEFORE or\ position == gtk.TREE_VIEW_DROP_AFTER: # Get sibling parent destination_iter = model.iter_parent(iter) else: # Must add task as a child of the dropped-on iter # Get parent destination_iter = iter if destination_iter: destination_tid = model.get_value(destination_iter, 0) else: #it means we have drag-n-dropped above the first task # we should consider the destination as a root then. destination_tid = None else: # Must add the task to the root # Parent = root => iter=None destination_tid = None tree = self.basetree.get_basetree() # Get dragged iter as a TaskTreeModel iter # If there is no selected task (empty selection.data), # explictly skip handling it (set to empty list) if selection.data == '': iters = [] else: iters = selection.data.split(',') dragged_iters = [] for iter in iters: if info == 0: try: dragged_iters.append(model.get_iter_from_string(iter)) except ValueError: #I hate to silently fail but we have no choice. #It means that the iter is not good. #Thanks shitty gtk API for not allowing us to test the string dragged_iter = None elif info in self.dnd_external_targets and destination_tid: f = self.dnd_external_targets[info][1] src_model = context.get_source_widget().get_model() dragged_iters.append(src_model.get_iter_from_string(iter)) for dragged_iter in dragged_iters: if info == 0: if dragged_iter and model.iter_is_valid(dragged_iter): dragged_tid = model.get_value(dragged_iter, 0) try: tree.move_node(dragged_tid, new_parent_id=destination_tid) except Exception, e: print 'Problem with dragging: %s' % e elif info in self.dnd_external_targets and destination_tid: source = src_model.get_value(dragged_iter,0) # Handle external Drag'n'Drop f(source, destination_tid) ######### Separators support ############################################## def _separator_func(self, model, itera, user_data=None): """ Call user function to determine if this node is separator """ if itera and model.iter_is_valid(itera): node_id = model.get_value(itera, 0) node = self.basetree.get_node(node_id) if self.separator_func: return self.separator_func(node) else: return False else: return False def set_row_separator_func(self, func): """ Enable support for row separators. @param func - function which determines if a node is separator, None will disable support for row separators. """ self.separator_func = func gtk.TreeView.set_row_separator_func(self,self._separator_func) ######### Multiple selection #################################################### def get_selected_nodes(self): """ Return list of node_id from liblarch for selected nodes """ # Get the selection in the gtk.TreeView selection = self.get_selection() # Get the selection iter if selection.count_selected_rows() <= 0: ids = [] else: model, paths = selection.get_selected_rows() iters = [model.get_iter(path) for path in paths] ts = self.get_model() #0 is the column of the tid ids = [ts.get_value(iter, 0) for iter in iters] return ids def set_multiple_selection(self, multiple_selection): """ Allow/forbid multiple selection in TreeView """ # TODO support for dragging multiple rows at the same time # See LP #817433 if multiple_selection: selection_type = gtk.SELECTION_MULTIPLE else: selection_type = gtk.SELECTION_SINGLE self.get_selection().set_mode(selection_type) liblarch-2.1.0/liblarch_gtk/treemodel.py0000664000175000017500000001725212003453413020567 0ustar ploumploum00000000000000# -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- # Liblarch - a library to handle directed acyclic graphs # Copyright (c) 2011-2012 - Lionel Dricot & Izidor Matušov # # This program is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, 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 Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . # ----------------------------------------------------------------------------- import gtk class TreeModel(gtk.TreeStore): """ Local copy of showed tree """ def __init__(self, tree, types): """ Initializes parent and create list of columns. The first colum is node_id of node """ self.count = 0 self.count2 = 0 self.types = [[str, lambda node: node.get_id()]] + types only_types = [python_type for python_type, access_method in self.types] gtk.TreeStore.__init__(self, *only_types) self.cache_paths = {} self.cache_position = {} self.tree = tree def set_column_function(self, column_num, column_func): """ Replace function for generating certain column. Original use case was changing method of generating background color during runtime - background by tags or due date """ if column_num < len(self.types): self.types[column_num][1] = column_func return True else: return False def connect_model(self): """ Register "signals", callbacks from liblarch. Also asks for the current status by providing add_task callback. We are able to connect to liblarch tree on the fly. """ self.tree.register_cllbck('node-added-inview',self.add_task) self.tree.register_cllbck('node-deleted-inview',self.remove_task) self.tree.register_cllbck('node-modified-inview',self.update_task) self.tree.register_cllbck('node-children-reordered',self.reorder_nodes) # Request the current state self.tree.get_current_state() def my_get_iter(self, path): """ Because we sort the TreeStore, paths in the treestore are not the same as paths in the FilteredTree. We do the conversion here. We receive a Liblarch path as argument and return a gtk.TreeIter""" #The function is recursive. We take iter for path (A,B,C) in cache. #If there is not, we take iter for path (A,B) and try to find C. if path == (): return None nid = str(path[-1]) self.count += 1 #We try to use the cache iter = self.cache_paths.get(path,None) toreturn = None if iter and self.iter_is_valid(iter) and nid == self.get_value(iter,0): self.count2 += 1 toreturn = iter else: root = self.my_get_iter(path[:-1]) #This is a small ad-hoc optimisation. #Instead of going through all the children nodes #We go directly at the last known position. pos = self.cache_position.get(path,None) if pos: iter = self.iter_nth_child(root,pos) if iter and self.get_value(iter,0) == nid: toreturn = iter if not toreturn: if root: iter = self.iter_children(root) else: iter = self.get_iter_first() while iter and self.get_value(iter,0) != nid: iter = self.iter_next(iter) self.cache_paths[path] = iter toreturn = iter # print "%s / %s" %(self.count2,self.count) # print "my_get_iter %s : %s" %(nid,self.get_string_from_iter(toreturn)) return toreturn def print_tree(self): """ Print TreeStore as Tree into console """ def push_to_stack(stack, level, iterator): """ Macro which adds a new element into stack if it is possible """ if iterator is not None: stack.append((level, iterator)) stack = [] push_to_stack(stack, 0, self.get_iter_first()) print "+"*50 print "Treemodel print_tree: " while stack != []: level, iterator = stack.pop() print "=>"*level, self.get_value(iterator, 0) push_to_stack(stack, level, self.iter_next(iterator)) push_to_stack(stack, level+1, self.iter_children(iterator)) print "+"*50 ### INTERFACE TO LIBLARCH ##################################################### def add_task(self, node_id, path): """ Add new instance of node_id to position described at path. @param node_id: identification of task @param path: identification of position """ node = self.tree.get_node(node_id) # Build a new row row = [] for python_type, access_method in self.types: value = access_method(node) row.append(value) # Find position to add task iter_path = path[:-1] iterator = self.my_get_iter(iter_path) self.cache_position[path] = self.iter_n_children(iterator) it = self.insert(iterator, -1, row) # Show the new task if possible # self.row_has_child_toggled(self.get_path(it), it) def remove_task(self, node_id, path): """ Remove instance of node. @param node_id: identification of task @param path: identification of position """ it = self.my_get_iter(path) if not it: raise Exception("Trying to remove node %s with no iterator"%node_id) actual_node_id = self.get_value(it, 0) assert actual_node_id == node_id self.remove(it) self.cache_position.pop(path) def update_task(self, node_id, path): """ Update instance of node by rebuilding the row. @param node_id: identification of task @param path: identification of position """ #We cannot assume that the node is in the tree because #update is asynchronus #Also, we should consider that missing an update is not critical #and ignoring the case where there is no iterator if self.tree.is_displayed(node_id): node = self.tree.get_node(node_id) #That call to my_get_iter is really slow! iterator = self.my_get_iter(path) if iterator: for column_num, (python_type, access_method) in enumerate(self.types): value = access_method(node) self.set_value(iterator, column_num, value) def reorder_nodes(self, node_id, path, neworder): """ Reorder nodes. This is deprecated signal. In the past it was useful for reordering showed nodes of tree. It was possible to delete just the last element and therefore every element must be moved to the last position and then deleted. @param node_id: identification of root node @param path: identification of poistion of root node @param neworder: new order of children of root node """ if path is not None: it = self.my_get_iter(path) else: it = None self.reorder(it, neworder) self.print_tree() liblarch-2.1.0/LICENSE0000664000175000017500000001674311716424035014631 0ustar ploumploum00000000000000 GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. 0. Additional Definitions. As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. "The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. 1. Exception to Section 3 of the GNU GPL. You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. 2. Conveying Modified Versions. If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. 3. Object Code Incorporating Material from Library Header Files. The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the object code with a copy of the GNU GPL and this license document. 4. Combined Works. You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the Combined Work with a copy of the GNU GPL and this license document. c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. d) Do one of the following: 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) 5. Combined Libraries. You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 6. Revised Versions of the GNU Lesser General Public License. The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. liblarch-2.1.0/README0000664000175000017500000000143011714741363014473 0ustar ploumploum00000000000000Liblarch, a python library to easily handle data structure Liblarch is a python library built to easily handle data structure such are lists, trees and acyclic graphs (tree where nodes can have multiple parents). There's also a liblarch-gtk binding that will allow you to use your data structure into a Gtk.Treeview. Liblarch support multiple views of one data structure and complex filtering. That way, you have a clear separation between your data themselves (Model) and how they are displayed (View). If you find Gtk.Treeview and Gtk.Treemodel hard to use, then liblarch is probably for you. The documentation can be read on: https://live.gnome.org/liblarch Liblarch is under the LGPLv3 license. Authors: Lionel Dricot Izidor Matušov