Search code examples
pythonmayapyside2qtguiqtwidgets

Maya UI for browsing assets failing to propagate multiple lists?


I'm creating a script in Maya with python that is designed to navigate local directory folders.

The way I've been asked to make it navigate is with a collection of lists each representing a different directory level.

enter image description here

now on selecting the 1st list [Type] it propagates the 2nd list correctly

giving me this feedback Item clicked on list 0 Received column: 0, Current column index: 0 Correct column Not last column Selected directory: D:\WORKSPACE\new_folderStructure\3D\ASSETS\PROPS Is directory

but when i click on an item on the 2nd list it doesnt seem to respond at all. Item clicked on list 1 Received column: 0, Current column index: 1

I'm very confused.

  • I have tried stripping it back to a more basic script that just adds numbers into each list when you click on an item in the list, this worked just fine.
  • Re-introducing the dir navigation this errored again.
  • I tried changing os.path.join to os.path.normpath()
  • I tried introducing checks to tell me the dir selected and again it just tells me nothing on clicking the 2nd list.
  • I tried changing the item Clicked Function, and separating it into new functions based on which list is interacted with.
  • Even asked chat gtp for help and it just lost its mind.
import os
import maya.cmds as cmds
from PySide2 import QtWidgets, QtGui

class AssetBrowser(QtWidgets.QWidget):
    def __init__(self, asset_dir, parent=None):
        super(AssetBrowser, self).__init__(parent)
        self.asset_dir = asset_dir
        self.levels = 6  # Number of levels to display
        self.list_titles = ["Type", "Category", "Asset", "LOD", "User", "Scene"]  # List titles
        self.file_lists = []  # List to store file list widgets for each level
        self.initUI()

    def initUI(self):
        self.setWindowTitle('Asset Browser')
        layout = QtWidgets.QVBoxLayout(self)

        # Adding the image
        image_label = QtWidgets.QLabel()
        pixmap = QtGui.QPixmap(r"D:\WORKSPACE\safeBank\images\safeBank_logoTitle_v001.png")
        image_label.setPixmap(pixmap)
        layout.addWidget(image_label)

        # Add frame for project settings
        project_frame = QtWidgets.QFrame()
        project_frame.setFrameShape(QtWidgets.QFrame.StyledPanel)
        project_layout = QtWidgets.QHBoxLayout(project_frame)
        layout.addWidget(project_frame)

        # Add button to set project directory
        self.set_project_button = QtWidgets.QPushButton("Set Project")
        self.set_project_button.clicked.connect(self.setProjectDirectory)
        project_layout.addWidget(self.set_project_button)

        # Add line edit for displaying selected folder path
        self.folder_path_lineedit = QtWidgets.QLineEdit()
        self.folder_path_lineedit.setReadOnly(True)
        project_layout.addWidget(self.folder_path_lineedit)

        # Create a layout for the lists
        lists_layout = QtWidgets.QHBoxLayout()
        layout.addLayout(lists_layout)

        # Create frames for each list
        for i in range(self.levels):
            frame = QtWidgets.QFrame()
            frame.setFrameShape(QtWidgets.QFrame.StyledPanel)
            frame_layout = QtWidgets.QVBoxLayout(frame)
            lists_layout.addWidget(frame)

            # Add title label
            title_label = QtWidgets.QLabel(self.list_titles[i])
            frame_layout.addWidget(title_label)

            # Create the file list widget
            file_list_widget = QtWidgets.QTreeWidget()
            file_list_widget.setHeaderHidden(True)
            frame_layout.addWidget(file_list_widget)

            # Connect itemClicked signal to unique itemClicked method for each list
            file_list_widget.itemClicked.connect(lambda item, col, index=i: self.itemClicked(item, col, index))

            self.file_lists.append(file_list_widget)

        # Add feedback field
        feedback_field = QtWidgets.QLabel()
        layout.addWidget(feedback_field)
        self.feedback_field = feedback_field

        # Set initial project directory
        self.setProjectDirectory(self.asset_dir)

        self.populateFileLists(self.asset_dir)

    def populateFileLists(self, directory):
        for level, file_list_widget in enumerate(self.file_lists):
            file_list_widget.clear()
            if os.path.exists(directory) and os.listdir(directory):  # Check if directory exists and is not empty
                self.populateDirectory(directory, file_list_widget)
                directory = os.path.join(directory, os.listdir(directory)[0])  # Go down a level
            else:
                break  # Stop populating lists if directory is empty or doesn't exist

    def populateDirectory(self, directory, file_list_widget):
        for item_name in sorted(os.listdir(directory)):
            item_path = os.path.join(directory, item_name)
            item = QtWidgets.QTreeWidgetItem(file_list_widget)
            item.setText(0, item_name)
            if os.path.isdir(item_path):
                item.setIcon(0, QtGui.QIcon.fromTheme("folder"))

    def itemClicked(self, item, column, index):
        print(f"Item clicked on list {index}")
        print(f"Received column: {column}, Current column index: {index}")

        if column == index:  # Only proceed if the click is in the correct column
            selected_directory = self.asset_dir

            # Build the path based on the selected folders in the previous columns
            for level in range(index + 1):
                selected_directory = os.path.join(selected_directory, self.file_lists[level].currentItem().text(0))
                print(f"Selected directory: {selected_directory}")

            if os.path.isdir(selected_directory):
                # Update the path for the next column
                self.populateFileList(selected_directory, self.file_lists[index + 1])  # Populate next column
                self.feedback_field.setText(selected_directory)  # Display selected directory in feedback field
            else:
                print(f"Selected directory is not valid: {selected_directory}")

    def populateFileList(self, directory, file_list_widget):
        file_list_widget.clear()
        self.populateDirectory(directory, file_list_widget)

    def setProjectDirectory(self, directory=None):
        if directory is None:
            directory = QtWidgets.QFileDialog.getExistingDirectory(self, "Select Project Directory")
        if directory:
            self.asset_dir = directory
            self.folder_path_lineedit.setText(directory)
            cmds.workspace(directory, openWorkspace=True)


