예제 #1
0
 def __init__(self):
     self.view = TaskTreeView()
     
     # TODO: Connect edited and (selection) changed event handlers.
     self.view.title_renderer.connect("edited", self._handle_cell_edited)
             
     # Monitor selection change events.
     self.view.get_selection().connect("changed",
         self._handle_selection_changed)
     
     # Declare the selection changed and title edited events.
     self.selection_state_changed = Event()
     self.entity_title_edited = Event()
     
     # Establish the tree store (model) that holds the task entity 
     # information.
     self.task_treestore = TaskTreeStore()
     
     # This index allows us to quickly look up where in the tree a given
     # entity is.
     self.entity_path_index = dict()
     self._tasks = dict()
     self._tasklists = dict()
     self.tree_states = dict()
     
     # Set default for clearing flag. This flag is used to help ignore 
     # "system" selection change events that seem to occur during the 
     # Gtk.TreeStore.clear() operation.
     self._is_clearing = False
     
     # Connect the tree store/row_data to the tree view.
     self.view.set_model(self.task_treestore)
예제 #2
0
class TaskTreeViewController(object):
    """
    Provides an interface to the task tree view. Converts task tree paths into
    usable entity lists to be consumed by the parent controller. Converts
    entity lists into new tree stores and displays the updated information.
    
    Manages tree state: selections, selection state, and expansion state.
    
    Maintain:
    -- Dict of tasklists, keyed by ID - still necessary?
    -- Dict of tasks, keyed by ID - still necessary?
    -- TaskTreeView/Gtk.TreeView
    -- TaskTreeStore/Gtk.TreeStore
    """            
    def __init__(self):
        self.view = TaskTreeView()
        
        # TODO: Connect edited and (selection) changed event handlers.
        self.view.title_renderer.connect("edited", self._handle_cell_edited)
                
        # Monitor selection change events.
        self.view.get_selection().connect("changed",
            self._handle_selection_changed)
        
        # Declare the selection changed and title edited events.
        self.selection_state_changed = Event()
        self.entity_title_edited = Event()
        
        # Establish the tree store (model) that holds the task entity 
        # information.
        self.task_treestore = TaskTreeStore()
        
        # This index allows us to quickly look up where in the tree a given
        # entity is.
        self.entity_path_index = dict()
        self._tasks = dict()
        self._tasklists = dict()
        self.tree_states = dict()
        
        # Set default for clearing flag. This flag is used to help ignore 
        # "system" selection change events that seem to occur during the 
        # Gtk.TreeStore.clear() operation.
        self._is_clearing = False
        
        # Connect the tree store/row_data to the tree view.
        self.view.set_model(self.task_treestore)
        
    def update_task_tree(self, tasklists, tasks):
        """
        Collect the current tree state, replace the tree model, and then 
        restore the tree state (as much as possible).
        """
        # Collect current tree state.
        self._rebuild_tree_state()
        
        # Clear out tree. Set clearing flag to disable selection change 
        # handling, as the clear operation will fire those events.
        self._is_clearing = True
        self.task_treestore.clear()
        self._is_clearing = False
        
        # Build a new tree with the updated task data.
        self._tasklists = tasklists
        self._tasks = tasks
        self.entity_path_index = self.task_treestore.build_tree(tasklists, tasks)
        
        # With the new tree structure in place, try to restore the old tree 
        # state to the fullest extent possible.
        self._restore_tree_state()
    
