def _on_node_changed_end(self, model, nodes): # maintain proper expansion for node in nodes: if node == self._master_node: for child in node.get_children(): if self.is_node_expanded(child): path = get_path_from_node( self.model, child, self.rich_model.get_node_column_pos()) self.expand_row(path, False) else: try: path = get_path_from_node( self.model, node, self.rich_model.get_node_column_pos()) except: path = None if path is not None: parent = node.get_parent() # NOTE: parent may lose expand state if it has one child # therefore, we should expand parent if it exists and is # visible (i.e. len(path)>1) in treeview if (parent and self.is_node_expanded(parent) and len(path) > 1): self.expand_row(path[:-1], False) if self.is_node_expanded(node): self.expand_row(path, False) # if nodes still exist, and expanded, try to reselect them sel_count = 0 selection = self.get_selection() for node in self.__sel_nodes2: sel_count += 1 if node.is_valid(): path2 = get_path_from_node( self.model, node, self.rich_model.get_node_column_pos()) if (path2 is not None and (len(path2) <= 1 or self.row_expanded(path2[:-1]))): # reselect and scroll to node selection.select_path(path2) # restore scroll gobject.idle_add(lambda : self.scroll_to_point(*self.__scroll)) # resume emitting selection changes self.__suppress_sel = False # emit de-selection if sel_count == 0: self.select_nodes([])
def edit_node(self, page): path = treemodel.get_path_from_node( self.model, page, self.rich_model.get_node_column_pos()) if path is None: # view page first if not in view self.emit("goto-node", page) path = treemodel.get_path_from_node( self.model, page, self.rich_model.get_node_column_pos()) assert path is not None self.set_cursor_on_cell(path, self.title_column, self.title_text, True) path, col = self.get_cursor() self.scroll_to_cell(path)
def set_node_expanded(self, node, expand): # don't save the expand state of the master node if len(treemodel.get_path_from_node( self.model, node, self.rich_model.get_node_column_pos())) > 1: node.set_attr("expanded2", expand)
def on_edit_attr(self, cellrenderertext, path, attr, new_text, validator=TextRendererValidator()): """Callback for completion of title editing""" # remember editing state self.editing_path = None new_text = unicode_gtk(new_text) # get node being edited node = self.model.get_value(self.model.get_iter(path), self._node_col) if node is None: return # determine value from new_text, if invalid, ignore it try: new_val = validator.parse(new_text) except: return # set new attr and catch errors try: node.set_attr(attr, new_val) except NoteBookError as e: self.emit("error", e.msg, e) # reselect node # need to get path again because sorting may have changed path = get_path_from_node(self.model, node, self.rich_model.get_node_column_pos()) if path is not None: self.set_cursor(path) gobject.idle_add(lambda: self.scroll_to_cell(path)) self.emit("edit-node", node, attr, new_val)
def set_node_expanded(self, node, expand): # don't save the expand state of the master node if len( treemodel.get_path_from_node( self.model, node, self.rich_model.get_node_column_pos())) > 1: node.set_attr("expanded2", expand)
def append_node(self, node): # do not allow appending of nodes unless we are masterless if self.get_master_node() is not None: return self.rich_model.append(node) if node.get_attr("expanded2", False): self.expand_to_path(treemodel.get_path_from_node( self.model, node, self.rich_model.get_node_column_pos())) self.set_sensitive(True)
def append_node(self, node): # do not allow appending of nodes unless we are masterless if self.get_master_node() is not None: return self.rich_model.append(node) if node.get_attr("expanded2", False): self.expand_to_path( treemodel.get_path_from_node( self.model, node, self.rich_model.get_node_column_pos())) self.set_sensitive(True)
def select_nodes(self, nodes): """Select nodes in treeview""" # NOTE: for now only select one node if len(nodes) > 0: node = nodes[0] path = get_path_from_node(self.model, node, self.rich_model.get_node_column_pos()) if path is not None: if len(path) > 1: self.expand_to_path(path[:-1]) self.set_cursor(path) gobject.idle_add(lambda: self.scroll_to_cell(path)) else: # unselect all nodes self.get_selection().unselect_all()
def view_nodes(self, nodes, nested=True): # TODO: learn how to deactivate expensive sorting #self.model.set_default_sort_func(None) #self.model.set_sort_column_id(-1, gtk.SORT_ASCENDING) # save sorting if a single node was selected if self._sel_nodes is not None and len(self._sel_nodes) == 1: self.save_sorting(self._sel_nodes[0]) if len(nodes) > 1: nested = False self._sel_nodes = nodes self.rich_model.set_nested(nested) # set master node self.set_master_node(None) # populate model roots = nodes self.rich_model.set_root_nodes(roots) # load sorting if single node is selected if len(nodes) == 1: self.load_sorting(nodes[0], self.model) # expand rows for node in roots: self.expand_to_path( treemodel.get_path_from_node( self.model, node, self.rich_model.get_node_column_pos())) # disable if no roots if len(roots) == 0: self.set_sensitive(False) else: self.set_sensitive(True) # update status self.display_page_count() self.emit("select-nodes", [])
def view_nodes(self, nodes, nested=True): # TODO: learn how to deactivate expensive sorting #self.model.set_default_sort_func(None) #self.model.set_sort_column_id(-1, gtk.SORT_ASCENDING) # save sorting if a single node was selected if self._sel_nodes is not None and len(self._sel_nodes) == 1: self.save_sorting(self._sel_nodes[0]) if len(nodes) > 1: nested = False self._sel_nodes = nodes self.rich_model.set_nested(nested) # set master node self.set_master_node(None) # populate model roots = nodes self.rich_model.set_root_nodes(roots) # load sorting if single node is selected if len(nodes) == 1: self.load_sorting(nodes[0], self.model) # expand rows for node in roots: self.expand_to_path(treemodel.get_path_from_node( self.model, node, self.rich_model.get_node_column_pos())) # disable if no roots if len(roots) == 0: self.set_sensitive(False) else: self.set_sensitive(True) # update status self.display_page_count() self.emit("select-nodes", [])
class KeepNoteBaseTreeView (gtk.TreeView): """Base class for treeviews of a NoteBook notes""" def __init__(self): gtk.TreeView.__init__(self) self.model = None self.rich_model = None self._notebook = None self._master_node = None self.editing = False self.__sel_nodes = [] self.__sel_nodes2 = [] self.__scroll = (0, 0) self.__suppress_sel = False self._node_col = None self._get_icon = None self._menu = None # TODO: style #print self.style #"vertical-separator" #print self.style_get_property("vertical-separator") #style = self.get_modifier_style() #self.modify_style(style) #print self.style_get_property("vertical-separator") # selection self.get_selection().connect("changed", self.__on_select_changed) self.get_selection().connect("changed", self.on_select_changed) # row expand/collapse self.connect("row-expanded", self._on_row_expanded) self.connect("row-collapsed", self._on_row_collapsed) # drag and drop state self._is_dragging = False # whether drag is in progress self._drag_count = 0 self._dest_row = None # current drag destition self._reorder = REORDER_ALL # enum determining the kind of reordering # that is possible via drag and drop # region, defined by number of vertical pixels from top and bottom of # the treeview widget, where drag scrolling will occur self._drag_scroll_region = 30 # clipboard self.connect("copy-clipboard", self._on_copy_node) self.connect("copy-tree-clipboard", self._on_copy_tree) self.connect("cut-clipboard", self._on_cut_node) self.connect("paste-clipboard", self._on_paste_node) # drop and drop events self.connect("drag-begin", self._on_drag_begin) self.connect("drag-end", self._on_drag_end) self.connect("drag-motion", self._on_drag_motion) self.connect("drag-drop", self._on_drag_drop) self.connect("drag-data-delete", self._on_drag_data_delete) self.connect("drag-data-get", self._on_drag_data_get) self.connect("drag-data-received", self._on_drag_data_received) # configure drag and drop events self.enable_model_drag_source( gtk.gdk.BUTTON1_MASK, [DROP_TREE_MOVE], gtk.gdk.ACTION_MOVE) self.drag_source_set( gtk.gdk.BUTTON1_MASK, [DROP_TREE_MOVE], gtk.gdk.ACTION_MOVE) self.enable_model_drag_dest([DROP_TREE_MOVE, DROP_URI], gtk.gdk.ACTION_MOVE| gtk.gdk.ACTION_COPY| gtk.gdk.ACTION_LINK) self.drag_dest_set(gtk.DEST_DEFAULT_HIGHLIGHT | gtk.DEST_DEFAULT_MOTION, [DROP_TREE_MOVE, DROP_URI], gtk.gdk.ACTION_DEFAULT| gtk.gdk.ACTION_MOVE| gtk.gdk.ACTION_COPY| gtk.gdk.ACTION_LINK| gtk.gdk.ACTION_PRIVATE| gtk.gdk.ACTION_ASK) def set_master_node(self, node): self._master_node = node if self.rich_model: self.rich_model.set_master_node(node) def get_master_node(self): return self._master_node def set_notebook(self, notebook): self._notebook = notebook # NOTE: not used yet if self.model: if hasattr(self.model, "get_model"): self.model.get_model().set_notebook(notebook) else: self.model.set_notebook(notebook) def set_model(self, model): """Set the model for the view""" # TODO: could group signal IDs into lists, for each detach # if model already attached, disconnect all of its signals if self.model is not None: self.rich_model.disconnect(self.changed_start_id) self.rich_model.disconnect(self.changed_end_id) self.rich_model.disconnect(self.insert_id) self.rich_model.disconnect(self.delete_id) self.rich_model.disconnect(self.has_child_id) self._node_col = None self._get_icon = None # set new model self.model = model self.rich_model = None gtk.TreeView.set_model(self, self.model) # set new model if self.model is not None: # look to see if model has an inner model (happens when we have # sorting models) if hasattr(self.model, "get_model"): self.rich_model = self.model.get_model() else: self.rich_model = model # init signals for model self.rich_model.set_notebook(self._notebook) self.changed_start_id = self.rich_model.connect("node-changed-start", self._on_node_changed_start) self.changed_end_id = self.rich_model.connect("node-changed-end", self._on_node_changed_end) self._node_col = self.rich_model.get_node_column_pos() self._get_icon = lambda row: \ self.model.get_value(row, self.rich_model.get_column_by_name("icon").pos) self.insert_id = self.model.connect("row-inserted", self.on_row_inserted) self.delete_id = self.model.connect("row-deleted", self.on_row_deleted) self.has_child_id = self.model.connect( "row-has-child-toggled", self.on_row_has_child_toggled) def set_popup_menu(self, menu): self._menu = menu def get_popup_menu(self): return self._menu def popup_menu(self, x, y, button, time): """Display popup menu""" if self._menu is None: return path = self.get_path_at_pos(int(x), int(y)) if path is None: return False path = path[0] if not self.get_selection().path_is_selected(path): self.get_selection().unselect_all() self.get_selection().select_path(path) self._menu.popup(None, None, None, button, time) self._menu.show() return True #========================================= # model change callbacks def _on_node_changed_start(self, model, nodes): # remember which nodes are selected self.__sel_nodes2 = list(self.__sel_nodes) # suppress selection changes while nodes are changing self.__suppress_sel = True # cancel editing self.cancel_editing() # save scrolling self.__scroll = self.widget_to_tree_coords(0, 0) def _on_node_changed_end(self, model, nodes): # maintain proper expansion for node in nodes: if node == self._master_node: for child in node.get_children(): if self.is_node_expanded(child): path = get_path_from_node(self.model, child, self.rich_model.get_node_column_pos()) self.expand_row(path, False) else: try: path = get_path_from_node(self.model, node, self.rich_model.get_node_column_pos()) except: path = None if path is not None: parent = node.get_parent() # NOTE: parent may lose expand state if it has one child # therefore, we should expand parent if it exists and is # visible (i.e. len(path)>1) in treeview if parent and self.is_node_expanded(parent) and \ len(path) > 1: self.expand_row(path[:-1], False) if self.is_node_expanded(node): self.expand_row(path, False) # if nodes still exist, and expanded, try to reselect them sel_count = 0 selection = self.get_selection() for node in self.__sel_nodes2: sel_count += 1 if node.is_valid(): path2 = get_path_from_node(self.model, node, self.rich_model.get_node_column_pos()) if (path2 is not None and (len(path2) <= 1 or self.row_expanded(path2[:-1]))): # reselect and scroll to node selection.select_path(path2) # restore scroll gobject.idle_add(lambda : self.scroll_to_point(*self.__scroll)) # resume emitting selection changes self.__suppress_sel = False # emit de-selection if sel_count == 0: self.select_nodes([]) def __on_select_changed(self, treeselect): """Keep track of which nodes are selected""" #model, paths = treeselect.get_selected_rows() #self.__sel_nodes = [self.model.get_value(self.model.get_iter(path), # self._node_col) # for path in paths] self.__sel_nodes = self.get_selected_nodes() if self.__suppress_sel: self.get_selection().stop_emission("changed") def is_node_expanded(self, node): # query expansion from nodes return node.get_attr("expanded", False) def set_node_expanded(self, node, expand): # save expansion in node node.set_attr("expanded", expand) # TODO: do I notify listeners of expand change # Will this interfere with on_node_changed callbacks def _on_row_expanded(self, treeview, it, path): """Callback for row expand Performs smart expansion (remembers children expansion)""" # save expansion in node self.set_node_expanded(self.model.get_value(it, self._node_col), True) # recursively expand nodes that should be expanded def walk(it): child = self.model.iter_children(it) while child: node = self.model.get_value(child, self._node_col) if self.is_node_expanded(node): path = self.model.get_path(child) self.expand_row(path, False) walk(child) child = self.model.iter_next(child) walk(it) def _on_row_collapsed(self, treeview, it, path): # save expansion in node self.set_node_expanded(self.model.get_value(it, self._node_col), False) def on_row_inserted(self, model, path, it): pass def on_row_deleted(self, model, path): pass def on_row_has_child_toggled(self, model, path, it): pass def cancel_editing(self): if self.editing: self.set_cursor_on_cell(self.editing, None, None, False) #self.cell_text.stop_editing(True) #=========================================== # actions def expand_node(self, node): """Expand a node in TreeView""" path = get_path_from_node(self.model, node, self.rich_model.get_node_column_pos()) if path is not None: self.expand_to_path(path) def collapse_all_beneath(self, path): """Collapse all children beneath a path""" it = self.model.get_iter(path) def walk(it): for child in iter_children(self.model, it): walk(child) path2 = self.model.get_path(it) self.collapse_row(path2) walk(it) #=========================================== # selection def select_nodes(self, nodes): """Select nodes in treeview""" # NOTE: for now only select one node if len(nodes) > 0: node = nodes[0] path = get_path_from_node(self.model, node, self.rich_model.get_node_column_pos()) if path is not None: if len(path) > 1: self.expand_to_path(path[:-1]) self.set_cursor(path) gobject.idle_add(lambda: self.scroll_to_cell(path)) else: # unselect all nodes self.get_selection().unselect_all() def on_select_changed(self, treeselect): """Callback for when selection changes""" nodes = self.get_selected_nodes() self.emit("select-nodes", nodes) return True def get_selected_nodes(self): """Returns a list of currently selected nodes""" iters = self.get_selected_iters() if len(iters) == 0: if self.editing: node = self._get_node_from_path(self.editing) if node: return [node] return [] else: return [self.model.get_value(it, self._node_col) for it in iters] def get_selected_iters(self): """Return a list of currently selected TreeIter's""" iters = [] self.get_selection().selected_foreach(lambda model, path, it: iters.append(it)) return iters # TODO: add a reselect if node is deleted # select next sibling or parent #============================================ # editing titles def on_editing_started(self, cellrenderer, editable, path): """Callback for start of title editing""" # remember editing state self.editing = path gobject.idle_add(lambda: self.scroll_to_cell(path)) def on_editing_canceled(self, cellrenderer): """Callback for canceled of title editing""" # remember editing state self.editing = None def on_edit_title(self, cellrenderertext, path, new_text): """Callback for completion of title editing""" # remember editing state self.editing = None new_text = unicode_gtk(new_text) # get node being edited node = self.model.get_value(self.model.get_iter(path), self._node_col) if node is None: return # do not allow empty names if new_text.strip() == "": return # set new title and catch errors if new_text != node.get_title(): try: node.rename(new_text) except NoteBookError, e: self.emit("error", e.msg, e) # reselect node # NOTE: I select the root inorder for set_cursor(path) to really take # effect (gtk seems to ignore a select call if it "thinks" the path # is selected) #if self.model.iter_n_children(None) > 0: # self.set_cursor((0,)) path = get_path_from_node(self.model, node, self.rich_model.get_node_column_pos()) if path is not None: self.set_cursor(path) gobject.idle_add(lambda: self.scroll_to_cell(path)) self.emit("edit-title", node, new_text)
def expand_node(self, node): """Expand a node in TreeView""" path = get_path_from_node(self.model, node, self.rich_model.get_node_column_pos()) if path is not None: self.expand_to_path(path)
def edit_node(self, node): path = treemodel.get_path_from_node(self.model, node, self.rich_model.get_node_column_pos()) gobject.idle_add(lambda: self.set_cursor_on_cell(path, self.column, self.cell_text, True))
class KeepNoteBaseTreeView (gtk.TreeView): """Base class for treeviews of a NoteBook notes""" def __init__(self): gtk.TreeView.__init__(self) self.model = None self.rich_model = None self._notebook = None self._master_node = None self.editing_path = False self.__sel_nodes = [] self.__sel_nodes2 = [] self.__scroll = (0, 0) self.__suppress_sel = False self._node_col = None self._get_icon = None self._get_node = self._get_node_default self._date_formats = {} self._menu = None # special attr's self._attr_title = "title" self._attr_icon = "icon" self._attr_icon_open = "icon_open" # selection self.get_selection().connect("changed", self.__on_select_changed) self.get_selection().connect("changed", self.on_select_changed) # row expand/collapse self.connect("row-expanded", self._on_row_expanded) self.connect("row-collapsed", self._on_row_collapsed) # drag and drop state self._is_dragging = False # whether drag is in progress self._drag_count = 0 self._dest_row = None # current drag destition self._reorder = REORDER_ALL # enum determining the kind of reordering # that is possible via drag and drop # region, defined by number of vertical pixels from top and bottom of # the treeview widget, where drag scrolling will occur self._drag_scroll_region = 30 # clipboard self.connect("copy-clipboard", self._on_copy_node) self.connect("copy-tree-clipboard", self._on_copy_tree) self.connect("cut-clipboard", self._on_cut_node) self.connect("paste-clipboard", self._on_paste_node) # drop and drop events self.connect("drag-begin", self._on_drag_begin) self.connect("drag-end", self._on_drag_end) self.connect("drag-motion", self._on_drag_motion) self.connect("drag-drop", self._on_drag_drop) self.connect("drag-data-delete", self._on_drag_data_delete) self.connect("drag-data-get", self._on_drag_data_get) self.connect("drag-data-received", self._on_drag_data_received) # configure drag and drop events self.enable_model_drag_source( gtk.gdk.BUTTON1_MASK, [DROP_TREE_MOVE], gtk.gdk.ACTION_MOVE) self.drag_source_set( gtk.gdk.BUTTON1_MASK, [DROP_TREE_MOVE], gtk.gdk.ACTION_MOVE) self.enable_model_drag_dest([DROP_TREE_MOVE, DROP_URI], gtk.gdk.ACTION_MOVE | gtk.gdk.ACTION_COPY | gtk.gdk.ACTION_LINK) self.drag_dest_set( gtk.DEST_DEFAULT_HIGHLIGHT | gtk.DEST_DEFAULT_MOTION, [DROP_TREE_MOVE, DROP_URI], gtk.gdk.ACTION_DEFAULT | gtk.gdk.ACTION_MOVE | gtk.gdk.ACTION_COPY | gtk.gdk.ACTION_LINK | gtk.gdk.ACTION_PRIVATE | gtk.gdk.ACTION_ASK) def set_master_node(self, node): self._master_node = node if self.rich_model: self.rich_model.set_master_node(node) def get_master_node(self): return self._master_node def set_notebook(self, notebook): self._notebook = notebook # NOTE: not used yet if self.model: if hasattr(self.model, "get_model"): self.model.get_model().set_notebook(notebook) else: self.model.set_notebook(notebook) def set_get_node(self, get_node_func=None): if get_node_func is None: self._get_node = self._get_node_default else: self._get_node = get_node_func def _get_node_default(self, nodeid): if self._notebook is None: return None return self._notebook.get_node_by_id(nodeid) def set_model(self, model): """Set the model for the view""" # TODO: could group signal IDs into lists, for each detach # if model already attached, disconnect all of its signals if self.model is not None: self.rich_model.disconnect(self.changed_start_id) self.rich_model.disconnect(self.changed_end_id) self.model.disconnect(self.insert_id) self.model.disconnect(self.delete_id) self.model.disconnect(self.has_child_id) self._node_col = None self._get_icon = None # set new model self.model = model self.rich_model = None gtk.TreeView.set_model(self, self.model) # set new model if self.model is not None: # look to see if model has an inner model (happens when we have # sorting models) if hasattr(self.model, "get_model"): self.rich_model = self.model.get_model() else: self.rich_model = model # init signals for model self.rich_model.set_notebook(self._notebook) self.changed_start_id = self.rich_model.connect( "node-changed-start", self._on_node_changed_start) self.changed_end_id = self.rich_model.connect( "node-changed-end", self._on_node_changed_end) self._node_col = self.rich_model.get_node_column_pos() self._get_icon = lambda row: \ self.model.get_value( row, self.rich_model.get_column_by_name("icon").pos) self.insert_id = self.model.connect("row-inserted", self.on_row_inserted) self.delete_id = self.model.connect("row-deleted", self.on_row_deleted) self.has_child_id = self.model.connect( "row-has-child-toggled", self.on_row_has_child_toggled) def set_popup_menu(self, menu): self._menu = menu def get_popup_menu(self): return self._menu def popup_menu(self, x, y, button, time): """Display popup menu""" if self._menu is None: return path = self.get_path_at_pos(int(x), int(y)) if path is None: return False path = path[0] if not self.get_selection().path_is_selected(path): self.get_selection().unselect_all() self.get_selection().select_path(path) self._menu.popup(None, None, None, button, time) self._menu.show() return True #======================================== # columns def clear_columns(self): for col in reversed(self.get_columns()): self.remove_column(col) def get_column_by_attr(self, attr): for col in self.get_columns(): if col.attr == attr: return col return None def _add_title_render(self, column, attr): # make sure icon attributes are in model self._add_model_column(self._attr_icon) self._add_model_column(self._attr_icon_open) # add renders cell_icon = self._add_pixbuf_render( column, self._attr_icon, self._attr_icon_open) title_text = self._add_text_render( column, attr, editable=True, validator=TextRendererValidator(validate=lambda x: x != "")) # record reference to title_text renderer self.title_text = title_text return cell_icon, title_text def _add_text_render(self, column, attr, editable=False, validator=TextRendererValidator()): # cell renderer text cell = gtk.CellRendererText() cell.set_fixed_height_from_font(1) column.pack_start(cell, True) column.add_attribute(cell, 'text', self.rich_model.get_column_by_name(attr).pos) column.add_attribute( cell, 'cell-background', self.rich_model.add_column( "title_bgcolor", str, lambda node: node.get_attr("title_bgcolor", None)).pos) column.add_attribute( cell, 'foreground', self.rich_model.add_column( "title_fgcolor", str, lambda node: node.get_attr("title_fgcolor", None)).pos) # set edit callbacks if editable: cell.connect("edited", lambda r, p, t: self.on_edit_attr( r, p, attr, t, validator=validator)) cell.connect("editing-started", lambda r, e, p: self.on_editing_started(r, e, p, attr, validator)) cell.connect("editing-canceled", self.on_editing_canceled) cell.set_property("editable", True) return cell def _add_pixbuf_render(self, column, attr, attr_open=None): cell = gtk.CellRendererPixbuf() column.pack_start(cell, False) column.add_attribute(cell, 'pixbuf', self.rich_model.get_column_by_name(attr).pos) #column.add_attribute( # cell, 'cell-background', # self.rich_model.add_column( # "title_bgcolor", str, # lambda node: node.get_attr("title_bgcolor", None)).pos) if attr_open: column.add_attribute( cell, 'pixbuf-expander-open', self.rich_model.get_column_by_name(attr_open).pos) return cell def _get_model_column(self, attr, mapfunc=lambda x: x): col = self.rich_model.get_column_by_name(attr) if col is None: self._add_model_column(attr, add_sort=False, mapfunc=mapfunc) col = self.rich_model.get_column_by_name(attr) return col def get_col_type(self, datatype): if datatype == "string": return str elif datatype == "integer": return int elif datatype == "float": return float elif datatype == "timestamp": return str else: return str def get_col_mapfunc(self, datatype): if datatype == "timestamp": return self.format_timestamp else: return lambda x: x def _add_model_column(self, attr, add_sort=True, mapfunc=lambda x: x): # get attribute definition from notebook attr_def = self._notebook.attr_defs.get(attr) # get datatype if attr_def is not None: datatype = attr_def.datatype default = attr_def.default else: datatype = "string" default = "" # value fetching get = lambda node: mapfunc(node.get_attr(attr, default)) # get coltype mapfunc_sort = lambda x: x if datatype == "string": coltype = str coltype_sort = str mapfunc_sort = lambda x: x.lower() elif datatype == "integer": coltype = int coltype_sort = int elif datatype == "float": coltype = float coltype_sort = float elif datatype == "timestamp": mapfunc = self.format_timestamp coltype = str coltype_sort = int else: coltype = str coltype_sort = str # builtin column types if attr == self._attr_icon: coltype = gdk.Pixbuf coltype_sort = None get = lambda node: get_node_icon(node, False, node in self.rich_model.fades) elif attr == self._attr_icon_open: coltype = gdk.Pixbuf coltype_sort = None get = lambda node: get_node_icon(node, True, node in self.rich_model.fades) # get/make model column col = self.rich_model.get_column_by_name(attr) if col is None: col = treemodel.TreeModelColumn(attr, coltype, attr=attr, get=get) self.rich_model.append_column(col) # define column sorting if add_sort and coltype_sort is not None: attr_sort = attr + "_sort" col = self.rich_model.get_column_by_name(attr_sort) if col is None: get_sort = lambda node: mapfunc_sort( node.get_attr(attr, default)) col = treemodel.TreeModelColumn( attr_sort, coltype_sort, attr=attr, get=get_sort) self.rich_model.append_column(col) def set_date_formats(self, formats): """Sets the date formats of the treemodel""" self._date_formats = formats def format_timestamp(self, timestamp): return (get_str_timestamp(timestamp, formats=self._date_formats) if timestamp is not None else u"") #========================================= # model change callbacks def _on_node_changed_start(self, model, nodes): # remember which nodes are selected self.__sel_nodes2 = list(self.__sel_nodes) # suppress selection changes while nodes are changing self.__suppress_sel = True # cancel editing self.cancel_editing() # save scrolling self.__scroll = self.widget_to_tree_coords(0, 0) def _on_node_changed_end(self, model, nodes): # maintain proper expansion for node in nodes: if node == self._master_node: for child in node.get_children(): if self.is_node_expanded(child): path = get_path_from_node( self.model, child, self.rich_model.get_node_column_pos()) self.expand_row(path, False) else: try: path = get_path_from_node( self.model, node, self.rich_model.get_node_column_pos()) except: path = None if path is not None: parent = node.get_parent() # NOTE: parent may lose expand state if it has one child # therefore, we should expand parent if it exists and is # visible (i.e. len(path)>1) in treeview if (parent and self.is_node_expanded(parent) and len(path) > 1): self.expand_row(path[:-1], False) if self.is_node_expanded(node): self.expand_row(path, False) # if nodes still exist, and expanded, try to reselect them sel_count = 0 selection = self.get_selection() for node in self.__sel_nodes2: sel_count += 1 if node.is_valid(): path2 = get_path_from_node( self.model, node, self.rich_model.get_node_column_pos()) if (path2 is not None and (len(path2) <= 1 or self.row_expanded(path2[:-1]))): # reselect and scroll to node selection.select_path(path2) # restore scroll gobject.idle_add(lambda: self.scroll_to_point(*self.__scroll)) # resume emitting selection changes self.__suppress_sel = False # emit de-selection if sel_count == 0: self.select_nodes([]) def __on_select_changed(self, treeselect): """Keep track of which nodes are selected""" self.__sel_nodes = self.get_selected_nodes() if self.__suppress_sel: self.get_selection().stop_emission("changed") def is_node_expanded(self, node): # query expansion from nodes return node.get_attr("expanded", False) def set_node_expanded(self, node, expand): # save expansion in node node.set_attr("expanded", expand) # TODO: do I notify listeners of expand change # Will this interfere with on_node_changed callbacks def _on_row_expanded(self, treeview, it, path): """Callback for row expand Performs smart expansion (remembers children expansion)""" # save expansion in node self.set_node_expanded(self.model.get_value(it, self._node_col), True) # recursively expand nodes that should be expanded def walk(it): child = self.model.iter_children(it) while child: node = self.model.get_value(child, self._node_col) if self.is_node_expanded(node): path = self.model.get_path(child) self.expand_row(path, False) walk(child) child = self.model.iter_next(child) walk(it) def _on_row_collapsed(self, treeview, it, path): # save expansion in node self.set_node_expanded(self.model.get_value(it, self._node_col), False) def on_row_inserted(self, model, path, it): pass def on_row_deleted(self, model, path): pass def on_row_has_child_toggled(self, model, path, it): pass def cancel_editing(self): if self.editing_path: self.set_cursor_on_cell(self.editing_path, None, None, False) #=========================================== # actions def expand_node(self, node): """Expand a node in TreeView""" path = get_path_from_node(self.model, node, self.rich_model.get_node_column_pos()) if path is not None: self.expand_to_path(path) def collapse_all_beneath(self, path): """Collapse all children beneath a path""" it = self.model.get_iter(path) def walk(it): for child in iter_children(self.model, it): walk(child) path2 = self.model.get_path(it) self.collapse_row(path2) walk(it) #=========================================== # selection def select_nodes(self, nodes): """Select nodes in treeview""" # NOTE: for now only select one node if len(nodes) > 0: node = nodes[0] path = get_path_from_node(self.model, node, self.rich_model.get_node_column_pos()) if path is not None: if len(path) > 1: self.expand_to_path(path[:-1]) self.set_cursor(path) gobject.idle_add(lambda: self.scroll_to_cell(path)) else: # unselect all nodes self.get_selection().unselect_all() def on_select_changed(self, treeselect): """Callback for when selection changes""" nodes = self.get_selected_nodes() self.emit("select-nodes", nodes) return True def get_selected_nodes(self): """Returns a list of currently selected nodes""" iters = self.get_selected_iters() if len(iters) == 0: if self.editing_path: node = self._get_node_from_path(self.editing_path) if node: return [node] return [] else: return [self.model.get_value(it, self._node_col) for it in iters] def get_selected_iters(self): """Return a list of currently selected TreeIter's""" iters = [] self.get_selection().selected_foreach(lambda model, path, it: iters.append(it)) return iters # TODO: add a reselect if node is deleted # select next sibling or parent #============================================ # editing attr def on_editing_started(self, cellrenderer, editable, path, attr, validator=TextRendererValidator()): """Callback for start of title editing""" # remember editing state self.editing_path = path # get node being edited and init gtk.Entry widget node = self.model.get_value(self.model.get_iter(path), self._node_col) if node is not None: val = node.get_attr(attr) try: editable.set_text(validator.format(val)) except: pass gobject.idle_add(lambda: self.scroll_to_cell(path)) def on_editing_canceled(self, cellrenderer): """Callback for canceled of title editing""" # remember editing state self.editing_path = None def on_edit_attr(self, cellrenderertext, path, attr, new_text, validator=TextRendererValidator()): """Callback for completion of title editing""" # remember editing state self.editing_path = None new_text = unicode_gtk(new_text) # get node being edited node = self.model.get_value(self.model.get_iter(path), self._node_col) if node is None: return # determine value from new_text, if invalid, ignore it try: new_val = validator.parse(new_text) except: return # set new attr and catch errors try: node.set_attr(attr, new_val) except NoteBookError, e: self.emit("error", e.msg, e) # reselect node # need to get path again because sorting may have changed path = get_path_from_node(self.model, node, self.rich_model.get_node_column_pos()) if path is not None: self.set_cursor(path) gobject.idle_add(lambda: self.scroll_to_cell(path)) self.emit("edit-node", node, attr, new_val)