pythonmayapyside2qtwidgetsqfilesystemmodel

Maya UI for browsing assets failing to navigate like OS X finder in Column view?


I am designing an Asset browser for navigating a pre-defined folder structure and with the help of @musicamante its now looking alot better!

its meant to function in a similar way to OS X's finder in Column view mode:

OS X's finder in Column view mode

I'm just having a little bit of trouble with how the lists display's. At launch the lists seem to want to show the root dir in the lists that should be blank.

UI example with problem

Does anyone have an idea what I need to do have these lists display blank? Or generally get them to function more like OS X's finder in Column view mode? At the moment I can only get it to remain expanded as far as 6 but I have no idea what to do if their directory structure goes more than 6 folders deep... Help?

import os
from PySide2 import QtWidgets, QtGui, QtCore
import maya.cmds as cmds
import maya.mel as mel  # Import the Maya MEL module


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_views = []  # List to store file views for each level
        self.thumbnail_label = None
        self.file_info_text = None
        self.selected_file_path = None  # Variable to store the selected file path
        self.initUI()

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

        # Adding the image
        image_label = QtWidgets.QLabel()
        pixmap = QtGui.QPixmap(r"N:\Asset_Browser\tools\safeBank\icons\safeBank_logoTitle_v001.png")
        image_label.setPixmap(pixmap)
        layout.addWidget(image_label)

        # Add a frame for the project setting and folder path
        project_frame = QtWidgets.QFrame()
        project_frame.setFrameShape(QtWidgets.QFrame.StyledPanel)
        project_frame_layout = QtWidgets.QHBoxLayout(project_frame)
        layout.addWidget(project_frame)

        # Add the set project button
        set_project_button = QtWidgets.QPushButton("Set Project")
        set_project_button.clicked.connect(self.setProject)
        project_frame_layout.addWidget(set_project_button)

        # Add the folder path line edit
        self.folder_path_lineedit = QtWidgets.QLineEdit()
        project_frame_layout.addWidget(self.folder_path_lineedit)

        # Add the refresh button
        refresh_icon = QtGui.QPixmap(r"N:\Asset_Browser\tools\safeBank\icons\refresh.png")
        refresh_label = QtWidgets.QLabel()
        refresh_label.setPixmap(refresh_icon)
        refresh_label.mousePressEvent = self.refreshDirectories
        project_frame_layout.addWidget(refresh_label)

        # Create a layout for the views, thumbnail, and file info
        views_thumbnail_layout = QtWidgets.QHBoxLayout()
        layout.addLayout(views_thumbnail_layout)

        # Create views for each level
        for i in range(self.levels):
            view = QtWidgets.QListView()
            model = QtWidgets.QFileSystemModel()
            model.setRootPath(QtCore.QDir.rootPath())
            view.setModel(model)
            view.setIconSize(QtCore.QSize(20, 20))  # Set icon size
            model.setFilter(QtCore.QDir.AllDirs | QtCore.QDir.Files | QtCore.QDir.NoDotAndDotDot)
            model.setNameFilters(["*.mb", "*.ma", "*.fbx"])
            model.setNameFilterDisables(False)
            view.clicked.connect(lambda index, i=i: self.viewClicked(index, i))
            views_thumbnail_layout.addWidget(view)
            self.file_views.append(view)
        
        # Populate only the first view upon launch
        self.populateDirectory(self.asset_dir, self.file_views[0])
        
        # Clear the root index for all other views to keep them blank
        for view in self.file_views[1:]:
            view.setRootIndex(QtCore.QModelIndex())
            
        # Connect the directory change for the first view to update subsequent views
        self.file_views[0].doubleClicked.connect(lambda index: self.populateNextView(index))
        
        # Add a frame for the thumbnail and file info
        info_frame = QtWidgets.QFrame()
        info_frame.setFrameShape(QtWidgets.QFrame.StyledPanel)
        info_frame_layout = QtWidgets.QVBoxLayout(info_frame)
        views_thumbnail_layout.addWidget(info_frame)

        # Add thumbnail label
        self.thumbnail_label = QtWidgets.QLabel()
        self.thumbnail_label.setFixedSize(200, 200)
        info_frame_layout.addWidget(self.thumbnail_label)

        # Add file info text field
        self.file_info_text = QtWidgets.QTextEdit()
        info_frame_layout.addWidget(self.file_info_text)

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

        # Add a frame for search and notes (occupying half the window width each)
        search_notes_frame = QtWidgets.QFrame()
        search_notes_frame.setFrameShape(QtWidgets.QFrame.StyledPanel)
        search_notes_layout = QtWidgets.QHBoxLayout(search_notes_frame)
        layout.addWidget(search_notes_frame)

        # Add a frame for search (occupying half the window width)
        search_frame = QtWidgets.QFrame()
        search_frame.setFrameShape(QtWidgets.QFrame.StyledPanel)
        search_frame_layout = QtWidgets.QVBoxLayout(search_frame)
        search_notes_layout.addWidget(search_frame)

        # Add the search bar to the search frame
        search_bar_layout = QtWidgets.QHBoxLayout()
        search_frame_layout.addLayout(search_bar_layout)
        self.search_bar = QtWidgets.QLineEdit()
        search_bar_layout.addWidget(self.search_bar)

        # Add the search button to the search frame
        search_button = QtWidgets.QPushButton("Search")
        search_button.clicked.connect(self.searchFiles)
        search_bar_layout.addWidget(search_button)
        
        # Connect returnPressed signal of the search bar to searchFiles method
        self.search_bar.returnPressed.connect(self.searchFiles)

        # Add the search results list to the search frame
        self.results_list = QtWidgets.QListWidget()
        search_frame_layout.addWidget(self.results_list)

        # Add a frame for notes (occupying half the window width)
        notes_frame = QtWidgets.QFrame()
        notes_frame.setFrameShape(QtWidgets.QFrame.StyledPanel)
        notes_layout = QtWidgets.QVBoxLayout(notes_frame)
        search_notes_layout.addWidget(notes_frame)

        # Add notes box to the notes frame
        notes_label = QtWidgets.QLabel("Notes:")
        notes_layout.addWidget(notes_label)
        self.notes_box = QtWidgets.QTextEdit()
        notes_layout.addWidget(self.notes_box)

    def populateDirectory(self, directory, view):
        view.model().setRootPath(directory)
        view.setRootIndex(view.model().index(directory))
        
        # Find and select the current directory in the previous file list
        if len(self.file_views) > 1:
            prev_view = self.file_views[self.file_views.index(view) - 1]
            match_indexes = prev_view.model().match(
                prev_view.model().index(prev_view.model().rootPath()),
                QtCore.Qt.DisplayRole,
                os.path.basename(directory),
                1,
                QtCore.Qt.MatchExactly | QtCore.Qt.MatchRecursive
            )
            if match_indexes:
                prev_view.selectionModel().select(
                    match_indexes[0], QtCore.QItemSelectionModel.ClearAndSelect)
    
            # Clear the root index for all other views to keep them blank
            for subsequent_view in self.file_views[self.file_views.index(view) + 1:]:
                subsequent_view.setRootIndex(QtCore.QModelIndex())


    def viewClicked(self, index, view_index):
        selected_file_path = self.file_views[view_index].model().filePath(index)
        self.folder_path_lineedit.setText(selected_file_path)  # Update folder path line edit
        if os.path.isdir(selected_file_path):
            self.populateDirectory(selected_file_path, self.file_views[view_index + 1])
        else:
            self.selected_file_path = selected_file_path
            self.displayNotes()
            self.displayThumbnail(selected_file_path)

    def displayNotes(self):
        # Check if a file is selected
        if self.selected_file_path is not None:
            # Construct the note file path by replacing the file extension with '.txt'
            note_file_path = os.path.splitext(self.selected_file_path)[0] + '.txt'
            # Check if the note file exists
            if os.path.isfile(note_file_path):
                # Read and display the contents of the note file
                with open(note_file_path, 'r') as f:
                    notes_content = f.read()
                self.notes_box.setText(notes_content)
                return
        # If no note file found or no file is selected, clear the notes box
        self.notes_box.clear()

    def displayThumbnail(self, file_path):
        # Construct the thumbnail file path
        thumbnail_path = os.path.splitext(file_path)[0] + '.png'
        # Debug print for the thumbnail path
        print(f"Thumbnail Path: {thumbnail_path}")
        # Check if thumbnail exists
        if os.path.exists(thumbnail_path):
            try:
                pixmap = QtGui.QPixmap(thumbnail_path)
                self.thumbnail_label.setPixmap(pixmap.scaled(200, 200, QtCore.Qt.KeepAspectRatio))
                return
            except Exception as e:
                print(f"Error loading thumbnail: {e}")
        else:
            print("Thumbnail not found.")

    def refreshDirectories(self, event):
        for view in self.file_views:
            view.model().refresh()

    def setProject(self):
        selected_directory = self.folder_path_lineedit.text()
        mel.eval('setProject "{}";'.format(selected_directory))

    def searchFiles(self):
        search_text = self.search_bar.text()
        selected_directory = self.folder_path_lineedit.text()
        results = []
    
        # Search only within the last selected folder
        for root, dirs, files in os.walk(selected_directory):
            for file in files:
                if search_text in file and file.lower().endswith(('.mb', '.ma', '.fbx')):
                    results.append(os.path.join(root, file))
    
        self.results_list.clear()
        self.results_list.addItems(results)

