Search code examples
pythonpyqt4qtreewidgetqtreewidgetitem

Derive and set color to the index of newly-added children


I have created subclass of QTreeWidget and QTreeWidgetItem where I am trying to highlight the new added items (where the text will be colored red).

The tree hierarchy that I have adopted is as follows:

|-- parent
|--|-- child
|--|-- child

The tree widget is initially populated from a dictionary.

To get the difference, I did it by converting the current hierarchy in the tree widget into a dictionary and have it compared against the initial dictionary that it was populated with.

However if I add in a new child to an existing parent in which the name of the new child already existed in another parent, the same method as mentioned above does not works, as it will colored the first result that it find.

To replicate:

  • select menuB
  • right mouse click > add new sub item
  • input name: a101
  • hit "Highlight Diff." button
  • a101 child item under menuA is highlighted instead of the one in menuB

What would be the best way to go in getting the index of newly added child(ren)?

Thank you for any replies.

P.S: If anyone has better suggestion for the parent highlighting, please feel free to chip in.

class CustomTreeWidgetItem(QtGui.QTreeWidgetItem):
    def __init__(self, widget=None, text=None, is_tristate=False):
        super(CustomTreeWidgetItem, self).__init__(widget)

        self.setText(0, text)

        if is_tristate:
            # Solely for the Parent item
            self.setFlags(
                self.flags()
                | QtCore.Qt.ItemIsTristate
                | QtCore.Qt.ItemIsEditable
                | QtCore.Qt.ItemIsUserCheckable
            )
        else:
            self.setFlags(
                self.flags()
                | QtCore.Qt.ItemIsEditable
                | QtCore.Qt.ItemIsUserCheckable
            )
            self.setCheckState(0, QtCore.Qt.Unchecked)

    def setData(self, column, role, value):
        state = self.checkState(column)
        QtGui.QTreeWidgetItem.setData(self, column, role, value)
        if (role == QtCore.Qt.CheckStateRole and
            state != self.checkState(column)):
            tree_widget = CustomTreeWidget()
            if tree_widget is not None:
                tree_widget.itemToggled.emit(self, column)



class CustomTreeWidget(QtGui.QTreeWidget):
    def __init__(self, widget=None):
        super(CustomTreeWidget, self).__init__(widget)
        self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
        self.customContextMenuRequested.connect(self.show_custom_menu)


    def show_custom_menu(self):
        base_node = self.selectedItems()[0]

        qmenu = QtGui.QMenu(self)
        remove_action = QtGui.QAction("Remove item", self)
        remove_action.triggered.connect(self.remove_selected_item)
        qmenu.addAction(remove_action)

        # The following options are only effected for top-level items
        # top-level items do not have `parent()`
        if not base_node.parent():
            add_new_child_action = QtGui.QAction("Add new sub item", self)
            add_new_child_action.triggered.connect(
                partial(self.adds_new_child_item, base_node)
            )
            # qmenu.addAction(add_new_child_action)
            qmenu.insertAction(remove_action, add_new_child_action)

        qmenu.exec_(QtGui.QCursor.pos())

    def add_item_dialog(self, title):
        text, ok = QtGui.QInputDialog.getText(
            self,
            "Add {0} Item".format(title),
            "Enter name for {0}-Item:".format(title)
        )
        if ok and text != "":
            return text

    def add_new_parent_item(self):
        input_text = self.add_item_dialog("Parent")
        if input_text:
            CustomTreeWidgetItem(self, input_text, is_tristate=True)

    def adds_new_child_item(self, base_node):

        input_text = self.add_item_dialog("Sub")
        if input_text:
            CustomTreeWidgetItem(base_node, input_text)
            self.setItemExpanded(base_node, True)

    def remove_selected_item(self):
        root = self.invisibleRootItem()
        for item in self.selectedItems():
            (item.parent() or root).removeChild(item)

    def derive_tree_items(self, mode="all"):
        all_items = defaultdict(list)

        root_item = self.invisibleRootItem()
        top_level_count = root_item.childCount()

        for i in range(top_level_count):
            top_level_item = root_item.child(i)
            top_level_item_name = str(top_level_item.text(0))
            child_num = top_level_item.childCount()

            all_items[top_level_item_name] = []

            for n in range(child_num):
                child_item = top_level_item.child(n)
                child_item_name = str(child_item.text(0)) or ""

                if mode == "all":
                    all_items[top_level_item_name].append(child_item_name)

                elif mode == "checked":
                    if child_item.checkState(0) == QtCore.Qt.Checked:
                        all_items[top_level_item_name].append(child_item_name)

                elif mode == "unchecked":
                    if child_item.checkState(0) == QtCore.Qt.Unchecked:
                        all_items[top_level_item_name].append(child_item_name)

        return all_items


