Search code examples
pythonqmlpyside2

PySide2/QML append actions to ToolBar and navigate hierarchical


I'm trying to achieve same thing as mentioned in below post with QML.
Breadcrumbs navigation using QToolBar and QListView

I'm not able to figure out, How to append ToolButton via PySide2 to QML ToolBar and update Gridview relatively (based on given hierarchical data).

main.qml

import QtQuick 2.13
import QtQuick.Controls 2.13
import QtQuick.Layouts 1.13
import QtQuick.Controls.Styles 1.4

ApplicationWindow {
    id: mainWindowId
    visible: true
    width: 960
    height: 540
    title: qsTr("Breadcrumbs Test")

    Rectangle {
        width: parent.width
        height: parent.height

        ColumnLayout {
            width: parent.width
            height: parent.height
            spacing: 6

            TextField {
                id: filterTextFieldId
                Layout.fillWidth: true
                Layout.preferredHeight: 40
                font {
                    family: "SF Pro Display"
                 pixelSize: 22
                }
                placeholderText: "Type Filter Expression"
                color: "dodgerblue"
            }

            ToolBar {

                background: Rectangle {
                    color: "transparent"
                }

                RowLayout {
                    anchors.fill: parent
                    spacing: 10

                    ToolButton {
                        Layout.preferredHeight: 20
                        contentItem: Text {
                                text: qsTr('Home')
                                color: "#FFFFFF"
                                horizontalAlignment: Text.AlignHCenter
                                verticalAlignment: Text.AlignVCenter
                            }
                        background: Rectangle {
                                    radius: 12
                                    color:  "#40e0d0"
                                }
                        onClicked: crumbsNavigation.on_buttonTriggered()
                    }

                    ToolButton {
                        Layout.preferredHeight: 20
                        contentItem: Text {
                                text: qsTr('About')
                                color: "#FFFFFF"
                                horizontalAlignment: Text.AlignHCenter
                                verticalAlignment: Text.AlignVCenter
                            }
                        background: Rectangle {
                                    radius: 12
                                    color:  "#40e0d0"
                                }
                        onClicked: crumbsNavigation.on_buttonTriggered()
                    }

                    ToolButton {
                        Layout.preferredHeight: 20
                        contentItem: Text {
                                text: qsTr('Contact')
                                color: "#FFFFFF"
                                horizontalAlignment: Text.AlignHCenter
                                verticalAlignment: Text.AlignVCenter
                            }
                        background: Rectangle {
                                    radius: 12
                                    color:  "#40e0d0"
                                }
                        onClicked: crumbsNavigation.on_buttonTriggered()
                    }
                }
            }

            Rectangle {
                Layout.fillWidth: true
                Layout.fillHeight: true
                color: "dodgerblue"

                GridView {
                    id: crumbsViewId
                    width: parent.width
                    height: parent.height
                    anchors.fill: parent
                    anchors.margins: 12
                    cellWidth: 130
                    cellHeight: 130
                    model: crumbsNavigation.model
                    delegate: Text {text:qsTr('Hello'); color:"white"}
                    focus: true

                }
            }
        }
    }
}

qmlBreadcrumbs.py

from PySide2 import QtCore, QtQuick, QtGui, QtWidgets, QtQml
import os
import sys
import re

crumbs_data = {"books":{
    "web":{
      "front-end":{
        "html":["the missing manual", "core html5 canvas"],
        "css":["css pocket reference", "css in depth"],
        "js":["you don't know js", "eloquent javascript"]
      },
      "back-end":{
        "php":["modern php", "php web services"],
        "python":["dive into python", "python for everybody", 
        "Think Python", "Effective Python", "Fluent Python"]
      }
    },
    "database":{
      "sql":{
        "mysql":["mysql in a nutshell", "mysql cookbook"],
        "postgresql":["postgresql up and running", "practical postgresql"]
      },
      "nosql":{
        "mongodb":["mongodb in action", "scaling mongodb"],
        "cassandra":["practical cassandra", "mastering cassandra"]
}}}}


def dict_to_model(item, d):
    if isinstance(d, dict):
        for k, v in d.items():
            it = QtGui.QStandardItem(k)
            item.appendRow(it)
            dict_to_model(it, v)
    elif isinstance(d, list):
        for v in d:
            dict_to_model(item, v)
    else:
        item.appendRow(QtGui.QStandardItem(str(d)))

class crumbsNavigation(QtCore.QObject):
    clicked = QtCore.Signal(QtCore.QModelIndex)

    def __init__(self, json_data, parent=None):
        super(crumbsNavigation, self).__init__(parent)

        self.model =  QtGui.QStandardItemModel(self)
        dict_to_model(self.model.invisibleRootItem(), json_data)
        it = self.model.item(0, 0)
        ix = self.model.indexFromItem(it)

    @QtCore.Slot(QtCore.QModelIndex)
    def on_clicked(self, index):
        if not self.model.hasChildren(index):
            self.clicked.emit(index)
            return
        action = self.toolbar.addAction(index.data())
        action.setData(QtCore.QPersistentModelIndex(index))
        self.listview.setRootIndex(index)

    @QtCore.Slot(QtWidgets.QAction)
    def on_actionTriggered(self, action):
        ix = action.data()
        model = ix.model()
        self.listview.setRootIndex(QtCore.QModelIndex(ix))
        self.toolbar.clear()
        ixs = []
        while  ix.isValid():
            ixs.append(ix)
            ix = ix.parent()
        for ix in reversed(ixs):
            action = self.toolbar.addAction(ix.data())
            action.setData(ix)

    @QtCore.Slot()
    def on_buttonTriggered(self):
        print('Toolbutton Triggered')