if __name__ == '__main__':
    import sys
    app = QtWidgets.QApplication.instance() or QtWidgets.QApplication(sys.argv)
    asset_dir = r"N:\Asset_Browser\work\Asset"
    asset_browser = AssetBrowser(asset_dir)
    asset_browser.show()
    sys.exit(app.exec_())

nothing seems to keep them blank until needed.

Any help is greatly appreciated!


Solution

  • In Qt item views, an invalid QModelIndex represents the root of the model. For a QFileSystemModel, that means the file system root (/ on *nix, "My Computer" on Windows).

    There is no immediate way to show an empty list with such a model (maybe there is on Windows using proper QDir.Filters, but I'm not sure), but there are possible alternatives:

    In order to achieve the second solution, you obviously need to keep a reference to the model: just create a property when the view is created, and then it's just a matter of resetting the model whenever necessary.

            self.dummy = QStringListModel()
            ...
            for i in range(self.levels):
                view = QtWidgets.QListView()
                view.fsModel = model = QtWidgets.QFileSystemModel()
                model.setRootPath(QtCore.QDir.rootPath())
                ...
    
        def populateDirectory(self, directory, view):
            view.setModel(view.fsModel)
            ...
            for subsequent_view in self.file_views[self.file_views.index(view) + 1:]:
                subsequent_view.setModel(self.dummy)
    

    Alternatively, you can subclass the model and provide a way to "invalidate" it:

    class MyFileSystemModel(QFileSystemModel):
        _valid = True
        def invalidate(self):
            self.modelAboutToBeReset.emit()
            self._valid = False
            self.modelReset.emit()
    
        def rowCount(self, parent=QModelIndex()):
            if self._valid:
                return super().rowCount(parent)
            return 0
    
        def setRootPath(self, path):
            self._valid = True
            return super().setRootPath(path)
    
    
    class AssetBrowser(QtWidgets.QWidget):
        ...
        def populateDirectory(self, directory, view):
            view.model().setRootPath(directory)
            ...
            for subsequent_view in self.file_views[self.file_views.index(view) + 1:]:
                subsequent_view.invalidate()