diff --git a/.gitignore b/.gitignore index 499ad86..a88d1c6 100644 --- a/.gitignore +++ b/.gitignore @@ -137,4 +137,7 @@ dmypy.json .pyre/ # VSCode files -.vscode/ \ No newline at end of file +.vscode/ + +# FFMEPG install folder +Hestia/core/ffmpeg/* \ No newline at end of file diff --git a/Hestia/__init__.py b/Hestia/__init__.py index e8ea9cc..7cbfd5f 100644 --- a/Hestia/__init__.py +++ b/Hestia/__init__.py @@ -3,7 +3,7 @@ :file: __init__.py :brief: Initialize file. :author: PiloeGAO (Leo DEPOIX) - :version: 0.0.3 + :version: 0.0.4 """ from os import sys, path diff --git a/Hestia/core/IOUtils.py b/Hestia/core/IOUtils.py new file mode 100644 index 0000000..4e1ca33 --- /dev/null +++ b/Hestia/core/IOUtils.py @@ -0,0 +1,70 @@ +""" + :package: Hestia + :file: IOUtils.py + :brief: IO functions. + :author: PiloeGAO (Leo DEPOIX) + :version: 0.0.4 +""" +import sys, os, shutil, subprocess + +def makeFolder(path): + """Build a folder. + + Args: + path (str): Folder path. + + Returns: + bool: Creation status. + """ + if(not os.path.isdir(path)): + try: + os.makedirs(path) + except OSError as error: + print("Directory %s can't be created (%s)" % (path, error)) + return False + else: + return True + else: + return False + +def copyFile(filePath, targetPath, **kwargs): + """Copy a file from a directory to another. + + Args: + filePath (str): Input path. + targetPath (str): Ouput path. + + Returns: + bool: Copy status. + """ + oldFilename = os.path.split(filePath)[1] + if( not os.path.isfile(targetPath + os.sep + oldFilename)): + shutil.copy(filePath, targetPath) + + if(kwargs["newName"] != None): + src = targetPath + os.sep + oldFilename + dst = targetPath + os.sep + kwargs["newName"] + os.path.splitext(oldFilename)[1] + os.rename(src, dst) + + return True + return False + +def videoConverter(filePath, targetPath): + """Convert video to MP4. + + Args: + filePath (str): Input path. + targetPath (str): Ouput path. + + Returns: + bool: Convert status. + """ + ffmpeg_installDir = os.path.dirname(os.path.abspath(__file__)) + os.sep + "ffmpeg" + os.sep + "bin" + + # TODO: Support MacOS and Linux. + if(os.path.isdir(ffmpeg_installDir) and sys.platform.startswith("win32")): + ffmepg_exe = ffmpeg_installDir + os.sep + "ffmpeg.exe" + subprocess.call("%s -i %s -vcodec libx264 -acodec aac %s" % (ffmepg_exe, filePath, targetPath)) + return True + + return False \ No newline at end of file diff --git a/Hestia/core/category.py b/Hestia/core/category.py index 1c0316b..f88db3a 100644 --- a/Hestia/core/category.py +++ b/Hestia/core/category.py @@ -3,7 +3,7 @@ :file: category.py :brief: Category base class. :author: PiloeGAO (Leo DEPOIX) - :version: 0.0.3 + :version: 0.0.4 """ class Category(): @@ -13,13 +13,15 @@ class Category(): id (str, optional): Category's ID. Defaults to "". name (str, optional): Catgeory's name. Defaults to "". description (str, optional): Category's description. Defaults to "". - type (str, optional): Category's type (must be "Asset" or "Shot"). Defaults to "". + type (str, optional): Category's type (must be "Assets" or "Shots"). Defaults to "". """ - def __init__(self, id="", name="", description="", type=""): + def __init__(self, id="", name="", description="", type="", **kwargs): self.__id = id self.__name = name self.__description = description self.__type = type + + self.__rawDatas = kwargs["rawDatas"] if "rawDatas" in kwargs else "" self.__entities = [] @@ -77,6 +79,15 @@ def type(self): """ return self.__type + @property + def rawDatas(self): + """Get the raw datas of the class. + + Returns: + dict: Raw datas + """ + return self.__rawDatas + @property def entities(self): """Get the entities stored in the category. diff --git a/Hestia/core/entity.py b/Hestia/core/entity.py index acdec4a..1864b45 100644 --- a/Hestia/core/entity.py +++ b/Hestia/core/entity.py @@ -3,7 +3,7 @@ :file: entity.py :brief: Entity base class. :author: PiloeGAO (Leo DEPOIX) - :version: 0.0.3 + :version: 0.0.4 """ class Entity(): @@ -16,9 +16,10 @@ class Entity(): name (str, optional): Entity's name. Defaults to "". description (str, optional): Entity's description. Defaults to "". icon (str, optional): Entity's icon. Defaults to "". + tasks (list: class: "Task"): Entity's tasks. Defaults to []. versions (list: class: "Version"): Entity's version. Defaults to []. """ - def __init__(self, manager, entityType = "Assets", id = "", name = "", description = "", icon = "", versions=[], **kwargs): + def __init__(self, manager, entityType = "Assets", id = "", name = "", description = "", icon = "", tasks=[], versions=[], **kwargs): self.__manager = manager # Common datas. self.__type = entityType @@ -26,20 +27,20 @@ def __init__(self, manager, entityType = "Assets", id = "", name = "", descripti self.__name = name self.__description = description + self.__rawDatas = kwargs["rawDatas"] if "rawDatas" in kwargs else "" + self.__iconDownloaded = False self.__icon = icon + self.__tasks = tasks self.__versions = versions # Shot specific datas. - self.__frameNumber = 0 - if("frameNumber" in kwargs): - self.__frameNumber = int(kwargs["frameNumber"]) + self.__frameNumber = int(kwargs["frameNumber"]) if "frameNumber" in kwargs else 0 self.__assignedAssets = kwargs["assignedAssets"] if "assignedAssets" in kwargs else [] @property def type(self): """Get the type of entity. - Returns: str: Entity type. """ @@ -89,6 +90,15 @@ def description(self, description): description (str): The description of the entity """ self.__description = description + + @property + def rawDatas(self): + """Get the raw datas of the class. + + Returns: + dict: Raw datas + """ + return self.__rawDatas @property def icon(self): @@ -114,6 +124,15 @@ def icon(self, icon): self.__icon = icon self.__iconDownloaded = True + @property + def tasks(self): + """Get tasks of the entity. + + Returns: + list: class:`Task`: Task of the entity. + """ + return self.__tasks + @property def versions(self): """Get versions of the entity. diff --git a/Hestia/core/links/dccs/defaultIntegration.py b/Hestia/core/links/dccs/defaultIntegration.py index ba1a1b0..a4c4673 100644 --- a/Hestia/core/links/dccs/defaultIntegration.py +++ b/Hestia/core/links/dccs/defaultIntegration.py @@ -3,7 +3,7 @@ :file: defaultIntegration.py :brief: Default integration class. :author: PiloeGAO (Leo DEPOIX) - :version: 0.0.3 + :version: 0.0.4 """ class DefaultIntegration(object): @@ -12,13 +12,35 @@ class DefaultIntegration(object): def __init__(self, manager=None): self.__manager = manager + self._name = "standalone" + self._active = False + self._defaultFormat = "" self._availableFormats = [] self._supportInstances = False self._instances = False + self._supportScreenshots = False + @property + def name(self): + """Get the name of the integration. + + Returns: + str: Integration name + """ + return self._name + + @property + def defaultFormat(self): + """Get the default format. + + Returns: + str: Default format/extension. + """ + return self._defaultFormat + @property def availableFormats(self): """Get the available formats. @@ -56,6 +78,15 @@ def instances(self, newStatus): print("Instances new status" + str(newStatus)) self._instances = newStatus + @property + def supportScreenshots(self): + """Get the screenshots support. + + Returns: + bool: Is screenshot support is available. + """ + return self._supportScreenshots + def initializeFileFormats(self): """Initialize the file formats list. @@ -150,5 +181,51 @@ def assignShaderToSelectedAsset(self, version): def extractAssets(self): """Extracts assets for shot building file. """ + return NotImplementedError + + def takePlayblast(self, startFrame, endFrame, path): + """Take a playblast of the scene. + + Args: + startFrame (int): Start frame. + endFrame (int): End frame. + path (sty): Ouput path. + + Returns: + bool: Function status. + """ + return NotImplementedError + + def openFile(self, path): + """Open the file in the DCC. + + Args: + path (str): File path. + + Returns: + bool: Function status. + """ + return NotImplementedError + + def saveFile(self, path): + """Save current file to the given path. + Args: + path (str): File path. + + Returns: + bool: Functions status. + """ + return NotImplementedError + + def exportSelection(self, path, extension): + """Export selection to the path with the correct format. + + Args: + path (str): Output path. + extension (str): Extensionof the file. + + Returns: + bool: Function status. + """ return NotImplementedError \ No newline at end of file diff --git a/Hestia/core/links/dccs/guerillaIntegration.py b/Hestia/core/links/dccs/guerillaIntegration.py index c526c1d..03d35e0 100644 --- a/Hestia/core/links/dccs/guerillaIntegration.py +++ b/Hestia/core/links/dccs/guerillaIntegration.py @@ -3,7 +3,7 @@ :file: guerillaIntegration.py :brief: Guerilla Render integration class. :author: PiloeGAO (Leo DEPOIX) - :version: 0.0.3 + :version: 0.0.4 """ import os @@ -24,10 +24,14 @@ class GuerillaIntegration(DefaultIntegration): def __init__(self, manager=None): self.__manager = manager + self._name = "Guerilla" + if(not integrationActive): self.__manager.logging.error("Guerilla Libraries not found!") self._active = integrationActive + + self._defaultFormat = ".gproject" self.initializeFileFormats() # Guerilla Render support instance by using "References". @@ -153,4 +157,52 @@ def assignShaderToSelectedAsset(self, version): def extractAssets(self): """Extracts assets for shot building file. """ + return NotImplemented + + def takePlayblast(self, startFrame, endFrame, path): + """Take a playblast of the scene. + + Args: + startFrame (int): Start frame. + endFrame (int): End frame. + path (sty): Ouput path. + + Returns: + bool: Function status. + """ + return NotImplemented + + + def openFile(self, path): + """Open the file in the DCC. + + Args: + path (str): File path. + + Returns: + bool: Function status. + """ + return NotImplemented + + def saveFile(self, path): + """Save current file to the given path. + + Args: + path (str): File path. + + Returns: + bool: Functions status. + """ + return NotImplemented + + def exportSelection(self, path, extension): + """Export selection to the path with the correct format. + + Args: + path (str): Output path. + extension (str): Extensionof the file. + + Returns: + bool: Function status. + """ return NotImplemented \ No newline at end of file diff --git a/Hestia/core/links/dccs/mayaIntegration.py b/Hestia/core/links/dccs/mayaIntegration.py index db7a91b..c528f6e 100644 --- a/Hestia/core/links/dccs/mayaIntegration.py +++ b/Hestia/core/links/dccs/mayaIntegration.py @@ -3,7 +3,7 @@ :file: mayaIntegration.py :brief: Maya integration class. :author: PiloeGAO (Leo DEPOIX) - :version: 0.0.3 + :version: 0.0.4 """ import os @@ -25,15 +25,20 @@ class MayaIntegration(DefaultIntegration): def __init__(self, manager=None): self.__manager = manager + self._name = "Maya" + if(not integrationActive): self.__manager.logging.error("Maya Libraries not found!") self._active = integrationActive + + self._defaultFormat = ".ma" self.initializeFileFormats() # Autodesk Maya support instance by using "References". - self._supportInstances = True - self._instances = True + self._supportInstances = True + self._instances = True + self._supportScreenshots = True def initializeFileFormats(self): """Initialize the file formats list. @@ -346,6 +351,9 @@ def assignShaderToSelectedAsset(self, version): Returns: bool: Function status. """ + if(len(cmds.ls(sl=True)) == 0): + return False + currentAsset = cmds.ls(sl=True)[0] if(cmds.attributeQuery("isHestiaAsset", node=currentAsset, exists=True) @@ -366,4 +374,110 @@ def extractAssets(self): print("%s : " % t) print(cmds.xform(t, query=True, matrix=True, worldSpace=True)) - return False \ No newline at end of file + return False + + def takePlayblast(self, startFrame, endFrame, path): + """Take a playblast of the scene. + + Args: + startFrame (int): Start frame. + endFrame (int): End frame. + path (sty): Ouput path. + + Returns: + bool: Function status. + """ + # set screenshot dimensions + width = 1920 + height = 1080 + + if(startFrame == endFrame): + # From: https://gist.github.com/gfxhacks/f3e750f416f94952d7c9894ed8f78a71 + # Take a single image. + currentFrame = startFrame + if(startFrame == -1): + currentFrame = int(cmds.currentTime(query=True)) + + cmds.playblast(fr=currentFrame, v=False, fmt="image", c="png", orn=False, cf=path, wh=[width,height], p=100) + else: + # TODO: Use framerange from inputs. + # Take a video. + cmds.playblast(v=False, orn=False, f=path, wh=[width,height], p=100) + + return True + + + def openFile(self, version): + """Open the file in the DCC. + + Args: + version (class:`Version`): Version of the asset. + + Returns: + bool: Function status. + """ + filePath = str(version.workingPath) + if(os.path.isfile(filePath) and + (os.path.splitext(filePath)[1] == ".ma" or os.path.splitext(filePath)[1] == ".mb")): + cmds.file(new=True, force=True) + cmds.file(version.workingPath, o=True) + return True + else: + return False + + def saveFile(self, path): + """Save current file to the given path. + + Args: + path (str): File path. + + Returns: + bool: Functions status. + """ + if(not os.path.isfile(path)): + cmds.file(rename=path) + cmds.file(force=True, save=True, type="mayaAscii") + return True + + return False + + def exportSelection(self, path, extension): + """Export selection to the path with the correct format. + + Args: + path (str): Output path. + extension (str): Extensionof the file. + + Returns: + bool: Function status. + """ + if(os.path.isfile(path)): + self.__manager.logging.error("File \"%s\" already exist, skipping export." % path) + return False + + if(len(cmds.ls(sl=True)) == 0): + self.__manager.logging.error("Nothing selected, skipping export.") + return False + + extension = extension.lower() + + if(extension == ".ma"): + cmds.file(path, type='mayaAscii', exportSelected=True) + elif(extension == ".mb"): + cmds.file(path, type='mayaBinary', exportSelected=True) + elif(extension == ".abc"): + oldSelection = cmds.ls(sl=True) + + # Select hierarchy. + cmds.select(hierarchy=True) + command = "-uvWrite -worldSpace " + "-selection -file " + path + cmds.AbcExport ( j = command ) + + # Reset selection + cmds.select(oldSelection) + elif(extension == ".obj"): + cmds.file(path, type='OBJexport', exportSelected=True) + else: + return False + + return True \ No newline at end of file diff --git a/Hestia/core/links/projectManagers/defaultWrapper.py b/Hestia/core/links/projectManagers/defaultWrapper.py index b78ae47..3a48eca 100644 --- a/Hestia/core/links/projectManagers/defaultWrapper.py +++ b/Hestia/core/links/projectManagers/defaultWrapper.py @@ -3,7 +3,7 @@ :file: defaultWrapper.py :brief: Default wrapper class. :author: PiloeGAO (Leo DEPOIX) - :version: 0.0.3 + :version: 0.0.4 """ import getpass diff --git a/Hestia/core/links/projectManagers/kitsuWrapper.py b/Hestia/core/links/projectManagers/kitsuWrapper.py index efa66b5..8de1bd1 100644 --- a/Hestia/core/links/projectManagers/kitsuWrapper.py +++ b/Hestia/core/links/projectManagers/kitsuWrapper.py @@ -3,13 +3,14 @@ :file: kitsuWrapper.py :brief: Kitsu wrapper class. :author: PiloeGAO (Leo DEPOIX) - :version: 0.0.3 + :version: 0.0.4 """ -import os, json +import os, json, sys import gazu -from .defaultWrapper import DefaultWrapper +from .defaultWrapper import DefaultWrapper from ....core.project import Project +from ....core.task import Task from ....core.category import Category from ....core.entity import Entity from ....core.version import Version @@ -27,6 +28,7 @@ def __init__(self, manager, api=""): self.__api = api self.__active = False self._username = "" + self.__userID = "" self.__debugKitsuData = False @@ -72,6 +74,7 @@ def login(self, username="", password=""): return False else: self._username = username + " (Online Mode: Kitsu)" + self.__userID = gazu.client.get_current_user()["id"] # Enable caching for faster download from online. gazu.cache.enable() @@ -101,20 +104,63 @@ def getDatasFromProject(self, project): """ self.__manager.logging.info("Getting datas for: %s" % project["name"]) + # Setup project variables. + description = project["description"] if project["description"] != None else "" + fps = project["fps"] if project["fps"] != None else 0 + ratio = project["ratio"] if project["ratio"] != None else 0 + resolution = project["resolution"] if project["resolution"] != None else 0 + mountPoint = project["file_tree"]["output"]["mountpoint"] if project["file_tree"] != None else "" + rootPoint = project["file_tree"]["output"]["root"] if project["file_tree"] != None else "" + outputFilenameAsset = project["file_tree"]["output"]["file_name"]["asset"] if project["file_tree"] != None else "" + outputFilenameShot = project["file_tree"]["output"]["file_name"]["shot"] if project["file_tree"] != None else "" + outputFolderPathAsset = project["file_tree"]["output"]["folder_path"]["asset"] if project["file_tree"] != None else "" + outputFolderPathShot = project["file_tree"]["output"]["folder_path"]["shot"] if project["file_tree"] != None else "" + workingFilenameAsset = project["file_tree"]["working"]["file_name"]["asset"] if project["file_tree"] != None else "" + workingFilenameShot = project["file_tree"]["working"]["file_name"]["shot"] if project["file_tree"] != None else "" + workingFolderPathAsset = project["file_tree"]["working"]["folder_path"]["asset"] if project["file_tree"] != None else "" + workingFolderPathShot = project["file_tree"]["working"]["folder_path"]["shot"] if project["file_tree"] != None else "" + # Get and create a new project. - newProject = Project(id=project["id"], name=project["name"], description=project["description"], - fps=project["fps"], ratio=project["ratio"], resolution=project["resolution"]) + newProject = Project(id=project["id"], name=project["name"], description=description, + fps=fps, ratio=ratio, resolution=resolution, + mountPoint=mountPoint, + rootPoint=rootPoint, + outputFilenameAsset=outputFilenameAsset, + outputFilenameShot=outputFilenameShot, + outputFolderPathAsset=outputFolderPathAsset, + outputFolderPathShot=outputFolderPathShot, + workingFilenameAsset=workingFilenameAsset, + workingFilenameShot=workingFilenameShot, + workingFolderPathAsset=workingFolderPathAsset, + workingFolderPathShot=workingFolderPathShot, + rawDatas=project) if(self.__manager.debug and self.__debugKitsuData): self.__manager.logging.debug(json.dumps(project, sort_keys=True, indent=4)) + + # Get, create and add tasks to project. + tasks = gazu.task.all_task_types() + + for task in tasks: + taskType = "Assets" if task["for_shots"] == "false" else "Shots" + newTask = Task(taskType=taskType, id=task["id"], name=task["name"], rawDatas=task) + newProject.addTask(newTask) + + if(self.__manager.debug and self.__debugKitsuData): + self.__manager.logging.debug(json.dumps(tasks, sort_keys=True, indent=4)) + + self.__manager.logging.info("Tasks loaded.") # Get, create and add categories to project. categories = gazu.asset.all_asset_types_for_project(project) for category in categories: - newCategory = Category(id=category["id"], name=category["name"], description="", type="Assets") + newCategory = Category(id=category["id"], name=category["name"], description="", type="Assets", rawDatas=category) newProject.addCategory(newCategory) + if(self.__manager.debug and self.__debugKitsuData): + self.__manager.logging.debug(json.dumps(categories, sort_keys=True, indent=4)) + self.__manager.logging.info("Categories loaded.") # Get, create and add assets to categories. @@ -127,8 +173,13 @@ def getDatasFromProject(self, project): if(self.__manager.debug and self.__debugKitsuData): self.__manager.logging.debug(json.dumps(assetData, sort_keys=True, indent=4)) + # Get tasks for asset. + assetTasks = [] + for assetTask in gazu.task.all_task_types_for_asset(assetData): + assetTasks.append([task for task in newProject.tasks if task.id == assetTask["id"]][0]) + # Output versionning. - versions = self.getVersions(assetData) + versions = self.getVersions(newProject, assetData) # Buildint the Entity with all datas. newAsset = Entity(manager=self.__manager, @@ -137,7 +188,9 @@ def getDatasFromProject(self, project): name=asset["name"], description=asset["description"], icon="", - versions=versions) + tasks=assetTasks, + versions=versions, + rawDatas=asset) assetCategory = [category for category in newProject.categories if category.name == assetData["asset_type_name"]][0] assetCategory.addEntity(newAsset) @@ -151,7 +204,8 @@ def getDatasFromProject(self, project): newCategory = Category(id=sequence["id"], name=sequence["name"], description=sequence["description"], - type="Shots") + type="Shots", + rawDatas=sequence) newProject.addCategory(newCategory) @@ -179,8 +233,14 @@ def getDatasFromProject(self, project): # Get Assets assigned in the shot. assignedAssets = [str(asset["id"]) for asset in gazu.asset.all_assets_for_shot(shotData)] + + # Get tasks for shot. + shotTasks = [] + for shotTask in gazu.task.all_task_types_for_shot(shotData): + shotTasks.append([task for task in newProject.tasks if task.id == shotTask["id"]][0]) + # Output versionning. - versions = self.getVersions(shotData) + versions = self.getVersions(newProject, shotData) newShot = Entity(manager=self.__manager, entityType="Shots", @@ -188,9 +248,11 @@ def getDatasFromProject(self, project): name=shot["name"], description=shot["description"], icon="", + tasks=shotTasks, versions=versions, frameNumber=nb_frames, - assignedAssets=assignedAssets) + assignedAssets=assignedAssets, + rawDatas=shot) shotSequence = [sequence for sequence in newProject.categories if sequence.name == shotData["sequence_name"]][0] shotSequence.addEntity(newShot) @@ -240,7 +302,7 @@ def downloadPreview(self, entityType="Assets", entityId=None): return icon_path - def getVersions(self, entityData=None): + def getVersions(self, project=None, entityData=None): """Get versions for entity. Args: @@ -253,14 +315,150 @@ def getVersions(self, entityData=None): outputs = gazu.files.all_output_files_for_entity(entityData) for output in outputs: - task_type = gazu.task.get_task_type(output["task_type_id"]) + task = [task for task in project.tasks if task.id == output["task_type_id"]][0] newVersion = Version(id=output["id"], - name="%s: Revision %s" % (task_type["name"], output["revision"]), + name="", description="", + task=task, workingPath=output["source_file"]["path"], - outputPath=output["path"]) + outputPath=output["path"], + revisionNumber=output["revision"], + rawDatas=output) versions.append(newVersion) + + return versions + + def publish(self, entity=None, name="", comment="", taskTypeID="", taskStatus="TODO", version="", software="", outputType="", workingFilePath="", outputFiles=[], previewFilePath=""): + """Publish files (working and outputs) to Kitsu. (Code from Guillaume Baratte project's called managerTools) + + Args: + entity (class: `Entity`): Entity targeted. + name (str, optional): Publish name. Defaults to "". + comment (str, optional): Publish comment. Defaults to "". + taskTypeID (str, optional): Tasktype ID. Defaults to "". + taskStatus (str, optional): Status of the publish. Defaults to "TODO". + version (str, optional): Version. Defaults to "". + software (str, optional): Software name. Defaults to "". + outputType (str, optional): Output type name. Defaults to "". + workingFilePath (str, optional): Working file path. Defaults to "". + outputFiles (list, optional): Outputs files. Defaults to []. + previewFilePath (str, optional): Preview image/video path. Defaults to "". + + Returns: + bool: Publish status. + """ + task = gazu.task.get_task_by_entity(entity.id, taskTypeID) + + # Add working file. + workingFileData = { + "name": name, + "comment": comment, + "person_id": self.__userID, + "task_id": task["id"], + "revision": version, + "mode": "working" + } + + # Assigning softwate. + if(software != ""): + softwareData = gazu.client.fetch_first( + "softwares", + { + "name": name + }) + + if(softwareData != None): + workingFileData["software_id"] = softwareData["id"] + + # Create the working file entry on Zou. + workingFilePublishData = gazu.client.post( + "data/tasks/%s/working-files/new" % task["id"], + workingFileData + ) + + # Set the path in the DB entry. + gazu.client.put("data/working-files/%s" % workingFilePublishData["id"], + { + "path": workingFilePath + } + ) + + # Add output files. + outputFilesPublishData = [] + for outputFilePath in outputFiles: + filename = os.path.split(outputFilePath)[1] + extension = os.path.splitext(outputFilePath)[1] + + outputTypeName = "" + if(extension == ".ma"): + outputTypeName = "Maya Rig" + elif(extension == ".abc" and entity.type == "Assets"): + outputTypeName = "ABC Modeling" + elif(extension == ".abc" and entity.type == "Shots"): + outputTypeName = "ABC Animation" + else: + outputTypeName = outputType + + outputTypeData = gazu.client.fetch_first( + "output-types", + { + "name": outputTypeName + } + ) + + ouputFileData = { + "output_type_id": outputTypeData["id"], + "task_type_id": task["task_type_id"], + "comment": comment, + "revision": version, + "representation": "output", + "name": "%s_%s" % (name, filename), + "nb_elements": 1, + "sep": "/", + "working_file_id": workingFilePublishData["id"], + "person_id": self.__userID + } + + outputFilePublishData = gazu.client.post( + "data/entities/%s/output-files/new" % task["entity_id"], + ouputFileData + ) + + gazu.client.put("data/output-files/%s" % outputFilePublishData["id"], + { + "path" : outputFilePath + } + ) + + outputFilesPublishData.append(outputFilePublishData) + + # Add the comment. + taskStatusData = gazu.task.get_task_status_by_short_name(taskStatus.lower()) - return versions \ No newline at end of file + if(taskStatusData != None): + commentData = { + "task_status_id": taskStatusData["id"], + "comment": comment, + "person_id": self.__userID + } + + commentPublishData = gazu.client.post('actions/tasks/%s/comment' % task["id"], commentData) + + # Add the preview. + previewPublishData = gazu.client.post("actions/tasks/%s/comments/%s/add-preview" % ( + task["id"], + commentPublishData['id'] + ), {}) + + gazu.client.upload( + 'pictures/preview-files/%s' % previewPublishData["id"], + previewFilePath + ) + + gazu.task.set_main_preview(previewPublishData) + else: + self.__manager.logging.error("Couldn't find the status for publishing, comment and preview wouldn't be published.") + + return True \ No newline at end of file diff --git a/Hestia/core/manager.py b/Hestia/core/manager.py index 59fe5d5..1ddf980 100644 --- a/Hestia/core/manager.py +++ b/Hestia/core/manager.py @@ -3,7 +3,7 @@ :file: manager.py :brief: Manager class. :author: PiloeGAO (Leo DEPOIX) - :version: 0.0.3 + :version: 0.0.4 """ import shutil, logging import tempfile, atexit @@ -23,7 +23,7 @@ class Manager(): projects (list(class: "Project"), optional): Projects list. Defaults to []. """ def __init__(self, integration = "standalone", projects = [Project(name="local", description="Local file system.")], **kwargs): - self.__version = "0.0.3" + self.__version = "0.0.4" # Loading preferences. self.__preferences = Preferences(manager=self) @@ -240,4 +240,9 @@ def connectToOnline(self, cleanProjects=True, **kwargs): return True return False - return False \ No newline at end of file + return False + + def cleanTemporaryFolder(self): + """Force cleaning temporary folder. + """ + shutil.rmtree(self.__tempFolder) \ No newline at end of file diff --git a/Hestia/core/preferences.py b/Hestia/core/preferences.py index 1047aa2..73624e9 100644 --- a/Hestia/core/preferences.py +++ b/Hestia/core/preferences.py @@ -3,7 +3,7 @@ :file: preferences.py :brief: Preference class. :author: PiloeGAO (Leo DEPOIX) - :version: 0.0.3 + :version: 0.0.4 """ try: import configparser diff --git a/Hestia/core/project.py b/Hestia/core/project.py index bdad50a..638d0a7 100644 --- a/Hestia/core/project.py +++ b/Hestia/core/project.py @@ -3,8 +3,16 @@ :file: project.py :brief: Project class. :author: PiloeGAO (Leo DEPOIX) - :version: 0.0.3 + :version: 0.0.4 """ +import os + +from . import IOUtils + +from Hestia.core.version import Version +from Hestia.core.task import Task +from Hestia.core.category import Category +from Hestia.core.entity import Entity class Project(): """Project class. @@ -14,33 +22,48 @@ class Project(): name (str, optional): Project's name. Defaults to "". description (str, optional): Project's description. Defaults to "". """ - def __init__(self, id="", name="", description="", **kwargs): + def __init__(self, id="", name="", description="", tasks=[], **kwargs): + # Project name. self.__id = id self.__name = name self.__description = description + self.__tasks = tasks + + self.__rawDatas = kwargs["rawDatas"] if "rawDatas" in kwargs else "" - self.__framerate = 0 - self.__ratio = "16:9" - self.__resolution = 1080 - self.__startFrame = 1000 - self.__preRoll = 24 - self.__postRoll = 24 - - if("fps" in kwargs): - self.__framerate = int(float(kwargs["fps"])) - if("ratio" in kwargs): - self.__ratio = kwargs["ratio"] - if("resolution" in kwargs): - self.__resolution = int(kwargs["resolution"]) - if("startFrame" in kwargs): - self.__startFrame = int(kwargs["startFrame"]) - if("preRoll" in kwargs): - self.__preRoll = int(kwargs["preRoll"]) - if("postRoll" in kwargs): - self.__postRoll = int(kwargs["postRoll"]) + # Project technical datas. + self.__framerate = int(float(kwargs["fps"])) if "fps" in kwargs else 0 + self.__ratio = kwargs["ratio"] if "ratio" in kwargs else "" + self.__resolution = int(kwargs["resolution"]) if "resolution" in kwargs else 1080 + self.__startFrame = int(kwargs["startFrame"]) if "startFrame" in kwargs else 1000 + self.__preRoll = int(kwargs["preRoll"]) if "preRoll" in kwargs else 24 + self.__postRoll = int(kwargs["postRoll"]) if "postRoll" in kwargs else 24 + # Project categories. self.__categories = [] self.__currentCategory = 0 + + # Project file management. + self.__supportFileTree = False + self.__mountPoint = str(kwargs["mountPoint"]) if "mountPoint" in kwargs else "" + self.__rootPoint = str(kwargs["rootPoint"]) if "rootPoint" in kwargs else "" + self.__outputFilenameAsset = str(kwargs["outputFilenameAsset"]) if "outputFilenameAsset" in kwargs else "" + self.__outputFilenameShot = str(kwargs["outputFilenameShot"]) if "outputFilenameShot" in kwargs else "" + self.__outputFolderPathAsset = str(kwargs["outputFolderPathAsset"]) if "outputFolderPathAsset" in kwargs else "" + self.__outputFolderPathShot = str(kwargs["outputFolderPathShot"]) if "outputFolderPathShot" in kwargs else "" + self.__workingFilenameAsset = str(kwargs["workingFilenameAsset"]) if "workingFilenameAsset" in kwargs else "" + self.__workingFilenameShot = str(kwargs["workingFilenameShot"]) if "workingFilenameShot" in kwargs else "" + self.__workingFolderPathAsset = str(kwargs["workingFolderPathAsset"]) if "workingFolderPathAsset" in kwargs else "" + self.__workingFolderPathShot = str(kwargs["workingFolderPathShot"]) if "workingFolderPathShot" in kwargs else "" + + if(os.path.isdir(self.__mountPoint) and self.__rootPoint != "" + and self.__outputFilenameAsset != "" and self.__outputFilenameShot !="" + and self.__outputFolderPathAsset != "" and self.__outputFolderPathShot != "" + and self.__workingFilenameAsset != "" and self.__workingFilenameShot != "" + and self.__workingFolderPathAsset != "" and self.__workingFolderPathShot != ""): + self.__supportFileTree = True + + self.__specialCharactersList = [" ", "-", "'", "\"", "`", "^"] @property def id(self): @@ -87,6 +110,41 @@ def description(self, description): """ self.__description = description + @property + def tasks(self): + """Get the tasks of the projects. + + Returns: + list(class:`Task`): Tasks. + """ + return self.__tasks + + @tasks.setter + def tasks(self, tasks): + """Set the tasks of the project. + + Args: + tasks (list: class:`Tasks`): New tasks. + """ + self.__tasks = tasks + + def addTask(self, newTask): + """Add a task to project. + + Args: + newTask (class: "Task"): New task to add. + """ + self.__tasks.append(newTask) + + @property + def rawDatas(self): + """Get the raw datas of the class. + + Returns: + dict: Raw datas + """ + return self.__rawDatas + @property def framerate(self): """Get the framerate of the project. @@ -197,7 +255,236 @@ def entities(self): # TODO: Move to comprehensive list. entities = [] if(len(self.categories) > 0): - for category in self.categories: + for category in self.__categories: for entity in category.entities: entities.append(entity) - return entities \ No newline at end of file + return entities + + @property + def outputFilenameAsset(self): + """Get the filename structure (output only) for assets. + + Returns: + str: Output filename for assets. + """ + return self.__outputFilenameAsset + + @property + def outputFilenameShot(self): + """Get the filename structure (output only) for shots. + + Returns: + str: Output filename for shots. + """ + return self.__outputFilenameShot + + @property + def outputFolderpathAsset(self): + """Get the folder path structure (output only) for assets. + + Returns: + str: Output folder path for assets. + """ + return self.__mountPoint + self.__rootPoint + os.sep + self.__outputFolderPathAsset.replace("", self.__name, 1) + + @property + def outputFolderpathShot(self): + """Get the folder path structure (output only) for shots. + + Returns: + str: Output folder path for shot. + """ + return self.__mountPoint + self.__rootPoint + os.sep + self.__outputFolderPathShot.replace("", self.__name, 1) + + @property + def workingFilenameAsset(self): + """Get the filename structure (working files only) for assets. + + Returns: + str: Output filename for assets. + """ + return self.__workingFilenameAsset + + @property + def workingFilenameShot(self): + """Get the filename structure (working files only) for assets. + + Returns: + str: Output filename for shots. + """ + return self.__workingFilenameShot + + @property + def workingFolderpathAsset(self): + """Get the folder path structure (working files only) for assets. + + Returns: + str: Output folder path for assets. + """ + return self.__mountPoint + self.__rootPoint + os.sep + self.__workingFolderPathAsset.replace("", self.__name, 1) + + @property + def workingFolderpathShot(self): + """Get the folder path structure (working files only) for shots. + + Returns: + str: Output folder path for shots. + """ + return self.__mountPoint + self.__rootPoint + os.sep + self.__workingFolderPathShot.replace("", self.__name, 1) + + def getLastVersion(self, entity, taskType): + """Find the next version for publishing + + Args: + entity (class: `Entity`): Entity. + taskType (class: `Task`): Task. + + Returns: + int: Version number. + """ + versionNumber = 0 + for version in entity.versions: + if(version.task.id == taskType.id and version.revisionNumber > versionNumber): + versionNumber = version.revisionNumber + versionNumber = versionNumber + 1 + + return versionNumber + + def getFolderpath(self, exportType="output", category=None, entity=None, taskType=None, versionNumber=-1, **kwargs): + """Get the folderpath for entity. + + Args: + exportType (str, optional): Export type, "output" ou "working". Defaults to "output". + category (class:`Category`, optional): Category of the entity. Defaults to None. + entity (class:`Entity`, optional): Entity. Defaults to None. + taskType (class:`Task`, optional): Task. Defaults to None. + versionNumber (int, optional): Version of the entity, use "-1" for autocount. Defaults to 0. + + Returns: + str: Folder path. + """ + categoryName = category.name + entityName = entity.name + taskName = taskType.name.lower() + + # This is need because our production filetree on IZES isn't correctly setup. + if(entity.type == "Assets"): + categoryName = categoryName.lower() + + for specialCharacter in self.__specialCharactersList: + categoryName = categoryName.replace(specialCharacter, "_") + entityName = entityName.replace(specialCharacter, "_") + taskName = taskName.replace(specialCharacter, "_") + + path = "" + + if(versionNumber == -1): + # Find the last version number automaticly. + versionNumber = self.getLastVersion(entity=entity, taskType=taskType) + + if(exportType == "output"): + if(category.type == "Assets"): + path = self.outputFolderpathAsset + path = path.replace("", categoryName) + path = path.replace("", entityName) + else: + path = self.outputFolderpathShot + path = path.replace("", categoryName) + path = path.replace("", entityName) + elif(exportType == "working"): + if(category.type == "Assets"): + path = self.workingFolderpathAsset + path = path.replace("", categoryName) + path = path.replace("", entityName) + else: + path = self.workingFolderpathShot + path = path.replace("", categoryName) + path = path.replace("", entityName) + else: + return "%s_%s_%s_V%03d/" % (categoryName, entityName, taskName, versionNumber) + + path = path.replace("", taskName) + + if("withoutVersion" in kwargs): + path = path.replace("/", "") + else: + path = path.replace("", "V%03d" % versionNumber) + + return path + + def getFilename(self, exportType="output", category=None, entity=None, taskType=None, versionNumber=-1): + """Get the filename for the entity. + + Args: + exportType (str, optional): Export type, "output" ou "working". Defaults to "output". + category (class:`Category`, optional): Category of the entity. Defaults to None. + entity (class:`Entity`, optional): Entity. Defaults to None. + taskType (class:`Task`, optional): Task. Defaults to None. + versionNumber (int, optional): Version of the entity, use "-1" for autocount. Defaults to 0. + + Returns: + str: File name. + """ + categoryName = category.name.lower() + entityName = entity.name + taskName = taskType.name.lower() + + for specialCharacter in self.__specialCharactersList: + categoryName = categoryName.replace(specialCharacter, "_") + entityName = entityName.replace(specialCharacter, "_") + taskName = taskName.replace(specialCharacter, "_") + + filename = "" + + if(versionNumber == -1): + # Find the last version number automaticly. + versionNumber = self.getLastVersion(entity=entity, taskType=taskType) + + if(exportType == "output"): + if(category.type == "Assets"): + filename = self.outputFilenameAsset + filename = filename.replace("", categoryName) + filename = filename.replace("", entityName) + else: + filename = self.outputFilenameShot + filename = filename.replace("", categoryName) + filename = filename.replace("", entityName) + elif(exportType == "working"): + if(category.type == "Assets"): + filename = self.workingFilenameAsset + filename = filename.replace("", categoryName) + filename = filename.replace("", entityName) + else: + filename = self.outputFilenameShot + filename = filename.replace("", categoryName) + filename = filename.replace("", entityName) + else: + return "%s_%s_%s_V%03d" % (categoryName, entityName, taskName, versionNumber) + + filename = filename.replace("", taskName) + filename = filename.replace("", "V%03d" % versionNumber) + + return filename + + def buildFolderTree(self): + """Build the foldertree for the project. + + Returns: + bool: Status. + """ + for category in self.__categories: + for entity in category.entities: + for task in self.__tasks: + IOUtils.makeFolder(self.getFolderpath(exportType="working", category=category, entity=entity, taskType=task, versionNumber=-1, withoutVersion=True)) + IOUtils.makeFolder(self.getFolderpath(exportType="output", category=category, entity=entity, taskType=task, versionNumber=-1, withoutVersion=True)) + + return True + + @property + def supportFileTree(self): + """Get filetree support status. + + Returns: + bool: Filetree support status. + """ + return self.__supportFileTree \ No newline at end of file diff --git a/Hestia/core/task.py b/Hestia/core/task.py new file mode 100644 index 0000000..f8ee3e1 --- /dev/null +++ b/Hestia/core/task.py @@ -0,0 +1,59 @@ +""" + :package: Hestia + :file: task.py + :brief: Task base class. + :author: PiloeGAO (Leo DEPOIX) + :version: 0.0.4 +""" + +class Task(): + """Task class. + + Args: + entityType (str): Entity's type. Defaults to "Assets". + id (str): Entity's ID. Defaults to "". + name (str, optional): Entity's name. Defaults to "". + description (str, optional): Entity's description. Defaults to "". + """ + def __init__(self, taskType = "Assets", id = "", name = "", **kwargs): + # Common datas. + self.__type = taskType + self.__id = id + self.__name = name + + self.__rawDatas = kwargs["rawDatas"] if "rawDatas" in kwargs else "" + @property + def id(self): + """Get the id of the entity. + + Returns: + str: Entity's ID. + """ + return self.__id + + @property + def name(self): + """Get the name of the entity. + + Returns: + str : The name of the entity. + """ + return self.__name + + @property + def type(self): + """Get the type of the class. + + Returns: + str: Task type. + """ + return self.__type + + @property + def rawDatas(self): + """Get the raw datas of the class. + + Returns: + dict: Raw datas + """ + return self.__rawDatas \ No newline at end of file diff --git a/Hestia/core/version.py b/Hestia/core/version.py index 99cb671..50cf261 100644 --- a/Hestia/core/version.py +++ b/Hestia/core/version.py @@ -3,22 +3,24 @@ :file: version.py :brief: Version base class. :author: PiloeGAO (Leo DEPOIX) - :version: 0.0.3 + :version: 0.0.4 """ from os import path class Version(): - def __init__(self, id="", name="", description="", workingPath="", outputPath="", type=""): - self.__id = id - self.__name = name - self.__description = description - self.__workingPath = workingPath - self.__outputPath = outputPath - - if(type == ""): - self.__type = path.splitext(self.__outputPath)[1] - else: - self.__type = type + def __init__(self, id="", name="", description="", task=None, workingPath="", outputPath="", type="", revisionNumber=0, **kwargs): + self.__id = id + self.__name = name + self.__description = description + self.__task = task + + self.__rawDatas = kwargs["rawDatas"] if "rawDatas" in kwargs else "" + + self.__workingPath = workingPath + self.__outputPath = outputPath + + self.__revisionNumber = revisionNumber + self.__type = path.splitext(self.__outputPath)[1] if type == "" else type @property def id(self): @@ -36,7 +38,10 @@ def name(self): Returns: str : The name of the version. """ - return self.__name + if(self.__name == ""): + return "%s: Revision %s" % (self.__task.name, self.__revisionNumber) + else: + return self.__name @name.setter def name(self, name): @@ -64,6 +69,24 @@ def description(self, description): description (str): The description of the version """ self.__description = description + + @property + def task(self): + """Get the task of the version. + + Returns: + class:`Task`: The task of the version. + """ + return self.__task + + @property + def rawDatas(self): + """Get the raw datas of the class. + + Returns: + dict: Raw datas + """ + return self.__rawDatas @property def workingPath(self): @@ -117,4 +140,13 @@ def type(self, type): Args: type (str): The output type of the version """ - self.__type = type \ No newline at end of file + self.__type = type + + @property + def revisionNumber(self): + """Get the revision number of the version. + + Returns: + int : The revision number of the version. + """ + return self.__revisionNumber \ No newline at end of file diff --git a/Hestia/dccs/guerilla/plugins/launchHestiaBrowser.py b/Hestia/dccs/guerilla/plugins/launchHestiaBrowser.py index aafbe9e..08ed44b 100644 --- a/Hestia/dccs/guerilla/plugins/launchHestiaBrowser.py +++ b/Hestia/dccs/guerilla/plugins/launchHestiaBrowser.py @@ -2,7 +2,7 @@ :package: Hestia :file: launchHestiaBrowser.py :author: ldepoix - :version: 0.0.3 + :version: 0.0.4 :brief: Class to start UI from Guerilla Render. """ from guerilla import Modifier, Document, pwarning diff --git a/Hestia/dccs/maya/scripts/launchHestiaBrowser.py b/Hestia/dccs/maya/scripts/launchHestiaBrowser.py index d5387cf..40e07d9 100644 --- a/Hestia/dccs/maya/scripts/launchHestiaBrowser.py +++ b/Hestia/dccs/maya/scripts/launchHestiaBrowser.py @@ -2,7 +2,7 @@ :package: Hestia :file: launchHestiaBrowser.py :author: ldepoix - :version: 0.0.3 + :version: 0.0.4 :brief: Class to start UI from Autodesk Maya. ''' diff --git a/Hestia/dccs/maya/scripts/userSetup.py b/Hestia/dccs/maya/scripts/userSetup.py index 0c36855..0d0040e 100644 --- a/Hestia/dccs/maya/scripts/userSetup.py +++ b/Hestia/dccs/maya/scripts/userSetup.py @@ -2,7 +2,7 @@ :package: Hestia :file: userSetup.py :author: ldepoix - :version: 0.0.3 + :version: 0.0.4 :brief: Autodesk Maya user setup script. ''' diff --git a/Hestia/dccs/maya/scripts/utils.py b/Hestia/dccs/maya/scripts/utils.py index 7cfae11..7714bcf 100644 --- a/Hestia/dccs/maya/scripts/utils.py +++ b/Hestia/dccs/maya/scripts/utils.py @@ -2,7 +2,7 @@ :package: Hestia :file: utils.py :author: ldepoix - :version: 0.0.3 + :version: 0.0.4 :brief: Utils functions for Hestia. ''' diff --git a/Hestia/loginWindow.py b/Hestia/loginWindow.py index e692835..5862cd4 100644 --- a/Hestia/loginWindow.py +++ b/Hestia/loginWindow.py @@ -2,7 +2,7 @@ :package: Hestia :file: loginWindow.py :author: PiloeGAO (Leo DEPOIX) - :version: 0.0.3 + :version: 0.0.4 :brief: Class to create the login window based on QtWidgets. """ import os diff --git a/Hestia/mainWindow.py b/Hestia/mainWindow.py index a774090..80e3749 100644 --- a/Hestia/mainWindow.py +++ b/Hestia/mainWindow.py @@ -2,7 +2,7 @@ :package: Hestia :file: mainWindow.py :author: PiloeGAO (Leo DEPOIX) - :version: 0.0.3 + :version: 0.0.4 :brief: Class to create the main window based on QtWidgets. """ try: @@ -16,6 +16,7 @@ from .core.manager import Manager from .loginWindow import LoginWindow +from .publishWindow import PublishWindow from .preferencesWindow import PreferencesWindow from .ui.header import Header @@ -51,13 +52,17 @@ def __init__(self, manager, parent=None): # Initialize UI. self.initUI() + # Initialize the publish window. + self.publishWindow = None + # Initialize the preference window. - self.preferencesWindow = PreferencesWindow(manager=self.__manager) + self.preferencesWindow = None # Show online login modal if not set to local. + self.loginWindow = None if(self.__manager.mode != "local" and not self.__manager.link.connected): - login = LoginWindow(manager=self.__manager, mainWindow=self) - login.show() + self.loginWindow = LoginWindow(manager=self.__manager, mainWindow=self) + self.loginWindow.show() def resizeEvent(self, event): """Get the size of the window on window resize. @@ -84,10 +89,16 @@ def closeEvent(self, event): event (class: 'QtEvent'): Event. """ if(True): - if(self.__manager.integration != "standalone"): + # Closing other window if Main Window is closed. + if(self.loginWindow != None): self.loginWindow.hide() + if(self.preferencesWindow != None): self.preferencesWindow.hide() + if(self.publishWindow != None): self.publishWindow.hide() + + if(self.__manager.integration.name != "standalone"): # This is needed for embedded Python versions # that won't support *atexit* lib. self.__manager.preferences.savePreferences() + self.__manager.cleanTemporaryFolder() event.accept() else: @@ -134,7 +145,17 @@ def initUI(self): def openPreferencesWindow(self): """Display the preferences window. """ + self.preferencesWindow = PreferencesWindow(manager=self.__manager) self.preferencesWindow.show() + + def openPublishWindow(self, entity): + """Display the publish window. + + Args: + entity (class:`Entity`): Entity datas to publish. + """ + self.publishWindow = PublishWindow(manager=self.__manager, mainWindow=self, entity=entity) + self.publishWindow.show() def updateLog(self, text=""): """Update log in the footer. diff --git a/Hestia/preferencesWindow.py b/Hestia/preferencesWindow.py index ba1d675..daf615e 100644 --- a/Hestia/preferencesWindow.py +++ b/Hestia/preferencesWindow.py @@ -2,7 +2,7 @@ :package: Hestia :file: preferencesWindow.py :author: PiloeGAO (Leo DEPOIX) - :version: 0.0.3 + :version: 0.0.4 :brief: Class to create the preferences window based on QtWidgets. """ try: @@ -13,6 +13,7 @@ from PySide.QtCore import * from PySide.QtGui import * +from .ui.widgets.gridWidget import GridWidget from .ui.widgets.dropDown import DropDown class PreferencesWindow(QWidget): @@ -53,35 +54,60 @@ def initUI(self): # Set the window style. self.setStyle(QStyleFactory.create("Fusion")) - # Set the main layout component. - self.mainLayout = QGridLayout() - self.mainLayout.setSpacing(10) - - # Set window size. + # Tab manager initializer. + self.mainLayout = QVBoxLayout() + self.tabWidget = QTabWidget() + # Set the main settings layout component. # Set service. self.serviceButton = DropDown(name="Service", description="Service used.", datas=self.__servicesAvailables, defaultValue=self.__currentService, functionToInvoke=None) - - self.mainLayout.addWidget(self.serviceButton, 0, 0) # Load previews. self.loadPreviews = QCheckBox("Download previews") self.loadPreviews.setToolTip("Warning: This can slowdown the GUI.") self.loadPreviews.setChecked(self.__loadPreviewStatus) - self.mainLayout.addWidget(self.loadPreviews, 1, 0) + + self.mainSettingsWidget = GridWidget(self.__manager, self, xSize=1, itemList=[ + self.serviceButton, + self.loadPreviews + ]) + self.tabWidget.addTab(self.mainSettingsWidget, "Main Settings") + + # Project manager settings. + self.projectManagerSettingsWidget = QWidget() + + self.projectManagerSettingsLayout = QVBoxLayout() + + if(self.__manager.projects[self.__manager.currentProject].supportFileTree): + self.buildProjectFolderTreeButton = QPushButton("Build folder tree") + self.buildProjectFolderTreeButton.clicked.connect(self.buildProjectFolderTree) + self.projectManagerSettingsLayout.addWidget(self.buildProjectFolderTreeButton) + + self.projectManagerSettingsWidget.setLayout(self.projectManagerSettingsLayout) + self.tabWidget.addTab(self.projectManagerSettingsWidget, "Project Manager Settings") + + # Set main layout to the window. + self.mainLayout.addWidget(self.tabWidget) + # Save button. self.saveButton = QPushButton("Save") self.saveButton.clicked.connect(self.savePreferences) - self.mainLayout.addWidget(self.saveButton, 2, 1) + self.mainLayout.addWidget(self.saveButton) - # Set main layout to the window. self.setLayout(self.mainLayout) + def buildProjectFolderTree(self): + """Build project foldertree. + """ + self.__manager.logging.info("Folder tree generation started.") + self.__manager.projects[self.__manager.currentProject].buildFolderTree() + self.__manager.logging.info("Folder tree successfully generated.") + def savePreferences(self): """Save preferences. """ diff --git a/Hestia/publishWindow.py b/Hestia/publishWindow.py new file mode 100644 index 0000000..58ef4df --- /dev/null +++ b/Hestia/publishWindow.py @@ -0,0 +1,329 @@ +""" + :package: Hestia + :file: publishWindow.py + :author: PiloeGAO (Leo DEPOIX) + :version: 0.0.4 + :brief: Class to create the publish window based on QtWidgets. +""" +import os +from datetime import datetime +import shutil +import sys + +global pysideVers +try: + from PySide2.QtCore import * + from PySide2.QtGui import * + from PySide2.QtWidgets import * + pysideVers = 2 +except: + from PySide.QtCore import * + from PySide.QtGui import * + pysideVers = 1 + +from .core import IOUtils + +from .ui.widgets.iconButton import IconButton +from .ui.widgets.dropDown import DropDown +from .ui.widgets.lineEdit import LineEdit +from .ui.widgets.textEdit import TextEdit +from .ui.widgets.gridWidget import GridWidget + +class PublishWindow(QWidget): + """Publish Window class. + + Args: + manager (class: `Manager`): Manager of Hestia. + entity (class: `Entity`): Entity to publish. + parent (class: `QtWidgets`, optional): PyQt parent. Defaults to None. + """ + def __init__(self, manager, mainWindow, entity, parent=None): + super(PublishWindow, self).__init__(parent=parent) + self.__manager = manager + self.__mainWindow = mainWindow + + self.__currentProject = self.__manager.projects[self.__manager.currentProject] + self.__category = self.__currentProject.categories[self.__currentProject.currentCategory] + self.__entity = entity + + self.__screenshotPath = "" + self.__screenshotSupport = self.__manager.integration.supportScreenshots + + self.__rootPath = os.path.dirname(os.path.abspath(__file__)) + + self.initUI() + + def initUI(self): + """Generate the window. + """ + + # Set the window title. + self.setWindowTitle("Hestia - Publish") + self.setWindowFlags(Qt.WindowStaysOnTopHint | Qt.CustomizeWindowHint | Qt.WindowCloseButtonHint) + + #self.resize(self.__windowWidth, self.__windowHeight) + + # Set the window style. + self.setStyle(QStyleFactory.create("Fusion")) + + # Set the main layout component. + self.mainLayout = QVBoxLayout() + + self.taskLayout = QHBoxLayout() + + # Task drop down. + self.taskDropBox = DropDown(name="Task", description="Task of the publish.", datas=self.getTasksNames()) + self.taskLayout.addWidget(self.taskDropBox) + + # Task status drop drown. + self.taskStatusDropDown = DropDown(name="Status", description="Task status.", datas=["WIP", "WFA"], defaultValue=1) + self.taskLayout.addWidget(self.taskStatusDropDown) + + self.mainLayout.addLayout(self.taskLayout) + + # Publish name. + self.publishName = LineEdit(name="Name", description="Publish Name", defaultValue="Publish %s | %s" % (self.__entity.name, datetime.now().strftime("%d/%m/%Y %H:%M:%S"))) + self.mainLayout.addWidget(self.publishName) + + self.mainLayout.setSpacing(0) + + # Publish comment. + self.publishComment = TextEdit(name="Comment", description="Publish comment.", defaultValue="") + self.mainLayout.addWidget(self.publishComment) + + # Output paths. + self.outputsList = [] + + self.outputScrollArea = QScrollArea() + self.outputGrid = GridWidget(manager=self.__manager, + parentGeometry=self.outputScrollArea.geometry(), + xSize=1, + itemList=self.outputsList, + emptyLabel="No outputs in list") + self.outputScrollArea.setWidget(self.outputGrid) + self.mainLayout.addWidget(self.outputScrollArea) + + self.outputButtonsLayout = QHBoxLayout() + + self.addOutputButton = QPushButton("Add output") + self.addOutputButton.clicked.connect(self.addOutput) + self.outputButtonsLayout.addWidget(self.addOutputButton) + + self.removeOutputButton = QPushButton("Remove last output") + self.removeOutputButton.clicked.connect(self.removeOutput) + self.outputButtonsLayout.addWidget(self.removeOutputButton) + + self.mainLayout.addLayout(self.outputButtonsLayout) + + # Preview path. + self.previewLayout = QHBoxLayout() + + self.previewTitle = QLabel("Preview: None") + self.previewLayout.addWidget(self.previewTitle) + + self.previewLayout.addStretch() + + if(pysideVers == 2): + iconPath = self.__rootPath + "/ui/icons/folder2-open.svg" + else: + iconPath = self.__rootPath + "/ui/icons/folder2-open.png" + self.previewButton = IconButton(name="Open file browser",description="Locate the screenshot", + iconPath=iconPath, iconScale=16, + functionToInvoke=self.openScreenshotExplorer) + self.previewLayout.addWidget(self.previewButton) + + if(self.__screenshotSupport): + if(pysideVers == 2): + iconPath = self.__rootPath + "/ui/icons/camera.svg" + else: + iconPath = self.__rootPath + "/ui/icons/camera.png" + self.screenshotButton = IconButton(name="Take screenshot", description="Take screenshot of current scene.", + iconPath=iconPath, iconScale=16, + status=1, functionToInvoke=self.takeScreenshot) + self.previewLayout.addWidget(self.screenshotButton) + + if(pysideVers == 2): + iconPath = self.__rootPath + "/ui/icons/camera-reels.svg" + else: + iconPath = self.__rootPath + "/ui/icons/camera-reels.png" + self.playblastButton = IconButton(name="Take a video", description="Take video of current scene.", + iconPath=iconPath, iconScale=16, + status=1, functionToInvoke=self.takePlayblast) + self.previewLayout.addWidget(self.playblastButton) + + self.mainLayout.addLayout(self.previewLayout) + + # Publish button. + self.publishButton = QPushButton("Publish file") + self.publishButton.clicked.connect(self.publish) + self.mainLayout.addWidget(self.publishButton) + + # Set main layout to the window. + self.setLayout(self.mainLayout) + + def getTasksNames(self): + """Get all tasks for entity. + + Returns: + list: str: List of task names. + """ + return [task.name for task in self.__entity.tasks] + + def addOutput(self): + """Add a new line in the output list. + """ + availableFormats = [format[1:].upper() for format in self.__manager.integration.availableFormats] + if(len(self.outputsList) < len(availableFormats)): + outputChoice = DropDown(name="Export Type", datas=availableFormats) + self.outputsList.append(outputChoice) + + self.outputGrid = GridWidget(manager=self.__manager, + parentGeometry=self.outputScrollArea.geometry(), + xSize=1, + itemList=self.outputsList, + emptyLabel="No outputs in list") + self.outputScrollArea.setWidget(self.outputGrid) + + def removeOutput(self): + """Remove the last line of the output list. + """ + if(len(self.outputsList) > 0): + del self.outputsList[-1] + + self.outputGrid = GridWidget(manager=self.__manager, + parentGeometry=self.outputScrollArea.geometry(), + xSize=1, + itemList=self.outputsList, + emptyLabel="No outputs in list") + self.outputScrollArea.setWidget(self.outputGrid) + + def openScreenshotExplorer(self): + """Open file explorer to set screenshot path. + """ + dialog = QFileDialog(self) + dialog.setFileMode(QFileDialog.AnyFile) + if dialog.exec_(): + self.__screenshotPath = dialog.selectedFiles()[0] + + self.previewTitle.setText("Preview : %s" % self.__screenshotPath) + + def takeScreenshot(self): + """Take a screenshot of the scene. + """ + path = self.__manager.tempFolder + os.sep + "preview.PNG" + self.__manager.integration.takePlayblast(startFrame=-1, endFrame=-1, path=path) + self.__screenshotPath = path + + self.previewTitle.setText("Preview : %s" % self.__screenshotPath) + + def takePlayblast(self): + """Take a playblast of the scene. + """ + if(sys.platform.startswith('win32')): + format = "avi" + elif(sys.platform.startswith('darwin')): + format = "mov" + else: + return False + + path_raw = self.__manager.tempFolder + os.sep + "preview_raw." + format + self.__manager.integration.takePlayblast(startFrame=-2, endFrame=0, path=path_raw) + + path = self.__manager.tempFolder + os.sep + "preview.mp4" + conversionStatus = IOUtils.videoConverter(path_raw, path) + os.remove(path_raw) + + if(conversionStatus and os.path.isfile(path)): + self.__screenshotPath = path + self.previewTitle.setText("Preview : %s" % self.__screenshotPath) + else: + self.__manager.logging.error("Conversion failed, aborting.") + + def publish(self): + """Publish function. + """ + publishTask = self.__entity.tasks[self.taskDropBox.currentValue] + publishTaskStatus = self.taskStatusDropDown.datas[self.taskStatusDropDown.currentValue] + + publishName = self.publishName.currentValue + publishComment = self.publishComment.currentValue + publishVersion = int(self.__currentProject.getLastVersion(entity=self.__entity, taskType=publishTask)) + workingPath = self.__currentProject.getFolderpath(exportType="working", category=self.__category, entity=self.__entity, taskType=publishTask, versionNumber=publishVersion) + workingFileName = self.__currentProject.getFilename(exportType="working", category=self.__category, entity=self.__entity, taskType=publishTask, versionNumber=publishVersion) + self.__manager.integration.defaultFormat + outputPath = self.__currentProject.getFolderpath(exportType="output", category=self.__category, entity=self.__entity, taskType=publishTask, versionNumber=publishVersion) + outputFileNames = [] + previewFilename = self.__currentProject.getFilename(exportType="output", category=self.__category, entity=self.__entity, taskType=publishTask, versionNumber=publishVersion) + "_preview" + + for i, widget in enumerate(self.outputsList): + outputFileNames.append(self.__currentProject.getFilename(exportType="output", category=self.__category, entity=self.__entity, taskType=publishTask, versionNumber=publishVersion) + self.__manager.integration.availableFormats[widget.currentValue]) + + if(publishName != "" and publishComment != "" + and workingPath != "" and workingFileName != "" + and outputPath != "" and os.path.isfile(self.__screenshotPath)): + + self.hide() + self.__mainWindow.hide() + + # Create the working and output directories. + IOUtils.makeFolder(workingPath) + IOUtils.makeFolder(outputPath) + + # Export files from DCC. + self.__manager.logging.info("Writing the working file.") + publishWorkingFilePath = "" + workingSaveStatus = self.__manager.integration.saveFile(workingPath + os.sep + workingFileName) + if(workingSaveStatus): + publishWorkingFilePath = workingPath + os.sep + workingFileName + + publishOutputFilePaths = [] + for i, outputFilename in enumerate(outputFileNames): + self.__manager.logging.info("Writing output file %s/%s." % (i, len(outputFileNames))) + path = outputPath + os.sep + outputFilename + extension = os.path.splitext(outputFilename)[1] + exportStatus = self.__manager.integration.exportSelection(path=path, extension=extension) + + # If export failed for current export (example: file already exist), + # remove the unnecessary file. + if(exportStatus): + publishOutputFilePaths.append(path) + + # Copy the preview to output folder. + self.__manager.logging.info("Writing the preview file.") + IOUtils.copyFile(self.__screenshotPath, outputPath, newName=previewFilename) + publishPreviewFilePath = outputPath + os.sep + previewFilename + os.path.splitext(self.__screenshotPath)[1] + + self.__manager.logging.info("Publishing online.") + # Publishing files to the project manager. + self.__manager.link.publish( + entity=self.__entity, + name=publishName, + comment=publishComment, + taskTypeID=publishTask.id, + taskStatus=publishTaskStatus, + version=publishVersion, + software=self.__manager.integration.name, + outputType="", # TODO: Ouput type need to be a list (sometimes ABC and PNG can be published simultaneously) + workingFilePath=publishWorkingFilePath, + outputFiles=publishOutputFilePaths, + previewFilePath=publishPreviewFilePath + ) + self.__manager.logging.info("Publishing done.") + + # TODO: Find a way to refresh project. + # Refreshing the project to get last datas uploaded. + + self.__mainWindow.show() + + # Temporary warning message. + # Show information message. + QMessageBox.warning(self, self.tr("Hestia"), + self.tr("Please close Hestia to get latest updates."), + QMessageBox.NoButton, + QMessageBox.Ok) + + else: + # Show information message. + QMessageBox.warning(self, self.tr("Hestia"), + self.tr("Some datas are missing."), + QMessageBox.NoButton, + QMessageBox.Ok) \ No newline at end of file diff --git a/Hestia/ui/contentView.py b/Hestia/ui/contentView.py index 4dc8629..0d56519 100644 --- a/Hestia/ui/contentView.py +++ b/Hestia/ui/contentView.py @@ -2,7 +2,7 @@ :package: Hestia :file: contentView.py :author: PiloeGAO (Leo DEPOIX) - :version: 0.0.3 + :version: 0.0.4 :brief: Class to create the content view of the window. """ import os @@ -112,6 +112,7 @@ def buildEntityList(self): for entity in range(len(self.__entities)): newEntity = EntityWidget(manager=self.__manager, + mainWindow=self.__mainWindow, asset=self.__entities[entity], iconSize=100, status=1) diff --git a/Hestia/ui/folderTreeView.py b/Hestia/ui/folderTreeView.py index 2e64faf..b15326a 100644 --- a/Hestia/ui/folderTreeView.py +++ b/Hestia/ui/folderTreeView.py @@ -2,7 +2,7 @@ :package: Hestia :file: folderTreeView.py :author: PiloeGAO (Leo DEPOIX) - :version: 0.0.3 + :version: 0.0.4 :brief: Class to create the folder tree of the window. """ try: @@ -81,14 +81,22 @@ def initUI(self): def changeCurrentCategoryType(self): """Updating the current selected category when type change. + + Returns: + bool: Status. """ - category = [category for category in self.__project.categories if category.type == self.__availableTypes[self.type.currentValue]][0] + category = [category for category in self.__project.categories if category.type == self.__availableTypes[self.type.currentValue]] + if(len(category) > 0): + category = category[0] + else: + return False if category != None: self.__project.currentCategory = self.__project.categories.index(category) self.__mainWindow.refreshCategory() self.refresh() + return True def refresh(self): """Force refresh of the widget. diff --git a/Hestia/ui/footer.py b/Hestia/ui/footer.py index edf6d7c..8735402 100644 --- a/Hestia/ui/footer.py +++ b/Hestia/ui/footer.py @@ -2,7 +2,7 @@ :package: Hestia :file: footer.py :author: PiloeGAO (Leo DEPOIX) - :version: 0.0.3 + :version: 0.0.4 :brief: Class to create the footer of the window. """ try: diff --git a/Hestia/ui/header.py b/Hestia/ui/header.py index 5fde4f6..590f6b1 100644 --- a/Hestia/ui/header.py +++ b/Hestia/ui/header.py @@ -2,7 +2,7 @@ :package: Hestia :file: header.py :author: PiloeGAO (Leo DEPOIX) - :version: 0.0.3 + :version: 0.0.4 :brief: Class to create the header of the window. """ import os diff --git a/Hestia/ui/icons/camera-reels.png b/Hestia/ui/icons/camera-reels.png new file mode 100644 index 0000000..a9e5065 Binary files /dev/null and b/Hestia/ui/icons/camera-reels.png differ diff --git a/Hestia/ui/icons/camera-reels.svg b/Hestia/ui/icons/camera-reels.svg new file mode 100644 index 0000000..1aa7b1c --- /dev/null +++ b/Hestia/ui/icons/camera-reels.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/Hestia/ui/icons/camera.png b/Hestia/ui/icons/camera.png new file mode 100644 index 0000000..67ddb7e Binary files /dev/null and b/Hestia/ui/icons/camera.png differ diff --git a/Hestia/ui/icons/camera.svg b/Hestia/ui/icons/camera.svg new file mode 100644 index 0000000..fb337fe --- /dev/null +++ b/Hestia/ui/icons/camera.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/Hestia/ui/icons/folder2-open.png b/Hestia/ui/icons/folder2-open.png new file mode 100644 index 0000000..01adc50 Binary files /dev/null and b/Hestia/ui/icons/folder2-open.png differ diff --git a/Hestia/ui/icons/folder2-open.svg b/Hestia/ui/icons/folder2-open.svg new file mode 100644 index 0000000..59d8382 --- /dev/null +++ b/Hestia/ui/icons/folder2-open.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/Hestia/ui/widgets/categoryWidget.py b/Hestia/ui/widgets/categoryWidget.py index ab9fb3f..e05c23c 100644 --- a/Hestia/ui/widgets/categoryWidget.py +++ b/Hestia/ui/widgets/categoryWidget.py @@ -3,7 +3,7 @@ :file: categoryWidget.py :brief: Category widget. :author: PiloeGAO (Leo DEPOIX) - :version: 0.0.3 + :version: 0.0.4 """ try: from PySide2.QtCore import * diff --git a/Hestia/ui/widgets/dropDown.py b/Hestia/ui/widgets/dropDown.py index 8cba301..6fca380 100644 --- a/Hestia/ui/widgets/dropDown.py +++ b/Hestia/ui/widgets/dropDown.py @@ -3,7 +3,7 @@ :file: dropDown.py :brief: Drop down field with text. :author: PiloeGAO (Leo DEPOIX) - :version: 0.0.3 + :version: 0.0.4 """ try: from PySide2.QtCore import * diff --git a/Hestia/ui/widgets/entityWidget.py b/Hestia/ui/widgets/entityWidget.py index 2ff7386..b71ff94 100644 --- a/Hestia/ui/widgets/entityWidget.py +++ b/Hestia/ui/widgets/entityWidget.py @@ -3,11 +3,12 @@ :file: entityWidget.py :brief: Entity widget. :author: PiloeGAO (Leo DEPOIX) - :version: 0.0.3 + :version: 0.0.4 """ from gazu import asset from Hestia.core.category import Category from os import path +import time global pysideVers try: @@ -33,10 +34,11 @@ class EntityWidget(QWidget): status (int, optional): Status of the button. Defaults to 1. parent (class: `QWidget`, optional): Parent widget. Defaults to None. """ - def __init__(self, manager=None, asset=None, iconSize=64, status=1, parent=None): + def __init__(self, manager=None, mainWindow=None, asset=None, iconSize=64, status=1, parent=None): super(EntityWidget, self).__init__(parent=parent) - self.__manager = manager - self.__asset = asset + self.__manager = manager + self.__mainWindow = mainWindow + self.__asset = asset self.__rootPath = path.dirname(path.abspath(__file__)) @@ -145,16 +147,30 @@ def createRightClickMenu(self, event): currentProject = self.__manager.projects[self.__manager.currentProject] if(currentProject.categories[currentProject.currentCategory].type == "Assets"): # Assign shader to asset button. - assignShader = menu.addAction("Assign shader to current object") - assignShader.triggered.connect(self.assignShaderToSelectedAsset) + if(len(self.__versions) > 0 and self.__manager.integration.name != "standalone"): + assignShader = menu.addAction("Assign shader to current object") + assignShader.triggered.connect(self.assignShaderToSelectedAsset) elif(currentProject.categories[currentProject.currentCategory].type == "Shots"): # Setup scene for shot button. setupShot = menu.addAction("Setup shot") setupShot.triggered.connect(self.setupSceneForShot) # Export to HSHOT button. + menu.addSeparator() extractAssets = menu.addAction("Export to Hestia shot (.hshot)") extractAssets.triggered.connect(self.exportShotToHSHOT) + # Entity publish area. + if(self.__manager.projects[self.__manager.currentProject].supportFileTree + and self.__manager.integration.name != "standalone"): + + menu.addSeparator() + if(len(self.__versions) > 0): + openFileMenu = menu.addAction("Open file") + openFileMenu.triggered.connect(self.openFile) + + publishEntity = menu.addAction("Publish selection") + publishEntity.triggered.connect(self.publishSelectionToProjectManager) + menu.exec_(event.globalPos()) def assignShaderToSelectedAsset(self): @@ -235,4 +251,37 @@ def exportShotToHSHOT(self): self.__manager.integration.extractAssets() + return True + + def openFile(self): + """Function to open file. + + Returns: + bool: Function status. + """ + # Show information message. + warningPopup = QMessageBox.warning(self, self.tr("Hestia"), + self.tr("Openning a new file will loose the current datas.\n" + \ + "Please save before."), + QMessageBox.Cancel, + QMessageBox.Ok) + + if(warningPopup == QMessageBox.Ok): + self.__currentVersion = self.__versions[self.versionDropDown.currentValue] + openStatus = self.__manager.integration.openFile(self.__currentVersion) + + if(not openStatus): + self.__manager.logging.error("Open failed.") + + return openStatus + else: + return False + + def publishSelectionToProjectManager(self): + """Function to publish entity to project manager. + + Returns: + bool: Function status. + """ + self.__mainWindow.openPublishWindow(entity=self.__asset) return True \ No newline at end of file diff --git a/Hestia/ui/widgets/gridWidget.py b/Hestia/ui/widgets/gridWidget.py index d472765..35ed5c5 100644 --- a/Hestia/ui/widgets/gridWidget.py +++ b/Hestia/ui/widgets/gridWidget.py @@ -3,7 +3,7 @@ :file: gridWidget.py :brief: Custom grid system for PyQt. :author: PiloeGAO (Leo DEPOIX) - :version: 0.0.3 + :version: 0.0.4 """ try: from PySide2.QtCore import * diff --git a/Hestia/ui/widgets/iconButton.py b/Hestia/ui/widgets/iconButton.py index 4867868..d53783c 100644 --- a/Hestia/ui/widgets/iconButton.py +++ b/Hestia/ui/widgets/iconButton.py @@ -3,7 +3,7 @@ :file: iconButton.py :brief: Button with icon. :author: PiloeGAO (Leo DEPOIX) - :version: 0.0.3 + :version: 0.0.4 """ import os diff --git a/Hestia/ui/widgets/lineEdit.py b/Hestia/ui/widgets/lineEdit.py index 1af2cf2..eaaf1c3 100644 --- a/Hestia/ui/widgets/lineEdit.py +++ b/Hestia/ui/widgets/lineEdit.py @@ -3,7 +3,7 @@ :file: lineEdit.py :brief: Drop down field with text. :author: PiloeGAO (Leo DEPOIX) - :version: 0.0.3 + :version: 0.0.4 """ try: from PySide2.QtCore import * diff --git a/Hestia/ui/widgets/pathSelector.py b/Hestia/ui/widgets/pathSelector.py index 32915ed..b6c5d97 100644 --- a/Hestia/ui/widgets/pathSelector.py +++ b/Hestia/ui/widgets/pathSelector.py @@ -3,7 +3,7 @@ :file: pathSelector.py :brief: Path field with search button. :author: PiloeGAO (Leo DEPOIX) - :version: 0.0.3 + :version: 0.0.4 """ try: from PySide2.QtCore import * diff --git a/Hestia/ui/widgets/sliderSelector.py b/Hestia/ui/widgets/sliderSelector.py index ec69e42..28c9679 100644 --- a/Hestia/ui/widgets/sliderSelector.py +++ b/Hestia/ui/widgets/sliderSelector.py @@ -3,7 +3,7 @@ :file: sliderSelector.py :brief: Slider with labels under it. :author: PiloeGAO (Leo DEPOIX) - :version: 0.0.3 + :version: 0.0.4 """ try: diff --git a/Hestia/ui/widgets/textEdit.py b/Hestia/ui/widgets/textEdit.py new file mode 100644 index 0000000..e74e5f3 --- /dev/null +++ b/Hestia/ui/widgets/textEdit.py @@ -0,0 +1,74 @@ +""" + :package: Hestia + :file: textEdit.py + :brief: Text Edit field with title. + :author: PiloeGAO (Leo DEPOIX) + :version: 0.0.4 +""" +try: + from PySide2.QtCore import * + from PySide2.QtGui import * + from PySide2.QtWidgets import * +except: + from PySide.QtCore import * + from PySide.QtGui import * + +class TextEdit(QWidget): + """Text Edit class. + + Args: + name (str, optional): Text of the button. Defaults to "". + description (str, optional): Tooltip. Defaults to "". + defaultValue (str, optional): Default value ID. Defaults to "". + parent (QtWidgets, optional): Parent widget. Defaults to None. + """ + def __init__(self, name="", description="", defaultValue="", parent=None): + super(TextEdit, self).__init__(parent=parent) + + self.__name = name + self.__description = description + self.__currentValue = defaultValue + + self.initUI() + + @property + def currentValue(self): + """Return value of the selected value in textEdit. + + Returns: + str: value. + """ + return self.__currentValue + + def initUI(self): + """Main UI creation function. + """ + # Setting the main layout as Vertical. + self.mainLayout = QVBoxLayout() + + # Create title. + self.title = QLabel(self.__name + " : ") + + # Add description as tooltip. + self.title.setToolTip(self.__description) + + # Add title to main layout. + self.mainLayout.addWidget(self.title) + + # Create the text edit. + self.textEdit = QTextEdit() + self.textEdit.setText(self.__currentValue) + + # Connect text edit with update method. + self.textEdit.textChanged.connect(self.changeCurrentValue) + + # Add text edit to main layout. + self.mainLayout.addWidget(self.textEdit) + + # Add the main layout to the window. + self.setLayout(self.mainLayout) + + def changeCurrentValue(self): + """Set current value from drop down. + """ + self.__currentValue = self.textEdit.toPlainText() \ No newline at end of file diff --git a/Hestia_0.0.2.png b/Hestia_0.0.2.png deleted file mode 100644 index 01c11c8..0000000 Binary files a/Hestia_0.0.2.png and /dev/null differ diff --git a/Hestia_0.0.4.png b/Hestia_0.0.4.png new file mode 100644 index 0000000..cb0b817 Binary files /dev/null and b/Hestia_0.0.4.png differ diff --git a/README.md b/README.md index 8026a75..612f7ae 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,16 @@ # Hestia **Project in active development! Please don't use it in production.** -An assets browser and a shot assembly system for CGI/VFX productions. +A production management system for CGI/VFX productions. -![Hestia (Asset Browser - V0.0.2)](./Hestia_0.0.2.png) +![Hestia (Asset Browser - V0.0.4)](./Hestia_0.0.4.png) ## FEATURES - [x] Mostly implemented Kitsu link. - [x] Preferences system to store login credentials. - [x] Maya integration with Asset import/reference system. - [x] Maya scene setup. +- [x] Basic publish system. ## Getting Started @@ -23,6 +24,8 @@ You can download stable versions in github release or clone the repository. git clone https://github.com/PiloeGAO/Hestia.git ``` +Download pre-build binaries for [FFMPEG](https://www.ffmpeg.org/) (Needed for playblast publishing - Only available for Windows). + ## Installation 1. Create a virtual environment for Python2.7 or Python 3.8. @@ -39,6 +42,8 @@ For Python 3.8: pip install -r requirements3.txt ``` +3. Create a folder "ffmpeg" inside of "./Hestia/core" and copy ffmpeg files inside. + ## Usage Start the standalone app with this command: @@ -70,7 +75,9 @@ Trello Board (with releases notes): https://trello.com/b/90POyZZC/hestia-dev-boa - [x] Designing the main system - [ ] Building the HSHOT file exporter - [ ] Building the HSHOT file importer - +- Publish System: + - [x] Basic publish system to Kitsu. + - [x] Take screenshot/playblast directly inside of Maya. ## Contributing Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.