コード例 #1
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([])
コード例 #2
0
ファイル: basetreeview.py プロジェクト: gemagomez/keepnote
    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([])
コード例 #3
0
ファイル: listview.py プロジェクト: gemagomez/keepnote
 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)
コード例 #4
0
ファイル: listview.py プロジェクト: InfosecSapper/keepnote
 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)
コード例 #5
0
ファイル: listview.py プロジェクト: gemagomez/keepnote
    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)
コード例 #6
0
    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)
コード例 #7
0
ファイル: listview.py プロジェクト: InfosecSapper/keepnote
    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)
コード例 #8
0
ファイル: listview.py プロジェクト: gemagomez/keepnote
    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)        
コード例 #9
0
ファイル: listview.py プロジェクト: InfosecSapper/keepnote
    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)
コード例 #10
0
ファイル: basetreeview.py プロジェクト: vatslav/perfectnote
    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()
コード例 #11
0
ファイル: listview.py プロジェクト: InfosecSapper/keepnote
    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", [])
コード例 #12
0
ファイル: listview.py プロジェクト: gemagomez/keepnote
    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", [])
コード例 #13
0
ファイル: basetreeview.py プロジェクト: vatslav/perfectnote
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)
コード例 #14
0
ファイル: basetreeview.py プロジェクト: vatslav/perfectnote
 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)
コード例 #15
0
 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))
コード例 #16
0
ファイル: basetreeview.py プロジェクト: uid-root/keepnote
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)