if __name__ == '__main__':
    import sys
    app = QtWidgets.QApplication.instance() or QtWidgets.QApplication(sys.argv)
    asset_dir = r"D:\WORKSPACE\new_folderStructure\GF\3D\ASSETS"
    asset_browser = AssetBrowser(asset_dir)
    asset_browser.show()
    sys.exit(app.exec_())

Solution

  • The column argument in itemClicked() is wrong, most importantly because it refers to the column of the clicked QTreeWidget, which, similarly to QTableView, can show multiple columns.
    You are only showing one column, so that value will always be 0, which is the reason for which if column == index: only works in the first QTreeWidget: the column is always 0, and the first QTreeWidget has index 0.

    Simply removing that comparison will solve the problem:

        def itemClicked(self, item, column, index):
            selected_directory = self.asset_dir
    
            for level in range(index + 1):
    
            ... etc.
    

    Note that using QTreeWidget for this purpose is completely pointless, because you are never using the primary feature of QTreeView, which is to show hierarchical structures.

    Since you are also never using more than one column, a much more accurate choice is to use QListWidget.

    class AssetBrowser(QWidget):
        ...
        def initUI(self):
            ...
            for i in range(self.levels):
                ...
                file_list_widget = QListWidget()
                frame_layout.addWidget(file_list_widget)
    
                file_list_widget.itemClicked.connect(
                    lambda item, index=i: self.itemClicked(item, index))
    
                self.file_lists.append(file_list_widget)
    
        ...
    
        def populateDirectory(self, directory, file_list_widget):
            for item_name in sorted(os.listdir(directory)):
                item_path = os.path.join(directory, item_name)
                item = QListWidgetItem(item_name, file_list_widget)
                if os.path.isdir(item_path):
                    item.setIcon(QIcon.fromTheme("folder"))
    
        def itemClicked(self, item, index):
            selected_directory = self.asset_dir
    
            for level in range(index + 1):
                selected_directory = os.path.join(selected_directory, 
                    self.file_lists[level].currentItem().text())
    
            if os.path.isdir(selected_directory):
                self.populateFileList(selected_directory, 
                    self.file_lists[index + 1])
                self.feedback_field.setText(selected_directory)
    
            # you should always clear the remaining lists
            for list_view in self.file_lists[index + 2:]:
                list_view.clear()
    

    There is another important issue. By default, populateFileLists automatically "fills" the remaining views if the given path goes beyond the "root" path; this is wrong from the UX perspective, because it doesn't show the user the currently selected directory within its list, and you're also arbitrarily sorting the contents alphabetically, which is not consistent with os.listdir(directory)[0] and with the conventional order used in file browsers.

    A more appropriate implementation should be the following:

        def populateFileLists(self, directory):
            prev_file_list = None
            for level, file_list_widget in enumerate(self.file_lists):
                file_list_widget.clear()
                if os.path.isdir(directory):
                    self.populateDirectory(directory, file_list_widget)
                    if prev_file_list:
                        match = prev_file_list.findItems(
                            os.path.basename(directory), Qt.MatchExactly)
                        if match:
                            prev_file_list.setCurrentItem(match[0])
                    prev_file_list = file_list_widget
                    for entry in sorted(os.listdir(directory)):
                        entry = os.path.join(directory, entry)
                        if os.path.isdir(entry):
                            directory = entry
                            break
                    else:
                        break
                else:
                    break
    

    Finally, when accessing the file system, using low level classes such as QTreeWidget or QListWidget makes things a bit cumbersome and prone to errors/bugs.

    QFileSystemModel provides a more appropriate interface through the file system, and it should be considered instead.
    Implementing this requires some further efforts, but also ensures a more accurate file system and object logic management. I strongly suggest to follow that path, then, and if you find issues while trying to achieve it, post another question.