#    def select_entity(self, target_entity):
#        entity_tree_path = self._get_path_for_entity_id(target_entity.entity_id)
#        assert entity_tree_path is not None
#        
#        # TODO: Should the controller really be digging this deep into the 
#        # view, or should the view provide an interface that includes a 
#        # select_path method?
#        self.view.get_selection().select_path(entity_tree_path)
    
    def set_entity_editable(self, entity, is_editable=True):
        # Find the entity within the task tree.        
        entity_tree_path = self._get_path_for_entity_id(entity.entity_id)
        assert entity_tree_path is not None
                
        # Expand any parent nodes of the entity (to ensure it's visible). This 
        # will only be relevant for task entities, as tasklist entities will 
        # always already be visible.
        tree_iter = self.task_treestore.get_iter_from_string(entity_tree_path)
        treepath = self.task_treestore.get_path(tree_iter)
        self.view.expand_to_path(treepath)        
        
        # Select the entity, making the title editable and holding the keyboard
        # focus.        
        treepath = self.task_treestore.get_path(tree_iter)
        self.view.start_editing(treepath)
    
    def _get_treepath_for_path(self, path):
        self.task_treestore.get_iter_from_string(path)
    
    def _get_parent_entity(self, entity):
        assert isinstance(entity, Task)
        
        return None
    
#    def _set_entity_expanded(self, entity, is_expanded=True):
#        # Find the entity within the task tree.
#        entity_tree_path = self._get_path_for_entity_id(entity.entity_id)
#        assert entity_tree_path is not None
#
#        # Expand or collapse the entity.
#        if is_expanded:
        
    def get_selected_entities(self):
        assert (not self._tasklist_selection_state == self.SelectionState.NONE or not self._task_selection_state == self.SelectionState.NONE)
        
        # Ensure tree state information is up to date.
        self._rebuild_tree_state()
                
        # Loop through the tree states, looking for selected entity IDs.
        selected_entities = list()
        for entity_id in self.tree_states.keys():
            tree_state = self.tree_states.get(entity_id)
            
            if tree_state.is_selected:
                if self._tasklists.has_key(entity_id):
                    entity = self._tasklists.get(entity_id)                    
                elif self._tasks.has_key(entity_id):
                    entity = self._tasks.get(entity_id)
                else:
                    raise ValueError("Entity with ID of {0} is not a required type (TaskList or Task)")
                selected_entities.append(entity)
        
        return selected_entities
    
    def _rebuild_tree_state(self):            
        # Clear out existing tree states.
        self.tree_states.clear()
        
        self._collect_tree_state()
        
    def _collect_tree_state(self, tree_iter=None):
        if tree_iter is None:
            # Assume a default position of the root node if nothing has been
            # specified. This allows the method to be called without arguments.
            tree_iter = self.task_treestore.get_iter_first()
            
        while tree_iter != None:
            tree_path = self.task_treestore.get_path(tree_iter)
            
            is_expanded = is_selected = False
            
            if self.view.row_expanded(tree_path):
                is_expanded = True
            
            if self.view.get_selection().path_is_selected(tree_path):
                is_selected = True
            
            if is_expanded or is_selected:
                entity_id = self.task_treestore[tree_iter][TreeNode.ENTITY_ID]

                self.tree_states[entity_id] = self.TreeState(is_expanded, is_selected)            
            
            if self.task_treestore.iter_has_child(tree_iter):
                child_iter = self.task_treestore.iter_children(tree_iter)
                
                self._collect_tree_state(child_iter)
            
            tree_iter = self.task_treestore.iter_next(tree_iter)
    
    def _restore_tree_state(self, tree_iter=None):
        if tree_iter is None:
            # Assume a default position of the root node if nothing has been
            # specified. This allows the method to be called without arguments.
            tree_iter = self.task_treestore.get_iter_first()
            
        while tree_iter != None:
            tree_path = self.task_treestore.get_path(tree_iter)
            current_entity_id = self.task_treestore[tree_iter][TreeNode.ENTITY_ID]
            
            if self.tree_states.has_key(current_entity_id):
                tree_row_state = self.tree_states[current_entity_id]
                
                if tree_row_state.is_expanded:
                    self.view.expand_row(tree_path, False)
                    
                if tree_row_state.is_selected:
                    self.view.get_selection().select_path(tree_path)
            
            if self.task_treestore.iter_has_child(tree_iter):
                child_iter = self.task_treestore.iter_children(tree_iter)
                
                self._restore_tree_state(child_iter)
            
            tree_iter = self.task_treestore.iter_next(tree_iter)
            
    def _get_entity_id_for_path(self, tree_path):
        if tree_path in self.entity_path_index.values():
            key_index = self.entity_path_index.values().index(tree_path)
            return self.entity_path_index.keys()[key_index]
        else:
            raise ValueError("Could not find an entity registered with path {0}".format(tree_path))
    
    def _get_path_for_entity_id(self, entity_id):
        if self.entity_path_index.has_key(entity_id):
            return self.entity_path_index.get(entity_id)
        else:
            raise ValueError("Could not find a path for entity with id {0}".format(entity_id))
        
    def _get_entity_for_path(self, tree_path):
        entity_id = self._get_entity_id_for_path(tree_path)
        
        if self._tasklists.has_key(entity_id):
            entity = self._tasklists.get(entity_id)
        elif self._tasks.has_key(entity_id):
            entity = self._tasks.get(entity_id)
        else:
            raise ValueError("Could not find an entity for the path {0} and entity id {1}".format(tree_path, entity_id))

        return entity
        
    def _handle_cell_edited(self, tree_title_cell, tree_path, updated_title):
        # Find the entity that was edited through the tree view.
        target_entity = self._get_entity_for_path(tree_path)
        assert target_entity is not None
        
        # Fire event, sending along the (unmodified) target entity and the
        # updated title text.
        self.entity_title_edited.fire(target_entity, updated_title)
    
    def _handle_selection_changed(self, selection_data):
        # If the clearing flag is set, return immediately to prevent handling
        # spurious selection change events.
        if self._is_clearing:
            return
        
        selected_rows = selection_data.get_selected_rows()[1]
                
        self._selected_tasks = list()
        self._selected_tasklists = list()
                
        # Count the tasklists and tasks in the selection.
        for new_selected_row in selected_rows:            
            selected_entity = self._get_entity_for_path(
                new_selected_row.to_string())

            if isinstance(selected_entity, TaskList):
                # This row represents a Tasklist.
                self._selected_tasklists.append(selected_entity)
            elif isinstance(selected_entity, Task):
                # This row represents a Task.
                self._selected_tasks.append(selected_entity)
        
        if len(self._selected_tasklists) == 1:
            self._tasklist_selection_state = TaskTreeViewController.SelectionState.SINGLE
        elif len(self._selected_tasklists) > 1:
            self._tasklist_selection_state = TaskTreeViewController.SelectionState.MULTIPLE_HETERGENOUS
        else:
            self._tasklist_selection_state = TaskTreeViewController.SelectionState.NONE
        
        if len(self._selected_tasks) == 1:
            self._task_selection_state = TaskTreeViewController.SelectionState.SINGLE
        elif len(self._selected_tasks) > 1:
            # Determine if all selected tasks belong to the same tasklist or
            # not.
            is_homogenous = True
            prev_tasklist_id = None
            for selected_task in self._selected_tasks:
                if prev_tasklist_id is not None and selected_task.tasklist_id != prev_tasklist_id:
                    is_homogenous = False
                    break
            
            if is_homogenous:
                self._task_selection_state = TaskTreeViewController.SelectionState.MULTIPLE_HOMOGENOUS
            else:
                self._task_selection_state = TaskTreeViewController.SelectionState.MULTIPLE_HETERGENOUS
        else:
            self._task_selection_state = TaskTreeViewController.SelectionState.NONE
                                    
        # Notify any listeners of the change in selection state.
        # TODO: Make this a bit smarter/more efficient by only firing when the
        # selection state actually changes, instead of on every selection 
        # event.
        self.selection_state_changed.fire(self._tasklist_selection_state, self._task_selection_state)  
        
    class TreeState(object):
        """
        Very simple convenience class to group the two tree node states 
        together.
        """
        def __init__(self, is_expanded=False, is_selected=False):
            self.is_expanded = is_expanded
            self.is_selected = is_selected      
        
    class SelectionState(object):
            NONE = 0
            SINGLE = 1
            MULTIPLE_HOMOGENOUS = 2 # All in same tasklist
            MULTIPLE_HETERGENOUS = 3 # In different tasklists