if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    engine = QtQml.QQmlApplicationEngine()
    crumbObject = crumbsNavigation(crumbs_data)
    engine.rootContext().setContextProperty("crumbsNavigation", crumbObject)
    engine.load(QtCore.QUrl.fromLocalFile('E:/Tech/main.qml'))
    if not engine.rootObjects():
        sys.exit(-1)
    engine.quit.connect(app.quit)
    sys.exit(app.exec_())

enter image description here


Solution

  • The logic in QML is the same in Qt Widgets but not with the same elements since for example neither QAction nor QToolBar exist in QML.

    I will summarize the logic in the following:

    • The rootIndex of the view (ListView/QListView) must be updated when an item in the view or the ToolBar/QToolBar is pressed.

    • ToolBar/QToolBar items must be rootIndex and its parents.

    On the other hand, the ListView does not allow to establish a rootIndex unlike QListView, so to implement the same functionality you must use DelegateModel.

    On the python side I implement a class that handles navigation, having for this the properties:

    • model: It has the information in a hierarchical way.
    • headers: It has the information of the text and the QModelIndex of the ToolBar.
    • rootIndex.

    main.py

    from PySide2 import QtCore, QtGui, QtWidgets, QtQml
    
    
    crumbs_data = # ...
    
    def dict_to_model(item, d):
        if isinstance(d, dict):
            for k, v in d.items():
                it = QtGui.QStandardItem(k)
                item.appendRow(it)
                dict_to_model(it, v)
        elif isinstance(d, list):
            for v in d:
                dict_to_model(item, v)
        else:
            item.appendRow(QtGui.QStandardItem(str(d)))
    
    
    class NavigationManager(QtCore.QObject):
        headersChanged = QtCore.Signal()
        rootIndexChanged = QtCore.Signal("QModelIndex")
    
        def __init__(self, json_data, parent=None):
            super().__init__(parent)
    
            self.m_model = QtGui.QStandardItemModel(self)
            dict_to_model(self.m_model.invisibleRootItem(), json_data)
    
            self.m_headers = []
            self.m_rootindex = QtCore.QModelIndex()
            self.rootIndexChanged.connect(self._update_headers)
    
            self.rootIndex = self.m_model.index(0, 0)
    
        def _update_headers(self, ix):
            self.m_headers = []
            while ix.isValid():
                self.m_headers.insert(0, [ix, ix.data()])
                ix = ix.parent()
            self.headersChanged.emit()
    
        @QtCore.Property(QtCore.QObject, constant=True)
        def model(self):
            return self.m_model
    
        @QtCore.Property("QVariantList", notify=headersChanged)
        def headers(self):
            return self.m_headers
    
        def get_root_index(self):
            return self.m_rootindex
    
        def set_root_index(self, ix):
            if self.m_rootindex != ix:
                self.m_rootindex = ix
                self.rootIndexChanged.emit(ix)
    
        rootIndex = QtCore.Property(
            "QModelIndex", fget=get_root_index, fset=set_root_index, notify=rootIndexChanged
        )
    
    
    if __name__ == "__main__":
        import os
        import sys
    
        navigation_manager = NavigationManager(crumbs_data)
    
        model = QtGui.QStandardItemModel()
        app = QtWidgets.QApplication(sys.argv)
        engine = QtQml.QQmlApplicationEngine()
        engine.rootContext().setContextProperty("navigation_manager", navigation_manager)
        current_dir = os.path.dirname(os.path.realpath(__file__))
        filename = os.path.join(current_dir, "main.qml")
        engine.load(QtCore.QUrl.fromLocalFile(filename))
        if not engine.rootObjects():
            sys.exit(-1)
        engine.quit.connect(app.quit)
        sys.exit(app.exec_())
    

    main.qml

    import QtQuick 2.13
    import QtQuick.Controls 2.13
    import QtQuick.Layouts 1.13
    import QtQml.Models 2.13
    
    ApplicationWindow {
        id: mainWindowId
        visible: true
        width: 960
        height: 540
        title: qsTr("Breadcrumbs Test")
    
        Rectangle {
            width: parent.width
            height: parent.height
    
            ColumnLayout {
                width: parent.width
                height: parent.height
                spacing: 6
    
                TextField {
                    id: filterTextFieldId
                    Layout.fillWidth: true
                    Layout.preferredHeight: 40
                    font {
                        family: "SF Pro Display"
                     pixelSize: 22
                    }
                    placeholderText: "Type Filter Expression"
                    color: "dodgerblue"
                }
    
                ToolBar {
                    background: Rectangle {
                        color: "transparent"
                    }
                    RowLayout {
                        anchors.fill: parent
                        spacing: 10
                        Repeater{
                            model: navigation_manager.headers
                            ToolButton {
                                Layout.preferredHeight: 20
                                contentItem: Text {
                                    text: model.modelData[1]
                                    color: "#FFFFFF"
                                    horizontalAlignment: Text.AlignHCenter
                                    verticalAlignment: Text.AlignVCenter
                                }
                                background: Rectangle {
                                    radius: 12
                                    color:  "#40e0d0"
                                }
                                onClicked: navigation_manager.rootIndex = model.modelData[0]
                            }
                        }
                    }
                }
                Rectangle {
                    Layout.fillWidth: true
                    Layout.fillHeight: true
                    color: "dodgerblue"
    
                    ListView{
                        id: view
                        anchors.fill: parent
                        anchors.margins: 12
                        model: DelegateModel {
                            model: navigation_manager.model
                            rootIndex: navigation_manager.rootIndex
                            delegate: Rectangle {
                                height: 25
                                color:"transparent"
                                Text { 
                                    text: model.display
                                    color:"white"
                                    MouseArea{
                                        anchors.fill: parent
                                        onClicked: {
                                            if (model.hasModelChildren)
                                                navigation_manager.rootIndex = view.model.modelIndex(index)
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }