Search code examples
pythonpyside2qlistviewqtoolbar

Breadcrumbs navigation using QToolBar and QListView


I have JSON data something like this

{"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"]
}}}}

Now I want to populate this data in QListView using QAbstractListModel with the QToolBar as breadcrumb navigation (QAction). QListView will update as user triggers key. So how can I implement this?

breadcrumbs
(source: gifyu.com)


Solution

  • The first thing is to convert the json into a model, in this case QStandardItemModel is used. Then we use that model in the QListView using setRootIndex() to indicate the items that will be displayed, on the other hand QActions are added to the QToolBar using the parent tree.

    from PyQt5 import QtCore, QtGui, QtWidgets
    
    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 Navigation(QtCore.QObject):
        clicked = QtCore.pyqtSignal(QtCore.QModelIndex)
        def __init__(self, json_data, parent=None):
            super(Navigation, self).__init__(parent)
            self.toolbar = QtWidgets.QToolBar()
            self.toolbar.actionTriggered.connect(self.on_actionTriggered)
            self.model =  QtGui.QStandardItemModel(self)
            dict_to_model(self.model.invisibleRootItem(), json_data)
            it = self.model.item(0, 0)
            ix = self.model.indexFromItem(it)
            root_action = self.toolbar.addAction(it.text())
            root_action.setData(QtCore.QPersistentModelIndex(ix))
            self.listview = QtWidgets.QListView()
            self.listview.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
            self.listview.clicked.connect(self.on_clicked)
            self.listview.setModel(self.model)
            self.listview.setRootIndex(ix)
    
        @QtCore.pyqtSlot(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.pyqtSlot(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)
    
    class MainWindow(QtWidgets.QMainWindow):
        def __init__(self, parent=None):
            super(MainWindow, self).__init__(parent)
    
            navigation = Navigation(data, self)
            navigation.clicked.connect(self.on_clicked)
    
            tree_view = QtWidgets.QTreeView()
            tree_view.setModel(navigation.model)
            navigation.model.setHorizontalHeaderLabels(["Tree Example"])
            tree_view.expandAll()
    
            self.addToolBar(navigation.toolbar)
    
            widget = QtWidgets.QWidget()
            self.setCentralWidget(widget)
            lay = QtWidgets.QHBoxLayout(widget)
            lay.addWidget(navigation.listview)
            lay.addWidget(tree_view)
    
        @QtCore.pyqtSlot(QtCore.QModelIndex)
        def on_clicked(self, index):
            print(index.data())
    
    if __name__ == '__main__':
        import sys
        app = QtWidgets.QApplication(sys.argv)
        w = MainWindow()
        w.show()
        sys.exit(app.exec_())