class MainApp(QtGui.QWidget):
    def __init__(self):
        super(MainApp, self).__init__()

        # initial dictionary that is populated into the tree widget
        test_dict = {
            "menuA": ["a101", "a102"],
            "menuC": ["c101", "c102", "c103"],
            "menuB": ["b101"],
        }

        self._tree = CustomTreeWidget()
        self._tree.header().hide()

        for pk, pv in sorted(test_dict.items()):
            parent = CustomTreeWidgetItem(self._tree, pk, is_tristate=True)

            for c in pv:
                child = CustomTreeWidgetItem(parent, c)

        self.orig_dict = self._tree.derive_tree_items()

        # Expand the hierarchy by default
        self._tree.expandAll()

        tree_layout = QtGui.QVBoxLayout()
        self.btn1 = QtGui.QPushButton("Add new item")
        self.btn2 = QtGui.QPushButton("Highlight Diff.")
        tree_layout.addWidget(self._tree)
        tree_layout.addWidget(self.btn1)
        tree_layout.addWidget(self.btn2)

        main_layout = QtGui.QHBoxLayout()
        main_layout.addLayout(tree_layout)

        self.setLayout(main_layout)
        self.setup_connections()

    def setup_connections(self):
        self.btn1.clicked.connect(self.add_parent_item)
        self.btn2.clicked.connect(self.highlight_diff)


    def add_parent_item(self):
        # Get current selected in list widget
        # CustomTreeWidgetItem(self._tree, "test", is_tristate=True)
        self._tree.add_new_parent_item()


    def highlight_diff(self):
        self.current_dict = self._tree.derive_tree_items()

        if self.orig_dict != self.current_dict:
            # check for key difference
            diff = [k for k in self.current_dict if k not in self.orig_dict]


            if diff:
                # get the difference of top-level items
                for d in diff:
                    top_item = self._tree.findItems(d, QtCore.Qt.MatchExactly|QtCore.Qt.MatchRecursive)
                    #print aaa[0].childCount()
                    top_item[0].setTextColor(0, QtGui.QColor(255, 0, 0))

                    if top_item[0].childCount():
                        for n in range(top_item[0].childCount()):
                            top_item[0].child(n).setTextColor(0, QtGui.QColor(255, 0, 0))

            # to highlight the child diff. of existing top-items
            # issue with this portion if the new added item name already existed
            for k, v in self.current_dict.items():
                if k in self.orig_dict:
                    diff = set(v).difference(self.orig_dict.get(k), [])

                    for d in diff:
                        child_item = self._tree.findItems(d, QtCore.Qt.MatchExactly|QtCore.Qt.MatchRecursive)
                        child_item[0].setTextColor(0, QtGui.QColor(255, 0, 0))



if __name__ == "__main__":
    app = QtGui.QApplication(sys.argv)
    w = MainApp()
    w.show()
    sys.exit(app.exec_())

