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)
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