def __init__(self, parent=None): QMainWindow.__init__(self, parent) self.ui = Ui_projectManager() self.ui.setupUi(self) self.ui_setConnections() # set some initial UI states self.ui.projectsSplitter.setSizes([450,450]) self.projects_getExisting() self.ui.projectTree.setColumnWidth(0, 200) self.ui.sceneTree.setColumnWidth(0, 200) self.ui.projectTree.setColumnHidden(1, True) self.ui.sceneTree.setColumnHidden(1, True) self.ui_buildFileTypeFilterMenu()
class ProjectManager(QMainWindow): ''' Modo Project Manager Class. ''' def __init__(self, parent=None): QMainWindow.__init__(self, parent) self.ui = Ui_projectManager() self.ui.setupUi(self) self.ui_setConnections() # set some initial UI states self.ui.projectsSplitter.setSizes([450,450]) self.projects_getExisting() self.ui.projectTree.setColumnWidth(0, 200) self.ui.sceneTree.setColumnWidth(0, 200) self.ui.projectTree.setColumnHidden(1, True) self.ui.sceneTree.setColumnHidden(1, True) self.ui_buildFileTypeFilterMenu() def ui_setConnections(self): ''' Connect signals and slots. ''' # widgets self.ui.togglePathsCheckBox.stateChanged.connect(self.ui_togglePaths) self.ui.projectTree.itemDoubleClicked.connect(self.scenes_getAll) self.ui.projectTree.itemSelectionChanged.connect(self.scenes_clearList) self.ui.sceneTree.itemDoubleClicked.connect(self.act_scn_openSelected) # project actions self.ui.act_newProject.triggered.connect(self.act_project_create) self.ui.act_addExisting.triggered.connect(self.act_proj_addExisting) self.ui.act_removeSelected.triggered.connect(self.act_proj_removeSelected) self.ui.act_setAsCurrent.triggered.connect(self.act_proj_setAsCurrent) self.ui.act_exploreProject.triggered.connect(self.act_proj_explore) self.ui.act_exploreSceneFolder.triggered.connect(self.act_scn_openFolder) self.ui.act_docs.triggered.connect(self.act_launchDocs) # scene actions self.ui.act_openSelectedScene.triggered.connect(self.act_scn_openSelected) self.ui.act_importSelectedScene.triggered.connect(self.act_scn_importSelected) self.ui.act_importSelectedAsRef.triggered.connect(self.act_scn_importSelectedAsRef) # context menus self.ui.projectTree.customContextMenuRequested.connect(self.contextMenu_projectList) self.ui.sceneTree.customContextMenuRequested.connect(self.contextMenu_sceneList) def ui_clearTreeWidget(self, treewidget): ''' Remove all items from the specified QTreeWidget. Arg 1: the target widget <QTreeWidget> ''' iterator = QTreeWidgetItemIterator(treewidget, QTreeWidgetItemIterator.All) while iterator.value(): iterator.value().takeChildren() iterator +=1 i = treewidget.topLevelItemCount() while i > -1: treewidget.takeTopLevelItem(i) i -= 1 def ui_buildFileTypeFilterMenu(self): ''' Build and display the filetype filters menu. This menu will stay open until you click off of it. ''' # create a new menu self.ui.filtersMenu = QMenu() # inherit the stylesheet from main ui self.ui.filtersMenu.setStyleSheet(self.styleSheet()) # set up the menu's event filter and action self.ui.evFilter = StickyMenu() self.ui.filtersMenu.installEventFilter(self.ui.evFilter) self.ui.filtersMenu.aboutToHide.connect(self.ui_closeFileTypeFilterMenu) # load prevous selection if possible data = False if os.path.exists(FILTERSPATH): try: data = pickle.load(open(FILTERSPATH, 'r')) except IOError: lx.out('PROJECT MANAGER: Unable to apply previous filters. Data incorrectly serialized.') # populate the list of filetype options fileTypes = self.ui_getFileTypes() for i in sorted(fileTypes): action = self.ui.filtersMenu.addAction(i) action.setCheckable(True) if data: if i in data: action.setChecked(True) # apply the menu to the button self.ui.filtersBtn.setMenu(self.ui.filtersMenu) def ui_closeFileTypeFilterMenu(self): ''' Close the file type filter menu, save out the checked items, and refresh the scene list ''' # serialize and store the checked items for later use selectedTypes = [action.text() for action in self.ui.filtersMenu.actions() if action.isChecked()] pickle.dump(selectedTypes, open(FILTERSPATH, 'w')) # refresh the scenes list self.scenes_getAll() def ui_getFileTypes(self): ''' Return compatible scene filetypes as a dictionary. ''' fileTypeLookup = { 'Modo (*.lxo)': '.lxo', 'Preset (*.lxl)': '.lxl', 'Lightwave (*.lwo)': '.lwo', 'Wavefront (*.obj)': '.obj', 'Alembic (*.abc)':'.abc', 'Filmbox (*.fbx)': '.fbx', 'Collada (*.dae)': '.dae', 'Rhino (*.3dm)': '.3dm', 'Autodesk DXF (.*dxf)': '.dxf', 'Adobe Illustrator (*.eps, *.ai)': '.eps|.ai', 'Stereolithography (*.stl)': '.stl', 'Videoscape (*.geo)': '.geo', 'Solidworks (*.sldprt, *.sldasm)': '.sldprt|.sldasm', 'Protein DB (*.pdb)': '.pdb' } return fileTypeLookup def ui_togglePaths(self): ''' Show or hide the path columns for the projects and scenes lists. ''' state = self.ui.togglePathsCheckBox.isChecked() self.ui.projectTree.setColumnHidden(1, not state) self.ui.projectTree.setColumnWidth(0, 150) self.ui.sceneTree.setColumnHidden(1, not state) self.ui.sceneTree.setColumnWidth(0, 200) def dialog_info(self, title, message): ''' Generic Qt Message box Arg 1: the window title <string> Arg 2: the message <string> ''' box = QMessageBox() box.setWindowTitle(title) box.setText(message) box.exec_() def dialog_inputString(self, title, text): ''' Generic Qt string input dialog. Arg 1: the window title <string> Arg 2: the field label <string> ''' dialog = QInputDialog() dialog.setWindowTitle(title) dialog.setLabelText(text) dialog.exec_() if dialog.textValue(): return dialog.textValue() return None def dialog_confirm(self, title, text): ''' Generic Qt confirmation request. Arg 1: the window title <string> Arg 2: the question <list> ''' box = QMessageBox() box.setWindowTitle(title) box.setText('\n'.join(text)) box.setStandardButtons(QMessageBox.Yes | QMessageBox.No) box.setDefaultButton(QMessageBox.No) return box.exec_() def explore(self, filename): ''' Platform-respective function for opening a file. Arg 1: the path to the file/folder <string> ''' if sys.platform == "win32": os.startfile(filename) else: opener ="open" if sys.platform == "darwin" else "xdg-open" subprocess.call([opener, filename]) def write_genericSysFile(self, folder): ''' Write a generic '.luxproject' system file to the specified path. Arg 1: the path to the file <string> ''' contents = [ '#LXProject#', 'Associate image Images', 'Associate irrad IrradianceCaches', 'Associate movie Movies', 'Associate image@renderframes Renders/Frames', 'Associate movie@rendermovies Renders/Movies', 'Associate movie_st@rendermovies Renders/Movies', 'Associate movie_nost@rendermovies Renders/Movies', 'Associate scene Scenes', 'Associate scene.saveAs Scenes', 'ScriptSearchPath Scripts' ] if os.path.exists(folder): sysFile = os.path.join(folder, '.luxproject') f = open(sysFile, 'w') f.write('\n'.join(contents)) f.close() def write_rootDefaultSysFile(self, folder): ''' Write a '.luxproject' system file to the specified path. Arg 1: the path to the file <string> The associations defined by this file are deliberately empty for now, forcing Modo's file requesters to default to the project root. ''' contents = [ '#LXProject#', 'Associate image ', 'Associate irrad ', 'Associate movie ', 'Associate image@renderframes ', 'Associate movie@rendermovies ', 'Associate movie_st@rendermovies ', 'Associate movie_nost@rendermovies ', 'Associate scene ', 'Associate scene.saveAs ', 'ScriptSearchPath ' ] if os.path.exists(folder): sysFile = os.path.join(folder, '.luxproject') f = open(sysFile, 'w') f.write('\n'.join(contents)) f.close() def write_projectListFile(self, projectPath): ''' Open the Project List File and append the specified path. Arg 1: the project path <string>. ''' # read the lines with open(PROJECTLISTFILE, 'r') as f: lines = [line.strip() for line in f if line.strip()] f.close() # if the path isn't already in the list, add it if not projectPath in lines: with open(PROJECTLISTFILE, 'w') as f: for line in lines: f.write(line + '\n') f.write(projectPath + '\n') f.close() def projects_getExisting(self): ''' Populate the Existing Projects list, via the projects.projlist file. ''' # clear the list self.ui_clearTreeWidget(self.ui.projectTree) # ensure the project list file exists: if not os.path.exists(PROJECTLISTFILE): open(PROJECTLISTFILE, 'w').close # read the contents of the projects.projlist file and add them to the list else: with open(PROJECTLISTFILE) as fileList: lines = [line.strip() for line in fileList if line.strip()] for line in lines: projectTitle = os.path.split(line)[1] # create the projectItem projectItem = QTreeWidgetItem() projectItem.setSizeHint(0, QSize(200, 25)) projectItem.setText(0, projectTitle) projectItem.setText(1, line) projectItem.setForeground(1 , QBrush(QColor('#575757'))) # display bad project paths in red if not os.path.exists(line): projectItem.setForeground(0, QBrush(QColor('#8C2727'))) projectItem.setForeground(1, QBrush(QColor('#8C2727'))) # add the item to the tree self.ui.projectTree.addTopLevelItem(projectItem) # sort the tree self.ui.projectTree.sortItems(0, Qt.AscendingOrder) def projects_getSelectedPath(self): ''' Return the path to the selected project. ''' if len(self.ui.projectTree.selectedItems()) > 0: selection = self.ui.projectTree.selectedItems()[0] projDir = str(selection.text(1)) return projDir.strip() return False def scenes_clearList(self): ''' Clear the contents of the Scenes List. ''' self.ui_clearTreeWidget(self.ui.sceneTree) def scenes_getAll(self): ''' Search the selected project for files and display them in the scene list. Display only filetypes which are checked in the filters menu. ''' if self.ui.projectTree.selectedItems(): # change the cursor to indicate activity QApplication.setOverrideCursor(Qt.BusyCursor) # start by clearing the scene list self.ui_clearTreeWidget(self.ui.sceneTree) # get a clean project path projectItem = self.ui.projectTree.selectedItems()[0] projDir = projectItem.text(1).strip() # get the checked file types from the filter list fileTypes = self.ui_getFileTypes() selectedTypes = [fileTypes[action.text()] for action in self.ui.filtersMenu.actions() if action.isChecked()] # walk the project and display all files of the checked types for root, dirs, files in os.walk(projDir): for file in files: filename, ext = os.path.splitext(file) if ext in selectedTypes: filePath = os.path.join(root, file) fileName = os.path.basename(filePath) relativePath = filePath.replace(projDir, '') # create the file item item = QTreeWidgetItem() item.setText(0, fileName) item.setText(1, relativePath) item.setSizeHint(0, QSize(200, 25)) item.setForeground(1 , QBrush(QColor('#575757'))) # add the item to the scene tree self.ui.sceneTree.addTopLevelItem(item) # sort the scene tree self.ui.sceneTree.sortItems(0, Qt.AscendingOrder) # restore the cursor to its normal state QApplication.restoreOverrideCursor() def scenes_getSelectedPath(self): ''' Return the path to the selected scene. ''' scenePath = None if self.ui.sceneTree.selectedItems(): projectItem = self.ui.projectTree.selectedItems()[0] projDir = projectItem.text(1).strip() sceneItem = self.ui.sceneTree.selectedItems()[0] sceneRelativePath = sceneItem.text(1) if os.path.exists(projDir + sceneRelativePath): scenePath = projDir + sceneRelativePath return scenePath def scenes_openOrImport(self, type): ''' Open or Import the selected 3D file. Arg 1: the type of operation <string> ('ref' | 'normal' | 'open') ''' scenePath = self.scenes_getSelectedPath() if scenePath is not None: if type == 'ref': lx.eval("+scene.importReference {%s}" %scenePath) else: lx.eval('scene.open "%s" %s' %(scenePath, type)) def act_project_create(self): ''' Create a Modo project at the destination specified by the user via File Dialog. ''' # create the project with the standard template try: lx.eval('?projdir.instantiate') folderPicked = True except: folderPicked = False if folderPicked: # get the path we just set platform = lx.service.Platform() for idx in range(platform.PathCount()): if platform.PathNameByIndex(idx) == "project": folder = platform.PathByIndex(idx) # update the project list file self.write_projectListFile(folder) # update the project list in the UI self.projects_getExisting() # log and inform lx.out('PROJECT MANAGER: A new project was created: %s' %folder) self.dialog_info('Project Manager', "Project '%s' was created!" %os.path.basename(folder)) def act_proj_explore(self): ''' Explore the selected project's directory. ''' if self.ui.projectTree.selectedItems(): projDir = self.projects_getSelectedPath() if os.path.exists(projDir): self.explore(projDir) else: self.dialog_info('Trouble exploring project folder...', 'Invalid project path.') def act_proj_setAsCurrent(self): ''' Set the selected project as the current project in Modo. ''' if self.ui.projectTree.selectedItems(): projDir = self.projects_getSelectedPath() if os.path.exists(projDir): lx.eval('projDir.chooseProject "%s"' %projDir) else: self.dialog_info('Trouble setting the project...', 'Invalid project path.') def act_proj_removeSelected(self): ''' Remove the selected project from the project list. ''' project = self.projects_getSelectedPath() if project: confirm = self.dialog_confirm( 'Remove Project...', ['Remove the selected project from the list?']) if confirm == QMessageBox.Yes: # start by reading the current list with open(PROJECTLISTFILE, 'r') as f: lines = [line.strip() for line in f if line.strip()] f.close() # if the path is in the list, remove it if project in lines: lines.remove(project) # and write the updated list with open(PROJECTLISTFILE, 'w') as f: for line in lines: f.write(line + '\n') f.close() # update the UI self.projects_getExisting() def act_proj_addExisting(self): ''' Add an existing project to the project list. ''' inputPath = QFileDialog.getExistingDirectory(self, 'Select a project...', '/home') if os.path.exists(inputPath): # look for a '.luxproject' file if '.luxproject' in [file for file in os.listdir(inputPath)]: f = open(os.path.join(inputPath, '.luxproject')) lines = f.readlines() # check if the '.luxproject' file is legit if lines[0].strip() == '#LXProject#': self.write_projectListFile(inputPath) self.projects_getExisting() return else: self.dialog_info('Unable to add project...', 'The .luxproject file is incomplete...') else: self.write_rootDefaultSysFile(inputPath) self.write_projectListFile(inputPath) self.projects_getExisting() def act_scn_openSelected(self): ''' Open the selected Modo-compatible file in the current instance of Modo ''' self.scenes_openOrImport('normal') def act_scn_importSelected(self): ''' Import the selected Modo-compatible file into the current instance of Modo ''' self.scenes_openOrImport('import') def act_scn_importSelectedAsRef(self): ''' Import the selected Modo-compatible file as referenced into the current instance of Modo ''' self.scenes_openOrImport('ref') def act_scn_openFolder(self): ''' Open the folder containing the selected scene. ''' scenePath = self.scenes_getSelectedPath() if scenePath: self.explore(os.path.dirname(scenePath)) def act_launchDocs(self): ''' Open the documentation in a web browser. ''' self.explore("http://www.timcrowson.com/modo-project-manager/") def contextMenu_projectList(self): ''' Build a contextual menu for the Project list. ''' menu = QMenu() menu.setStyleSheet('QMenu::item:selected{color: #f89a2b;background: #545454;}') menu.addAction('New Project...', self.act_project_create) menu.addAction('Set Selected As Current', self.act_proj_setAsCurrent) menu.addAction('Open Project Folder...', self.act_proj_explore) menu.addAction('Add Existing Project to List...', self.act_proj_addExisting) menu.addAction('Remove Selected Project from List', self.act_proj_removeSelected) menu.addAction('Show Scenes', self.scenes_getAll) menu.exec_(QCursor.pos()) def contextMenu_sceneList(self): ''' Build a contextual menu for the Scene list. ''' menu = QMenu() menu.setStyleSheet('QMenu::item:selected{color: #f89a2b;background: #545454;}') menu.addAction('Open Selected Scene', self.act_scn_openSelected) menu.addAction('Import Selected Scene', self.act_scn_importSelected) menu.addAction('Import Selected As Referenced', self.act_scn_importSelectedAsRef) menu.addAction('Open Scene Folder', self.act_scn_openFolder) menu.exec_(QCursor.pos())