Solution

  • You can save in a role a flag indicating if it is a new item or not and change the color using a delegate:

    import sys
    from functools import partial
    from PyQt4 import QtCore, QtGui
    from collections import defaultdict
    
    IsNewItemRole = QtCore.Qt.UserRole + 1000
    
    
    class CustomTreeDelegate(QtGui.QStyledItemDelegate):
        @property
        def text_color(self):
            if not hasattr(self, "_text_color"):
                self._text_color = QtGui.QColor()
            return self._text_color
    
        @text_color.setter
        def text_color(self, color):
            self._text_color = color
    
        def initStyleOption(self, option, index):
            super(CustomTreeDelegate, self).initStyleOption(option, index)
            if self.text_color.isValid() and index.data(IsNewItemRole):
                option.palette.setBrush(QtGui.QPalette.Text, self.text_color)
    
    
    class CustomTreeWidgetItem(QtGui.QTreeWidgetItem):
        def __init__(self, parent=None, text="", is_tristate=False, is_new_item=False):
            super(CustomTreeWidgetItem, self).__init__(parent)
            self.setText(0, text)
            flags = QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsUserCheckable
            if is_tristate:
                flags |= QtCore.Qt.ItemIsTristate
            else:
                self.setCheckState(0, QtCore.Qt.Unchecked)
            self.setFlags(self.flags() | flags)
            self.setData(0, IsNewItemRole, is_new_item)
    
        def setData(self, column, role, value):
            state = self.checkState(column)
            QtGui.QTreeWidgetItem.setData(self, column, role, value)
            if role == QtCore.Qt.CheckStateRole and state != self.checkState(column):
                tree_widget = self.treeWidget()
                if isinstance(tree_widget, CustomTreeWidget):
                    tree_widget.itemToggled.emit(self, column)
    
    
    class CustomTreeWidget(QtGui.QTreeWidget):
        itemToggled = QtCore.pyqtSignal(QtGui.QTreeWidgetItem, int)
    
        def __init__(self, widget=None):
            super(CustomTreeWidget, self).__init__(widget)
            self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
            self.customContextMenuRequested.connect(self.show_custom_menu)
    
        def show_custom_menu(self, pos):
            base_node = self.itemAt(pos)
            if base_node is None:
                return
    
            qmenu = QtGui.QMenu(self)
            remove_action = QtGui.QAction("Remove item", self)
            remove_action.triggered.connect(self.remove_selected_item)
            qmenu.addAction(remove_action)
    
            # The following options are only effected for top-level items
            # top-level items do not have `parent()`
            if base_node.parent() is None:
                add_new_child_action = QtGui.QAction("Add new sub item", self)
                add_new_child_action.triggered.connect(
                    partial(self.adds_new_child_item, base_node)
                )
                # qmenu.addAction(add_new_child_action)
                qmenu.insertAction(remove_action, add_new_child_action)
    
            qmenu.exec_(self.mapToGlobal(pos))
    
        def add_item_dialog(self, title):
            text, ok = QtGui.QInputDialog.getText(
                self, "Add {0} Item".format(title), "Enter name for {0}-Item:".format(title)
            )
            if ok:
                return text
    
        def add_new_parent_item(self):
            input_text = self.add_item_dialog("Parent")
            if input_text:
                it = CustomTreeWidgetItem(
                    self, input_text, is_tristate=True, is_new_item=True
                )
    
        def adds_new_child_item(self, base_node):
            input_text = self.add_item_dialog("Sub")
            if input_text:
                it = CustomTreeWidgetItem(base_node, input_text, is_new_item=True)
                self.setItemExpanded(base_node, True)
                it.setData(0, IsNewItemRole, True)
    
        def remove_selected_item(self):
            root = self.invisibleRootItem()
            for item in self.selectedItems():
                (item.parent() or root).removeChild(item)
    
    
    class MainApp(QtGui.QWidget):
        def __init__(self, parent=None):
            super(MainApp, self).__init__(parent)
    
            # initial dictionary that is populated into the tree widget
            test_dict = {
                "menuA": ["a101", "a102"],
                "menuC": ["c101", "c102", "c103"],
                "menuB": ["b101"],
            }
    
            self._tree = CustomTreeWidget()
            self._tree.header().hide()
    
            self._tree_delegate = CustomTreeDelegate(self._tree)
            self._tree.setItemDelegate(self._tree_delegate)
    
            for pk, pv in sorted(test_dict.items()):
                parent = CustomTreeWidgetItem(self._tree, pk, is_tristate=True)
    
                for c in pv:
                    child = CustomTreeWidgetItem(parent, c)
    
            # Expand the hierarchy by default
            self._tree.expandAll()
    
            tree_layout = QtGui.QVBoxLayout()
            self.btn1 = QtGui.QPushButton("Add new item")
            self.btn2 = QtGui.QPushButton("Highlight Diff.")
            tree_layout.addWidget(self._tree)
            tree_layout.addWidget(self.btn1)
            tree_layout.addWidget(self.btn2)
    
            main_layout = QtGui.QHBoxLayout(self)
            main_layout.addLayout(tree_layout)
    
            self.setup_connections()
    
        def setup_connections(self):
            self.btn1.clicked.connect(self.add_parent_item)
            self.btn2.clicked.connect(self.highlight_diff)
    
        def add_parent_item(self):
            # Get current selected in list widget
            # CustomTreeWidgetItem(self._tree, "test", is_tristate=True)
            self._tree.add_new_parent_item()
    
        def highlight_diff(self):
            self._tree_delegate.text_color = QtGui.QColor(255, 0, 0)
            self._tree.viewport().update()
    
    
    if __name__ == "__main__":
        app = QtGui.QApplication(sys.argv)
        w = MainApp()
        w.show()
        sys.exit(app.exec_())