diff --git a/app.py b/app.py index 93f55b56..8ea66651 100644 --- a/app.py +++ b/app.py @@ -7,17 +7,15 @@ # By accessing, using, copying or modifying this work you indicate your # agreement to the Shotgun Pipeline Toolkit Source Code License. All rights # not expressly granted therein are reserved by Shotgun Software Inc. - -""" -Multi Publish - -""" import os import sgtk class MultiPublish2(sgtk.platform.Application): + """ + Main app class for publisher. - (VALIDATE, PUBLISH) = range(2) + Command registration and API methods. + """ def init_app(self): """ @@ -26,16 +24,15 @@ def init_app(self): tk_multi_publish2 = self.import_module("tk_multi_publish2") # register command - cb = lambda : tk_multi_publish2.show_dialog(self) - menu_caption = "Publish..." + cb = lambda: tk_multi_publish2.show_dialog(self) + menu_caption = "Publish" menu_options = { "short_name": "publish", - "description": "Publishing of data into Shotgun", - + "description": "Publishing of data to Shotgun", # dark themed icon for engines that recognize this format "icons": { "dark": { - "png": os.path.join(self.disk_location, "resources", "publish_menu_icon.png") + "png": os.path.join(self.disk_location, "icon_256_dark.png") } } } @@ -49,7 +46,9 @@ def context_change_allowed(self): return True def destroy_app(self): - - self.log_debug("Destroying tk-multi-publish") + """ + Tear down the app + """ + self.log_debug("Destroying tk-multi-publish2") diff --git a/hooks/collector.py b/hooks/collector.py index f6984f1f..4da6353e 100644 --- a/hooks/collector.py +++ b/hooks/collector.py @@ -13,31 +13,45 @@ HookBaseClass = sgtk.get_hook_baseclass() + class GenericSceneCollector(HookBaseClass): """ - Collector that operates on the maya scene + A generic collector that handles files and general objects. """ def process_current_scene(self, parent_item): - return None + """ + Analyzes the current scene open in a DCC and parents a subtree of items + under the parent_item passed in. + + :param parent_item: Root item instance + """ + # default implementation does not do anything def process_file(self, parent_item, path): + """ + Analyzes the given file and creates one or more items + to represent it. + :param parent_item: Root item instance + :param path: Path to analyze + :returns: The main item that was created + """ file_name = os.path.basename(path) (file_name_no_ext, file_extension) = os.path.splitext(file_name) if file_extension in [".jpeg", ".jpg", ".png"]: file_item = parent_item.create_item("file.image", "Image File", file_name) - file_item.set_thumbnail(path) - file_item.set_icon(os.path.join(self.disk_location, "icons", "image.png")) + file_item.set_thumbnail_from_path(path) + file_item.set_icon_from_path(os.path.join(self.disk_location, "icons", "image.png")) elif file_extension in [".mov", ".mp4"]: file_item = parent_item.create_item("file.movie", "Movie File", file_name) - file_item.set_icon(os.path.join(self.disk_location, "icons", "quicktime.png")) + file_item.set_icon_from_path(os.path.join(self.disk_location, "icons", "quicktime.png")) else: file_item = parent_item.create_item("file", "Generic File", file_name) - file_item.set_icon(os.path.join(self.disk_location, "icons", "page.png")) + file_item.set_icon_from_path(os.path.join(self.disk_location, "icons", "page.png")) file_item.properties["extension"] = file_extension file_item.properties["path"] = path diff --git a/hooks/file_publisher.py b/hooks/file_publisher.py index 8b411a7c..8a8e3592 100644 --- a/hooks/file_publisher.py +++ b/hooks/file_publisher.py @@ -15,27 +15,54 @@ HookBaseClass = sgtk.get_hook_baseclass() -class SceneHook(HookBaseClass): +class GenericFilePublishPlugin(HookBaseClass): """ - Testing the new awesome hooks + Plugin for creating generic publishes in Shotgun """ @property def icon(self): + """ + Path to an png icon on disk + """ return os.path.join(self.disk_location, "icons", "shotgun.png") @property - def title(self): + def name(self): + """ + One line display name describing the plugin + """ return "Publish files to Shotgun" @property - def description_html(self): + def description(self): + """ + Verbose, multi-line description of what the plugin does. This + can contain simple html for formatting. + """ return """ Publishes files to shotgun. """ @property def settings(self): + """ + Dictionary defining the settings that this plugin expects to recieve + through the settings parameter in the accept, validate, publish and + finalize methods. + + A dictionary on the following form:: + + { + "Settings Name": { + "type": "settings_type", + "default": "default_value", + "description": "One line description of the setting" + } + + The type string should be one of the data types that toolkit accepts + as part of its environment configuration. + """ return { "File Types": { "type": "list", @@ -57,11 +84,38 @@ def settings(self): } @property - def subscriptions(self): + def item_filters(self): + """ + List of item types that this plugin is interested in. + Only items matching entries in this list will be presented + to the accept() method. Strings can contain glob patters + such as *, for example ["maya.*", "file.maya"] + """ return ["file*"] def accept(self, log, settings, item): - + """ + Method called by the publisher to determine if an item + is of any interest to this plugin. Only items matching + the filters defined via the item_filters property will + be presented to this method. + + A publish task will be generated for each item accepted + here. Returns a dictionary with the following booleans: + + - accepted: Indicates if the plugin is interested in + this value at all. + - required: If set to True, the publish task is + required and cannot be disabled. + - enabled: If True, the publish task will be + enabled in the UI by default. + + :param log: Logger to output feedback to. + :param settings: Dictionary of Settings. The keys are strings, matching the keys + returned in the settings property. The values are `Setting` instances. + :param item: Item to process + :returns: dictionary with boolean keys accepted, required and enabled + """ extension = item.properties["extension"] if extension.startswith("."): extension = extension[1:] @@ -89,10 +143,30 @@ def _get_matching_publish_type(self, extension, settings): return None def validate(self, log, settings, item): + """ + Validates the given item to check that it is ok to publish. + Returns a boolean to indicate validity. Use the logger to + output further details around why validation has failed. + + :param log: Logger to output feedback to. + :param settings: Dictionary of Settings. The keys are strings, matching the keys + returned in the settings property. The values are `Setting` instances. + :param item: Item to process + :returns: True if item is valid, False otherwise. + """ return True def publish(self, log, settings, item): - + """ + Executes the publish logic for the given + item and settings. Use the logger to give + the user status updates. + + :param log: Logger to output feedback to. + :param settings: Dictionary of Settings. The keys are strings, matching the keys + returned in the settings property. The values are `Setting` instances. + :param item: Item to process + """ extension = item.properties["extension"] if extension.startswith("."): extension = extension[1:] @@ -108,7 +182,7 @@ def publish(self, log, settings, item): "path": "file://%s" % item.properties["path"], "name": "%s%s" % (item.properties["prefix"], item.properties["extension"]), "version_number": item.properties["version"], - "thumbnail_path": item.get_thumbnail(), + "thumbnail_path": item.get_thumbnail_as_path(), "published_file_type": publish_type, } @@ -119,6 +193,16 @@ def publish(self, log, settings, item): def finalize(self, log, settings, item): + """ + Execute the finalization pass. This pass executes once + all the publish tasks have completed, and can for example + be used to version up files. + + :param log: Logger to output feedback to. + :param settings: Dictionary of Settings. The keys are strings, matching the keys + returned in the settings property. The values are `Setting` instances. + :param item: Item to process + """ pass diff --git a/hooks/maya.basic/icons/image.png b/hooks/icons/image.png similarity index 100% rename from hooks/maya.basic/icons/image.png rename to hooks/icons/image.png diff --git a/hooks/maya.basic/icons/quicktime.png b/hooks/icons/quicktime.png similarity index 100% rename from hooks/maya.basic/icons/quicktime.png rename to hooks/icons/quicktime.png diff --git a/hooks/maya.basic/alembic_publisher.py b/hooks/maya.basic/alembic_publisher.py index 26e87779..572142c9 100644 --- a/hooks/maya.basic/alembic_publisher.py +++ b/hooks/maya.basic/alembic_publisher.py @@ -9,34 +9,56 @@ # not expressly granted therein are reserved by Shotgun Software Inc. import sgtk import os -import re -import time -import glob -import maya.cmds as cmds -import maya.mel as mel HookBaseClass = sgtk.get_hook_baseclass() -class SceneHook(HookBaseClass): +class MayaAlembicCachePublishPlugin(HookBaseClass): """ - Testing the new awesome hooks + Plugin for creating publishes for maya alembic files that exist on disk """ @property def icon(self): + """ + Path to an png icon on disk + """ return os.path.join(self.disk_location, "icons", "shotgun.png") @property - def title(self): + def name(self): + """ + One line display name describing the plugin + """ return "Publish Alembic" @property - def description_html(self): + def description(self): + """ + Verbose, multi-line description of what the plugin does. This + can contain simple html for formatting. + """ return """Extracts alembic geometry.""" @property def settings(self): + """ + Dictionary defining the settings that this plugin expects to recieve + through the settings parameter in the accept, validate, publish and + finalize methods. + + A dictionary on the following form:: + + { + "Settings Name": { + "type": "settings_type", + "default": "default_value", + "description": "One line description of the setting" + } + + The type string should be one of the data types that toolkit accepts + as part of its environment configuration. + """ return { "Publish Type": { "type": "shotgun_publish_type", @@ -46,15 +68,52 @@ def settings(self): } @property - def subscriptions(self): + def item_filters(self): + """ + List of item types that this plugin is interested in. + Only items matching entries in this list will be presented + to the accept() method. Strings can contain glob patters + such as *, for example ["maya.*", "file.maya"] + """ return ["maya.alembic_file"] def accept(self, log, settings, item): - + """ + Method called by the publisher to determine if an item + is of any interest to this plugin. Only items matching + the filters defined via the item_filters property will + be presented to this method. + + A publish task will be generated for each item accepted + here. Returns a dictionary with the following booleans: + + - accepted: Indicates if the plugin is interested in + this value at all. + - required: If set to True, the publish task is + required and cannot be disabled. + - enabled: If True, the publish task will be + enabled in the UI by default. + + :param log: Logger to output feedback to. + :param settings: Dictionary of Settings. The keys are strings, matching the keys + returned in the settings property. The values are `Setting` instances. + :param item: Item to process + :returns: dictionary with boolean keys accepted, required and enabled + """ return {"accepted": True, "required": False, "enabled": True} def validate(self, log, settings, item): - + """ + Validates the given item to check that it is ok to publish. + Returns a boolean to indicate validity. Use the logger to + output further details around why validation has failed. + + :param log: Logger to output feedback to. + :param settings: Dictionary of Settings. The keys are strings, matching the keys + returned in the settings property. The values are `Setting` instances. + :param item: Item to process + :returns: True if item is valid, False otherwise. + """ # make sure parent is published if not item.parent.properties.get("is_published"): log.error("You must publish the main scene in order to publish alembic!") @@ -64,9 +123,17 @@ def validate(self, log, settings, item): def publish(self, log, settings, item): - + """ + Executes the publish logic for the given + item and settings. Use the logger to give + the user status updates. + + :param log: Logger to output feedback to. + :param settings: Dictionary of Settings. The keys are strings, matching the keys + returned in the settings property. The values are `Setting` instances. + :param item: Item to process + """ # save the maya scene - scene_folder = os.path.dirname(item.properties["path"]) filename = os.path.basename(item.properties["path"]) (filename_no_ext, extension) = os.path.splitext(filename) @@ -93,7 +160,7 @@ def publish(self, log, settings, item): "path": "file://%s" % publish_path, "name": filename, "version_number": version, - "thumbnail_path": item.get_thumbnail(), + "thumbnail_path": item.get_thumbnail_as_path(), "published_file_type": settings["Publish Type"].value, "dependency_ids": [item.parent.properties["shotgun_publish_id"]] } @@ -105,7 +172,16 @@ def publish(self, log, settings, item): def finalize(self, log, settings, item): - + """ + Execute the finalization pass. This pass executes once + all the publish tasks have completed, and can for example + be used to version up files. + + :param log: Logger to output feedback to. + :param settings: Dictionary of Settings. The keys are strings, matching the keys + returned in the settings property. The values are `Setting` instances. + :param item: Item to process + """ mov_path = item.properties["path"] log.info("Deleting %s" % item.properties["path"]) sgtk.util.filesystem.safe_delete_file(mov_path) diff --git a/hooks/maya.basic/collector.py b/hooks/maya.basic/collector.py index 0ff545bc..887ba6a8 100644 --- a/hooks/maya.basic/collector.py +++ b/hooks/maya.basic/collector.py @@ -13,6 +13,7 @@ HookBaseClass = sgtk.get_hook_baseclass() + class MayaSceneCollector(HookBaseClass): """ Collector that operates on the maya scene @@ -20,16 +21,24 @@ class MayaSceneCollector(HookBaseClass): def process_file(self, parent_item, path): """ - Extend the base processing capabilities with a maya - file detection which determines the maya project. + Analyzes the given file and creates one or more items + to represent it. Extends the base processing + capabilities with a maya file detection which + determines the maya project. + + :param parent_item: Root item instance + :param path: Path to analyze + :returns: The main item that was created """ if path.endswith(".ma") or path.endswith(".mb"): + # run base class logic to set basic properties for us item = super(MayaSceneCollector, self).process_file(parent_item, path) - item.update_type("file.maya", "Maya File") - item.set_icon(os.path.join(self.disk_location, "icons", "maya.png")) + item.type = "file.maya" + item.display_type = "Maya File" + item.set_icon_from_path(os.path.join(self.disk_location, "icons", "maya.png")) # set the workspace root for this item @@ -48,17 +57,27 @@ def process_file(self, parent_item, path): return item - def process_current_scene(self, parent_item): + """ + Analyzes the current scene open in a DCC and parents a subtree of items + under the parent_item passed in. + :param parent_item: Root item instance + """ + # create an item representing the current maya scene item = self.create_current_maya_scene(parent_item) + # look for playblasts self.create_playblasts(item) + # look for caches self.create_alembic_caches(item) - return item - def create_current_maya_scene(self, parent_item): + """ + Creates an item that represents the current maya scene. + :param parent_item: Parent Item instance + :returns: Item of type maya.scene + """ scene_file = cmds.file(query=True, sn=True) if scene_file == "": # make more pythonic @@ -74,12 +93,20 @@ def create_current_maya_scene(self, parent_item): current_scene.properties["path"] = scene_file current_scene.properties["project_root"] = cmds.workspace(q=True, rootDirectory=True) - current_scene.set_icon(os.path.join(self.disk_location, "icons", "maya.png")) + current_scene.set_icon_from_path(os.path.join(self.disk_location, "icons", "maya.png")) return current_scene def create_alembic_caches(self, parent_item): + """ + Creates items for alembic caches + Looks for a 'project_root' property on the parent item, + and if such exists, look for quicktimes in a 'cache/alembic' subfolder. + + :param parent_item: Parent Item instance + :returns: List of items of type maya.alembic_file + """ # use the workspace_root property on the parent to # extract playblast objects items = [] @@ -93,13 +120,21 @@ def create_alembic_caches(self, parent_item): if path.endswith(".abc"): item = parent_item.create_item("maya.alembic_file", "Alembic Cache File", filename) item.properties["path"] = path - item.set_icon(os.path.join(self.disk_location, "icons", "alembic.png")) + item.set_icon_from_path(os.path.join(self.disk_location, "icons", "alembic.png")) items.append(item) return items def create_playblasts(self, parent_item): + """ + Creates items for quicktime playblasts. + + Looks for a 'project_root' property on the parent item, + and if such exists, look for quicktimes in a 'movies' subfolder. + :param parent_item: Parent Item instance + :returns: List of items with type maya.playblast + """ # use the workspace_root property on the parent to # extract playblast objects items = [] @@ -113,7 +148,7 @@ def create_playblasts(self, parent_item): if path.endswith(".mov"): item = parent_item.create_item("maya.playblast", "Playblast in Maya Project", filename) item.properties["path"] = path - item.set_icon(os.path.join(self.disk_location, "icons", "popcorn.png")) + item.set_icon_from_path(os.path.join(self.disk_location, "icons", "popcorn.png")) items.append(item) return items diff --git a/hooks/maya.basic/icons/file.png b/hooks/maya.basic/icons/file.png deleted file mode 100644 index fd26aeb4..00000000 Binary files a/hooks/maya.basic/icons/file.png and /dev/null differ diff --git a/hooks/maya.basic/icons/play.png b/hooks/maya.basic/icons/play.png deleted file mode 100644 index 681574cd..00000000 Binary files a/hooks/maya.basic/icons/play.png and /dev/null differ diff --git a/hooks/maya.basic/playblast_publisher.py b/hooks/maya.basic/playblast_publisher.py index f29237ba..6b1bc270 100644 --- a/hooks/maya.basic/playblast_publisher.py +++ b/hooks/maya.basic/playblast_publisher.py @@ -9,50 +9,119 @@ # not expressly granted therein are reserved by Shotgun Software Inc. import sgtk import os -import re -import time -import glob -import maya.cmds as cmds HookBaseClass = sgtk.get_hook_baseclass() -class PlayblastPublisher(HookBaseClass): +class MayaPlayblastReviewPlugin(HookBaseClass): """ - Testing the new awesome hooks + Plugin for creating publishes for playblast quicktimes that exist on disk """ @property def icon(self): + """ + Path to an png icon on disk + """ return os.path.join(self.disk_location, "icons", "play.png") @property - def title(self): + def name(self): + """ + One line display name describing the plugin + """ return "Review Playblast" @property - def description_html(self): + def description(self): + """ + Verbose, multi-line description of what the plugin does. This + can contain simple html for formatting. + """ return """Send playblast to Shotgun for review.""" @property def settings(self): + """ + Dictionary defining the settings that this plugin expects to recieve + through the settings parameter in the accept, validate, publish and + finalize methods. + + A dictionary on the following form:: + + { + "Settings Name": { + "type": "settings_type", + "default": "default_value", + "description": "One line description of the setting" + } + + The type string should be one of the data types that toolkit accepts + as part of its environment configuration. + """ return {} @property - def subscriptions(self): + def item_filters(self): + """ + List of item types that this plugin is interested in. + Only items matching entries in this list will be presented + to the accept() method. Strings can contain glob patters + such as *, for example ["maya.*", "file.maya"] + """ return ["maya.playblast"] def accept(self, log, settings, item): - + """ + Method called by the publisher to determine if an item + is of any interest to this plugin. Only items matching + the filters defined via the item_filters property will + be presented to this method. + + A publish task will be generated for each item accepted + here. Returns a dictionary with the following booleans: + + - accepted: Indicates if the plugin is interested in + this value at all. + - required: If set to True, the publish task is + required and cannot be disabled. + - enabled: If True, the publish task will be + enabled in the UI by default. + + :param log: Logger to output feedback to. + :param settings: Dictionary of Settings. The keys are strings, matching the keys + returned in the settings property. The values are `Setting` instances. + :param item: Item to process + :returns: dictionary with boolean keys accepted, required and enabled + """ return {"accepted": True, "required": False, "enabled": True} def validate(self, log, settings, item): - + """ + Validates the given item to check that it is ok to publish. + Returns a boolean to indicate validity. Use the logger to + output further details around why validation has failed. + + :param log: Logger to output feedback to. + :param settings: Dictionary of Settings. The keys are strings, matching the keys + returned in the settings property. The values are `Setting` instances. + :param item: Item to process + :returns: True if item is valid, False otherwise. + """ return True def publish(self, log, settings, item): - + """ + Executes the publish logic for the given + item and settings. Use the logger to give + the user status updates. + + :param log: Logger to output feedback to. + :param settings: Dictionary of Settings. The keys are strings, matching the keys + returned in the settings property. The values are `Setting` instances. + :param item: Item to process + """ filename = os.path.basename(item.properties["path"]) (file_name_no_ext, _) = os.path.splitext(filename) @@ -88,7 +157,16 @@ def publish(self, log, settings, item): def finalize(self, log, settings, item): - + """ + Execute the finalization pass. This pass executes once + all the publish tasks have completed, and can for example + be used to version up files. + + :param log: Logger to output feedback to. + :param settings: Dictionary of Settings. The keys are strings, matching the keys + returned in the settings property. The values are `Setting` instances. + :param item: Item to process + """ mov_path = item.properties["path"] log.info("Deleting %s" % item.properties["path"]) sgtk.util.filesystem.safe_delete_file(mov_path) diff --git a/hooks/maya.basic/scene_publisher.py b/hooks/maya.basic/scene_publisher.py index 6c383891..316c3106 100644 --- a/hooks/maya.basic/scene_publisher.py +++ b/hooks/maya.basic/scene_publisher.py @@ -17,25 +17,52 @@ HookBaseClass = sgtk.get_hook_baseclass() -class SceneHook(HookBaseClass): +class MayaScenePublishPlugin(HookBaseClass): """ - Testing the new awesome hooks + Plugin for versioning and publishing the current maya scene """ @property def icon(self): + """ + Path to an png icon on disk + """ return os.path.join(self.disk_location, "icons", "shotgun.png") @property - def title(self): + def name(self): + """ + One line display name describing the plugin + """ return "Publish Maya Scene" @property - def description_html(self): + def description(self): + """ + Verbose, multi-line description of what the plugin does. This + can contain simple html for formatting. + """ return """Publishes the current maya scene. This will create a versioned backup and publish that.""" @property def settings(self): + """ + Dictionary defining the settings that this plugin expects to recieve + through the settings parameter in the accept, validate, publish and + finalize methods. + + A dictionary on the following form:: + + { + "Settings Name": { + "type": "settings_type", + "default": "default_value", + "description": "One line description of the setting" + } + + The type string should be one of the data types that toolkit accepts + as part of its environment configuration. + """ return { "Publish Type": { "type": "shotgun_publish_type", @@ -45,11 +72,38 @@ def settings(self): } @property - def subscriptions(self): + def item_filters(self): + """ + List of item types that this plugin is interested in. + Only items matching entries in this list will be presented + to the accept() method. Strings can contain glob patters + such as *, for example ["maya.*", "file.maya"] + """ return ["maya.scene"] def accept(self, log, settings, item): - + """ + Method called by the publisher to determine if an item + is of any interest to this plugin. Only items matching + the filters defined via the item_filters property will + be presented to this method. + + A publish task will be generated for each item accepted + here. Returns a dictionary with the following booleans: + + - accepted: Indicates if the plugin is interested in + this value at all. + - required: If set to True, the publish task is + required and cannot be disabled. + - enabled: If True, the publish task will be + enabled in the UI by default. + + :param log: Logger to output feedback to. + :param settings: Dictionary of Settings. The keys are strings, matching the keys + returned in the settings property. The values are `Setting` instances. + :param item: Item to process + :returns: dictionary with boolean keys accepted, required and enabled + """ project_root = item.properties["project_root"] context_file = os.path.join(project_root, "shotgun.context") log.debug("Looking for context file in %s" % context_file) @@ -58,14 +112,24 @@ def accept(self, log, settings, item): with open(context_file, "rb") as fh: context_str = fh.read() context_obj = sgtk.Context.deserialize(context_str) - item.set_context(context_obj) + item.context = context_obj except Exception, e: log.warning("Could not read saved context %s: %s" % (context_file, e)) return {"accepted": True, "required": False, "enabled": True} def validate(self, log, settings, item): - + """ + Validates the given item to check that it is ok to publish. + Returns a boolean to indicate validity. Use the logger to + output further details around why validation has failed. + + :param log: Logger to output feedback to. + :param settings: Dictionary of Settings. The keys are strings, matching the keys + returned in the settings property. The values are `Setting` instances. + :param item: Item to process + :returns: True if item is valid, False otherwise. + """ if item.properties["path"] is None: log.error("Please save your scene before you continue!") return False @@ -115,7 +179,16 @@ def _maya_find_additional_scene_dependencies(self): return ref_paths def publish(self, log, settings, item): - + """ + Executes the publish logic for the given + item and settings. Use the logger to give + the user status updates. + + :param log: Logger to output feedback to. + :param settings: Dictionary of Settings. The keys are strings, matching the keys + returned in the settings property. The values are `Setting` instances. + :param item: Item to process + """ # save the maya scene log.info("Saving maya scene...") cmds.file(save=True, force=True) @@ -164,7 +237,7 @@ def publish(self, log, settings, item): "path": "file://%s" % publish_path, "name": filename, "version_number": version_to_use, - "thumbnail_path": item.get_thumbnail(), + "thumbnail_path": item.get_thumbnail_as_path(), "published_file_type": settings["Publish Type"].value, #"dependency_paths": self._maya_find_additional_scene_dependencies(), # need to update core for this to work } @@ -178,7 +251,16 @@ def publish(self, log, settings, item): def finalize(self, log, settings, item): - + """ + Execute the finalization pass. This pass executes once + all the publish tasks have completed, and can for example + be used to version up files. + + :param log: Logger to output feedback to. + :param settings: Dictionary of Settings. The keys are strings, matching the keys + returned in the settings property. The values are `Setting` instances. + :param item: Item to process + """ project_root = item.properties["project_root"] context_file = os.path.join(project_root, "shotgun.context") with open(context_file, "wb") as fh: diff --git a/hooks/version_creator.py b/hooks/version_creator.py index 4403ab39..1c2c10b2 100644 --- a/hooks/version_creator.py +++ b/hooks/version_creator.py @@ -14,25 +14,52 @@ HookBaseClass = sgtk.get_hook_baseclass() -class SceneHook(HookBaseClass): +class ShotgunReviewPlugin(HookBaseClass): """ - Testing the new awesome hooks + Plugin for sending quicktimes and images to shotgun for review. """ @property def icon(self): + """ + Path to an png icon on disk + """ return os.path.join(self.disk_location, "icons", "play.png") @property - def title(self): + def name(self): + """ + One line display name describing the plugin + """ return "Send files to Shotgun review" @property - def description_html(self): + def description(self): + """ + Verbose, multi-line description of what the plugin does. This + can contain simple html for formatting. + """ return """Uploads files to Shotgun for Review.""" @property def settings(self): + """ + Dictionary defining the settings that this plugin expects to recieve + through the settings parameter in the accept, validate, publish and + finalize methods. + + A dictionary on the following form:: + + { + "Settings Name": { + "type": "settings_type", + "default": "default_value", + "description": "One line description of the setting" + } + + The type string should be one of the data types that toolkit accepts + as part of its environment configuration. + """ return { "File Extensions": { "type": "str", @@ -53,11 +80,38 @@ def settings(self): } @property - def subscriptions(self): + def item_filters(self): + """ + List of item types that this plugin is interested in. + Only items matching entries in this list will be presented + to the accept() method. Strings can contain glob patters + such as *, for example ["maya.*", "file.maya"] + """ return ["file.image", "file.movie"] def accept(self, log, settings, item): - + """ + Method called by the publisher to determine if an item + is of any interest to this plugin. Only items matching + the filters defined via the item_filters property will + be presented to this method. + + A publish task will be generated for each item accepted + here. Returns a dictionary with the following booleans: + + - accepted: Indicates if the plugin is interested in + this value at all. + - required: If set to True, the publish task is + required and cannot be disabled. + - enabled: If True, the publish task will be + enabled in the UI by default. + + :param log: Logger to output feedback to. + :param settings: Dictionary of Settings. The keys are strings, matching the keys + returned in the settings property. The values are `Setting` instances. + :param item: Item to process + :returns: dictionary with boolean keys accepted, required and enabled + """ valid_extensions = [] for ext in settings["File Extensions"].value.split(","): @@ -76,11 +130,31 @@ def accept(self, log, settings, item): return {"accepted": False} def validate(self, log, settings, item): + """ + Validates the given item to check that it is ok to publish. + Returns a boolean to indicate validity. Use the logger to + output further details around why validation has failed. + + :param log: Logger to output feedback to. + :param settings: Dictionary of Settings. The keys are strings, matching the keys + returned in the settings property. The values are `Setting` instances. + :param item: Item to process + :returns: True if item is valid, False otherwise. + """ return True def publish(self, log, settings, item): - + """ + Executes the publish logic for the given + item and settings. Use the logger to give + the user status updates. + + :param log: Logger to output feedback to. + :param settings: Dictionary of Settings. The keys are strings, matching the keys + returned in the settings property. The values are `Setting` instances. + :param item: Item to process + """ data = { "project": item.context.project, "code": item.properties["prefix"], @@ -102,7 +176,7 @@ def publish(self, log, settings, item): version = self.parent.shotgun.create("Version", data) # and payload - thumb = item.get_thumbnail() + thumb = item.get_thumbnail_as_path() if settings["Upload"].value: log.info("Uploading content") @@ -111,10 +185,24 @@ def publish(self, log, settings, item): # only upload thumb if we are not uploading the content # with uploaded content, the thumb is automatically extracted. log.info("Uploading thumbnail") - self.parent.shotgun.upload_thumbnail("Version", version["id"], item.get_thumbnail()) + self.parent.shotgun.upload_thumbnail( + "Version", + version["id"], + item.get_thumbnail_as_path() + ) def finalize(self, log, settings, item): + """ + Execute the finalization pass. This pass executes once + all the publish tasks have completed, and can for example + be used to version up files. + + :param log: Logger to output feedback to. + :param settings: Dictionary of Settings. The keys are strings, matching the keys + returned in the settings property. The values are `Setting` instances. + :param item: Item to process + """ pass diff --git a/icon_256.png b/icon_256.png index e418c8a7..8e05bd5f 100644 Binary files a/icon_256.png and b/icon_256.png differ diff --git a/icon_256_dark.png b/icon_256_dark.png new file mode 100644 index 00000000..bd44f788 Binary files /dev/null and b/icon_256_dark.png differ diff --git a/info.yml b/info.yml index dc3af2c0..ae725efd 100644 --- a/info.yml +++ b/info.yml @@ -8,20 +8,16 @@ # agreement to the Shotgun Pipeline Toolkit Source Code License. All rights # not expressly granted therein are reserved by Shotgun Software Inc. -# Metadata defining the behaviour and requirements for this app - -# expected fields in the configuration file for this app configuration: collector: type: hook - description: "Logic for enumerating things" + description: "Logic for extracting items from the scene and from dropped files." default_value: "{self}/collector.py:{self}/maya.basic/collector.py" - publish_plugins: type: list - description: "List of automations to process" + description: "List of publish plugins." values: type: dict items: @@ -75,7 +71,7 @@ description: "Provides UI and functionality to publish files to Shotgun." # Required minimum versions for this item to run requires_shotgun_version: -requires_core_version: "v0.14.58" +requires_core_version: "v0.18.0" requires_engine_version: # this app works in all engines - it does not contain diff --git a/python/tk_multi_publish2/__init__.py b/python/tk_multi_publish2/__init__.py index bc122210..39a32aab 100644 --- a/python/tk_multi_publish2/__init__.py +++ b/python/tk_multi_publish2/__init__.py @@ -8,14 +8,14 @@ # agreement to the Shotgun Pipeline Toolkit Source Code License. All rights # not expressly granted therein are reserved by Shotgun Software Inc. -import sgtk from sgtk.platform.qt import QtCore, QtGui + def show_dialog(app): """ - Show the main loader dialog + Show the main dialog ui - :param app: The parent App + :param app: The parent App """ # defer imports so that the app works gracefully in batch modes from .dialog import AppDialog diff --git a/python/tk_multi_publish2/dialog.py b/python/tk_multi_publish2/dialog.py index ddc5e6fc..e79f14b3 100644 --- a/python/tk_multi_publish2/dialog.py +++ b/python/tk_multi_publish2/dialog.py @@ -14,15 +14,10 @@ from sgtk.platform.qt import QtCore, QtGui from .ui.dialog import Ui_Dialog - from .processing import PluginManager -from .item import Item from .tree_item import PublishTreeWidgetItem, PublishTreeWidgetTask, PublishTreeWidgetPlugin - from .publish_logging import PublishLogWrapper -from .processing import ValidationFailure, PublishFailure - # import frameworks settings = sgtk.platform.import_framework("tk-framework-shotgunutils", "settings") help_screen = sgtk.platform.import_framework("tk-framework-qtwidgets", "help_screen") @@ -30,31 +25,35 @@ logger = sgtk.platform.get_logger(__name__) + class AppDialog(QtGui.QWidget): """ Main dialog window for the App """ + # the yin-yang modes (ITEM_CENTRIC, PLUGIN_CENTRIC) = range(2) + # details ui panes (SUMMARY_DETAILS, TASK_DETAILS, PLUGIN_DETAILS, ITEM_DETAILS, BLANK_DETAILS) = range(5) + # main right hand side tabs (DETAILS_TAB, PROGRESS_TAB) = range(2) + # modes for handling context (DISPLAY_CONTEXT, EDIT_CONTEXT) = range(2) def __init__(self, parent=None): """ - Constructor - - :param parent: The parent QWidget for this control + :param parent: The parent QWidget for this control """ QtGui.QWidget.__init__(self, parent) # create a settings manager where we can pull and push prefs later # prefs in this manager are shared self._settings_manager = settings.UserSettings(sgtk.platform.current_bundle()) + # create a background task manager self._task_manager = task_manager.BackgroundTaskManager(self, start_processing=True, @@ -71,7 +70,8 @@ def __init__(self, parent=None): self.ui.splitter.setStretchFactor(1, 1) # give tree view 360 width, rest to details pane - # note: value of second option does not seem to matter (as long as it's there) + # note: value of second option does not seem to + # matter (as long as it's there) self.ui.splitter.setSizes([360, 100]) # set up tree view to look slick @@ -89,16 +89,17 @@ def __init__(self, parent=None): self.ui.publish.clicked.connect(self.do_publish) self._close_ui_on_publish_click = False - + # create menu on the cog button self._menu = QtGui.QMenu() self._actions = [] self.ui.options.setMenu(self._menu) + self._refresh_action = QtGui.QAction("Refresh", self) self._refresh_action.setIcon(QtGui.QIcon(QtGui.QPixmap(":/tk_multi_publish2/reload.png"))) self._refresh_action.triggered.connect(self._refresh) self._menu.addAction(self._refresh_action) - self._separator_1= QtGui.QAction(self) + self._separator_1 = QtGui.QAction(self) self._separator_1.setSeparator(True) self._menu.addAction(self._separator_1) @@ -115,17 +116,17 @@ def __init__(self, parent=None): self._menu.addAction(self._separator_2) self._check_all_action = QtGui.QAction("Check All", self) - self._check_all_action.triggered.connect(lambda : self._check_all(True)) + self._check_all_action.triggered.connect(lambda: self._check_all(True)) self._menu.addAction(self._check_all_action) - self._uncheck_all_action = QtGui.QAction("Unckeck All", self) - self._uncheck_all_action.triggered.connect(lambda : self._check_all(False)) + self._uncheck_all_action = QtGui.QAction("Uncheck All", self) + self._uncheck_all_action.triggered.connect(lambda: self._check_all(False)) self._menu.addAction(self._uncheck_all_action) # when the description is updated self.ui.summary_comments.textChanged.connect(self._on_publish_comment_change) - # context edit + # context edit - TEMPORARY, PENDING DESIGN self.ui.summary_context_edit.clicked.connect(self._enable_context_edit_mode) self.ui.summary_context_select.set_bg_task_manager(self._task_manager) self.ui.summary_context_select.set_searchable_entity_types( @@ -146,7 +147,7 @@ def __init__(self, parent=None): self.ui.summary_thumbnail.screen_grabbed.connect(self._update_item_thumbnail) self.ui.item_thumbnail.screen_grabbed.connect(self._update_item_thumbnail) - # mode + # current yin-yang mode self._display_mode = self.ITEM_CENTRIC # currently displayed item @@ -167,6 +168,7 @@ def closeEvent(self, event): logger.debug("CloseEvent Received. Begin shutting down UI.") try: + # shut down global search widget self.ui.summary_context_select.destroy() # shut down main threadpool self._task_manager.shut_down() @@ -219,7 +221,7 @@ def _enable_context_edit_mode(self): def _update_context(self, entity_type, entity_id): self.ui.context_stack.setCurrentIndex(self.DISPLAY_CONTEXT) ctx = self._bundle.sgtk.context_from_entity(entity_type, entity_id) - self._current_item.set_context(ctx) + self._current_item.context = ctx self.ui.summary_context.setText(str(ctx)) def _on_publish_comment_change(self): @@ -240,7 +242,7 @@ def _on_summary_thumbnail_captured(self, pixmap): """ if not self._current_item: raise TankError("No current item set!") - self._current_item.set_thumbnail_pixmap(pixmap) + self._current_item.thumbnail = pixmap def _update_item_thumbnail(self, pixmap): """ @@ -249,15 +251,15 @@ def _update_item_thumbnail(self, pixmap): """ if not self._current_item: raise TankError("No current item set!") - self._current_item.set_thumbnail_pixmap(pixmap) + self._current_item.thumbnail = pixmap def _create_summary_details(self, item): self._current_item = item self.ui.details_stack.setCurrentIndex(self.SUMMARY_DETAILS) - self.ui.summary_icon.setPixmap(item.icon_pixmap) + self.ui.summary_icon.setPixmap(item.icon) self.ui.summary_comments.setPlainText(item.description) - self.ui.summary_thumbnail.set_thumbnail(item.thumbnail_pixmap) + self.ui.summary_thumbnail.set_thumbnail(item.thumbnail) self.ui.summary_header.setText("Publish summary for %s" % item.name) self.ui.summary_context.setText(str(item.context)) @@ -267,10 +269,10 @@ def _create_item_details(self, item): self._current_item = item self.ui.details_stack.setCurrentIndex(self.ITEM_DETAILS) - self.ui.item_icon.setPixmap(item.icon_pixmap) + self.ui.item_icon.setPixmap(item.icon) self.ui.item_name.setText(item.name) self.ui.item_type.setText(item.display_type) - self.ui.item_thumbnail.set_thumbnail(item.thumbnail_pixmap) + self.ui.item_thumbnail.set_thumbnail(item.thumbnail) self.ui.item_settings.set_static_data( [(p, item.properties[p]) for p in item.properties] @@ -281,7 +283,7 @@ def _create_task_details(self, task): self._current_item = None self.ui.details_stack.setCurrentIndex(self.TASK_DETAILS) - self.ui.task_icon.setPixmap(task.plugin.icon_pixmap) + self.ui.task_icon.setPixmap(task.plugin.icon) self.ui.task_name.setText(task.plugin.name) self.ui.task_description.setText(task.plugin.description) @@ -293,7 +295,7 @@ def _create_plugin_details(self, plugin): self._current_item = None self.ui.details_stack.setCurrentIndex(self.PLUGIN_DETAILS) - self.ui.plugin_icon.setPixmap(plugin.icon_pixmap) + self.ui.plugin_icon.setPixmap(plugin.icon) self.ui.plugin_name.setText(plugin.name) diff --git a/python/tk_multi_publish2/drop_area.py b/python/tk_multi_publish2/drop_area.py index 4c3b7fe2..4e4c49bb 100644 --- a/python/tk_multi_publish2/drop_area.py +++ b/python/tk_multi_publish2/drop_area.py @@ -1,4 +1,4 @@ -# Copyright (c) 2016 Shotgun Software Inc. +# Copyright (c) 2017 Shotgun Software Inc # # CONFIDENTIAL AND PROPRIETARY # @@ -18,6 +18,8 @@ def drop_area(cls): """ A class decorator which adds needed overrides to any QWidget so it can behave like a drop area and emit something_dropped signals + + @todo - move this into the qtwidgets framework """ class WrappedClass(cls): """ diff --git a/python/tk_multi_publish2/item.py b/python/tk_multi_publish2/item.py index 9eb8cb3f..87684abe 100644 --- a/python/tk_multi_publish2/item.py +++ b/python/tk_multi_publish2/item.py @@ -11,16 +11,30 @@ import sgtk from sgtk.platform.qt import QtCore, QtGui - from .ui.item import Ui_Item logger = sgtk.platform.get_logger(__name__) + class Item(QtGui.QFrame): """ - Represents a right hand side details pane in the UI + Widget representing a single item in the left hand side tree view. + (Connected to a designer ui setup) + + Each item has got the following associated properties: + + - An area which can either be a checkbox for selection + or a "dot" which signals progress udpates + + - An icon + + - A header text + + These widgets are plugged in as subcomponents inside a QTreeWidgetItem + via the PublishTreeWidget class hierarchy. """ + # status of checkbox / progress ( NEUTRAL, PROCESSING, @@ -35,33 +49,50 @@ class Item(QtGui.QFrame): def __init__(self, parent=None): """ - Constructor - - :param parent: The parent QWidget for this control + :param parent: The parent QWidget for this control """ QtGui.QFrame.__init__(self, parent) # set up the UI self.ui = Ui_Item() self.ui.setupUi(self) - self.set_status(self.NEUTRAL) + @property + def checkbox(self): + """ + The checkbox widget associated with this item + """ + return self.ui.checkbox + + @property + def icon(self): + """ + The icon pixmap associated with this item + """ + return self.ui.icon.pixmap() + def set_icon(self, pixmap): """ - Specifies if this item should be a plugin or a item + Set the icon to be used + + :param pixmap: Square icon pixmap to use """ self.ui.icon.setPixmap(pixmap) - @property - def checkbox(self): - return self.ui.checkbox + def set_header(self, title): + """ + Set the title of the item + + :param title: Header text. Can be html. + """ + self.ui.header.setText(title) def set_status(self, status): """ Set the status for the plugin - @param status: - @return: + :param status: An integer representing on of the + status constants defined by the class """ # reset self.ui.status.show_nothing() @@ -105,10 +136,5 @@ def set_status(self, status): raise sgtk.TankError("Invalid item status!") - def set_header(self, title): - """ - Set the title of the item - """ - self.ui.header.setText(title) diff --git a/python/tk_multi_publish2/item_status.py b/python/tk_multi_publish2/item_status.py index 90c348db..bd4ae35f 100644 --- a/python/tk_multi_publish2/item_status.py +++ b/python/tk_multi_publish2/item_status.py @@ -14,16 +14,18 @@ logger = sgtk.platform.get_logger(__name__) + class ItemStatus(QtGui.QWidget): """ - Publish Status Widget + Publish Status Widget. Small graphical widget used to display + a circular dot in different colors. """ (_MODE_OFF, _MODE_ON) = range(2) def __init__(self, parent=None): """ - Constructor + :param parent: The parent QWidget for this control """ QtGui.QWidget.__init__(self, parent) self._bundle = sgtk.platform.current_bundle() @@ -32,13 +34,14 @@ def __init__(self, parent=None): self._pen_color = None self._brush_color = None - - - ############################################################################################ - # public interface - def show_dot(self, ring_color, fill_color, dotted=False): + """ + Show a status dot using a particular color combination + :param ring_color: html color string for the ring border + :param fill_color: html color string for the fill color + :param dotted: if true then draw a dotted border + """ self._mode = self._MODE_ON self._pen_color = ring_color self._brush_color = fill_color @@ -47,15 +50,11 @@ def show_dot(self, ring_color, fill_color, dotted=False): def show_nothing(self): """ - Hide the overlay. + Turn off any dot that is being display. """ self._mode = self._MODE_OFF self.repaint() - - ############################################################################################ - # internal methods - def paintEvent(self, event): """ Render the UI. @@ -96,7 +95,6 @@ def paintEvent(self, event): r = QtCore.QRectF(0.0, 0.0, 14.0, 14.0) painter.drawEllipse(r) - finally: painter.end() diff --git a/python/tk_multi_publish2/processing/__init__.py b/python/tk_multi_publish2/processing/__init__.py index d851b8f6..1ee211fc 100644 --- a/python/tk_multi_publish2/processing/__init__.py +++ b/python/tk_multi_publish2/processing/__init__.py @@ -9,5 +9,3 @@ # not expressly granted therein are reserved by Shotgun Software Inc. from .plugin_manager import PluginManager - -from .errors import PublishError, PluginNotFoundError, PluginError, PluginValidationError, PublishFailure, ValidationFailure \ No newline at end of file diff --git a/python/tk_multi_publish2/processing/errors.py b/python/tk_multi_publish2/processing/errors.py deleted file mode 100644 index 03de6526..00000000 --- a/python/tk_multi_publish2/processing/errors.py +++ /dev/null @@ -1,53 +0,0 @@ -# Copyright (c) 2017 Shotgun Software Inc. -# -# CONFIDENTIAL AND PROPRIETARY -# -# This work is provided "AS IS" and subject to the Shotgun Pipeline Toolkit -# Source Code License included in this distribution package. See LICENSE. -# By accessing, using, copying or modifying this work you indicate your -# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights -# not expressly granted therein are reserved by Shotgun Software Inc. - -""" -All custom exceptions that this app emits are defined here. -""" - -from sgtk import TankError - -class PublishError(TankError): - """ - Base class for all publish related errors - """ - pass - -class ValidationFailure(PublishError): - """ - Indicates that validation has failed inside a hook - """ - pass - -class PublishFailure(PublishError): - """ - Indicates that validation has failed inside a hook - """ - pass - -class PluginError(PublishError): - """ - Base class for all plugin related errors - """ - pass - - -class PluginValidationError(PluginError): - """ - A plugin could not be found - """ - pass - - -class PluginNotFoundError(PluginError): - """ - A plugin could not be found - """ - pass diff --git a/python/tk_multi_publish2/processing/item.py b/python/tk_multi_publish2/processing/item.py index 3658db43..6f05886c 100644 --- a/python/tk_multi_publish2/processing/item.py +++ b/python/tk_multi_publish2/processing/item.py @@ -8,25 +8,44 @@ # agreement to the Shotgun Pipeline Toolkit Source Code License. All rights # not expressly granted therein are reserved by Shotgun Software Inc. -import sgtk import os +import sgtk import tempfile +from sgtk.platform.qt import QtCore, QtGui logger = sgtk.platform.get_logger(__name__) -from sgtk.platform.qt import QtCore, QtGui class Item(object): """ - An object representing an item that should be processed + An object representing an scene or file item that should be processed. + Items are constructed and returned by the collector hook. + + Items are organized as a tree with access to parent and children. + + In order to access the top level item in the tree, use the class method + :class:`Item.get_invisible_root_item()` """ + _invisible_root_item = None + + def __init__(self, item_type, display_type, name, parent): + """ + Items should always be generated via the :meth:`Item.create_item` factory + method and never created via the constructor. - def __init__(self, type, display_type, display_name, parent): - self._name = display_name - self._type = type + :param str item_type: Item type, typically following a hierarchical dot notation. + For example, 'file', 'file.image', 'file.quicktime' or 'maya_scene' + :param str display_type: Equivalent to the type, but for display purposes. + :param str name: The name to represent the item in a UI. + This can be a node name in a DCC or a file name. + :param Item parent: Parent item. + """ + self._name = name + self._type = item_type self._display_type = display_type self._parent = parent self._thumb_pixmap = None + self._icon_pixmap = None self._children = [] self._tasks = [] self._context = None @@ -34,77 +53,148 @@ def __init__(self, type, display_type, display_name, parent): self._parent = None self._icon_path = None self._description = None + self._created_temp_files = [] self._bundle = sgtk.platform.current_bundle() def __repr__(self): + """ + String representation + """ if self._parent: return "" % (self._parent, self._type, self._name) else: return "" % (self._type, self._name) - def update_type(self, item_type, display_type): + def __del__(self): + """ + Destructor """ - Updates the type that was set on creation. + # clean up temp files created + for temp_file in self._created_temp_files: + if os.path.exists(temp_file): + try: + os.remove(temp_file) + except Exception, e: + logger.warning( + "Could not remove temporary file '%s': %s" % (temp_file, e) + ) + else: + logger.debug("Removed temp file '%s'" % temp_file) - Useful when subclassing + @classmethod + def get_invisible_root_item(cls): """ - self._type = item_type - self._display_type = display_type + Returns the root of the tree of items. - def create_item(self, item_type, display_type, display_name): + :returns: :class:`Item` """ - Create a new item + if cls._invisible_root_item is None: + cls._invisible_root_item = Item("_root", "_root", "_root", parent=None) + return cls._invisible_root_item + + def is_root(self): + """ + Checks if the current item is the root + + :returns: True if the root item, False otherwise """ - child_item = Item(item_type, display_type, display_name, parent=self) + return self is self._invisible_root_item + + def create_item(self, item_type, display_type, name): + """ + Factory method for generating new items. + + :param str item_type: Item type, typically following a hierarchical dot notation. + For example, 'file', 'file.image', 'file.quicktime' or 'maya_scene' + :param str display_type: Equivalent to the type, but for display purposes. + :param str name: The name to represent the item in a UI. + This can be a node name in a DCC or a file name. + """ + child_item = Item(item_type, display_type, name, parent=self) self._children.append(child_item) child_item._parent = self logger.debug("Created %s" % child_item) return child_item - def set_thumbnail(self, path): - try: - self._thumb_pixmap = QtGui.QPixmap(path) - except Exception, e: - self._thumb_pixmap = None - logger.warning( - "%r: Could not load icon '%s': %s" % (self, path, e) - ) + @property + def parent(self): + """ + Parent Item object + """ + return self._parent - def set_thumbnail_pixmap(self, pixmap): - self._thumb_pixmap = pixmap + @property + def children(self): + """ + List of associated child items + """ + return self._children - def set_icon(self, path): - self._icon_path = path + @property + def properties(self): + """ + Access to a free form property bag where + item data can be stored. + """ + return self._properties - def set_description(self, description): - # update the description for this item - self._description = description + @property + def tasks(self): + """ + Tasks associated with this item. + """ + return self._tasks - def set_context(self, context): + def add_task(self, task): """ - Sets the context for the object item. + Adds a task to this item + + :param task: Task instance to be adde """ - self._context = context + self._tasks.append(task) - @property - def icon_pixmap(self): - if self._icon_path: - try: - pixmap = QtGui.QPixmap(self._icon_path) - return pixmap - except Exception, e: - logger.warning( - "%r: Could not load thumbnail '%s': %s" % (self, self._icon_path, e) - ) - elif self.parent: - return self.parent.icon_pixmap - else: - return None + def _get_name(self): + """ + The name of the item as a string. + """ + return self._name - @property - def context(self): + def _set_name(self, name): + # setter for name + self._name = name + + name = property(_get_name, _set_name) + + def _get_type(self): + """ + Item type as a string, typically following a hierarchical dot notation. + For example, 'file', 'file.image', 'file.quicktime' or 'maya_scene' + """ + return self._type + + def _set_type(self, item_type): + # setter for type + self._type = item_type + + type = property(_get_type, _set_type) + + def _get_display_type(self): + """ + A human readable type string, suitable for UI and display purposes. + """ + return self._display_type + + def _set_display_type(self, display_type): + # setter for display_type + self._display_type = display_type + + display_type = property(_get_display_type, _set_display_type) + + def _get_context(self): """ - Context inherited from parent or app + The context associated with this item. + If no context has been defined, the parent context + will be returned or None if that hasn't been defined. """ if self._context: return self._context @@ -113,24 +203,75 @@ def context(self): else: return self._bundle.context - @property - def thumbnail_pixmap(self): + def _set_context(self, context): + # setter for context property + self._context = context + + context = property(_get_context, _set_context) + + def _get_description(self): """ - Return parent thumb if nothing else found + Description of the item, description of the parent if not found + or None if no description could be found. + """ + if self._description: + return self._description + elif self.parent: + return self.parent.description + else: + return None + + def _set_description(self, description): + # setter for description property + self._description = description + + description = property(_get_description, _set_description) + + def _get_thumbnail(self): + """ + The associated thumbnail, as a QPixmap. + The thumbnail is an image to represent the item visually + such as a thumbnail of an image or a screenshot of a scene. + + If no thumbnail has been defined for this node, the parent + thumbnail is returned, or None if no thumbnail exists. """ if self._thumb_pixmap: return self._thumb_pixmap elif self.parent: - return self.parent.thumbnail_pixmap + return self.parent.thumbnail else: return None - def get_thumbnail(self): + def _set_thumbnail(self, pixmap): + self._thumb_pixmap = pixmap + + thumbnail = property(_get_thumbnail, _set_thumbnail) + + def set_thumbnail_from_path(self, path): + """ + Helper method. Parses the contents of the given file path + and tries to convert it into a QPixmap which is then + used to set the thumbnail for the item. + + :param str path: Path to a file on disk + """ + try: + self._thumb_pixmap = QtGui.QPixmap(path) + except Exception, e: + logger.warning( + "%r: Could not load '%s': %s" % (self, path, e) + ) + + def get_thumbnail_as_path(self): """ - Writes the thumbnail to disk as a temp file and - @return: + Helper method. Writes the associated thumbnail to a temp file + on disk and returns the path. This path is automatically deleted + when the object goes out of scope. + + :returns: Path to a file on disk or None if no thumbnail set """ - if self.thumbnail_pixmap is None: + if self.thumbnail is None: return None temp_path = tempfile.NamedTemporaryFile( @@ -138,46 +279,39 @@ def get_thumbnail(self): prefix="sgtk_thumb", delete=False ).name - self.thumbnail_pixmap.save(temp_path) - + self.thumbnail.save(temp_path) + self._created_temp_files.append(temp_path) return temp_path @property - def description(self): - if self._description: - return self._description + def icon(self): + """ + The associated icon, as a QPixmap. + The icon is a small square image used to represent the item visually + + If no icon has been defined for this node, the parent + icon is returned, or None if this doesn't exist + """ + if self._icon_pixmap: + return self._icon_pixmap elif self.parent: - return self.parent.description + return self.parent.icon_pixmap else: return None - @property - def children(self): - return self._children - - @property - def name(self): - return self._name - - @property - def display_type(self): - return self._display_type - - @property - def type(self): - return self._type - - @property - def parent(self): - return self._parent + def set_icon_from_path(self, path): + """ + Helper method. Parses the contents of the given file path + and tries to convert it into a QPixmap which is then + used to set the icon for the item. - @property - def properties(self): - return self._properties + :param str path: Path to a file on disk + """ + try: + self._icon_pixmap = QtGui.QPixmap(path) + except Exception, e: + logger.warning( + "%r: Could not load icon '%s': %s" % (self, path, e) + ) - @property - def tasks(self): - return self._tasks - def add_task(self, task): - self._tasks.append(task) diff --git a/python/tk_multi_publish2/processing/plugin.py b/python/tk_multi_publish2/processing/plugin.py index 394648a1..6639fec8 100644 --- a/python/tk_multi_publish2/processing/plugin.py +++ b/python/tk_multi_publish2/processing/plugin.py @@ -8,26 +8,28 @@ # agreement to the Shotgun Pipeline Toolkit Source Code License. All rights # not expressly granted therein are reserved by Shotgun Software Inc. -import re import sgtk -import collections - from sgtk.platform.qt import QtCore, QtGui - -logger = sgtk.platform.get_logger(__name__) - -from .errors import PluginValidationError, PluginNotFoundError, ValidationFailure, PublishFailure -from sgtk import TankError - - from .setting import Setting +logger = sgtk.platform.get_logger(__name__) class Plugin(object): + """ + Class that wraps around a publishing plugin hook - def __init__(self, name, path, settings, logger): + Each plugin object reflects an instance in the + app configuration. + """ + def __init__(self, name, path, settings, logger): + """ + :param name: Name to be used for this plugin instance + :param path: Path to publish plugin hook + :param settings: Dictionary of plugin-specific settings + :param logger: a logger object that will be used by the hook + """ # all plugins need a hook and a name self._name = name self._path = path @@ -35,27 +37,53 @@ def __init__(self, name, path, settings, logger): self._bundle = sgtk.platform.current_bundle() - # init the plugin + # create an instance of the hook self._plugin = self._bundle.create_hook_instance(self._path) self._configured_settings = {} self._required_runtime_settings = {} self._tasks = [] - self._logger = logger - self._settings = {} + + # kick things off self._validate_and_resolve_config() + self._icon_pixmap = self._load_plugin_icon() + def __repr__(self): + """ + String representation + """ + return "" % self._path + def _load_plugin_icon(self): + """ + Loads the icon defined by the hook. + :returns: QPixmap or None if not found + """ + # load plugin icon + pixmap = None + try: + icon_path = self._plugin.icon + try: + pixmap = QtGui.QPixmap(icon_path) + except Exception, e: + logger.warning( + "%r: Could not load icon '%s': %s" % (self, icon_path, e) + ) + except AttributeError: + # plugin does not have an icon + pass - def __repr__(self): - return "" % self._path + return pixmap def _validate_and_resolve_config(self): """ - Validate all values. Resolve links + Init helper method. + + Validates plugin settings and creates Setting objects + that can be accessed from the settings property. """ try: settings_defs = self._plugin.settings @@ -79,53 +107,77 @@ def _validate_and_resolve_config(self): if settings_def_name in self._raw_config_settings: # this setting was provided by the config # todo - validate - setting.set_value(self._raw_config_settings[settings_def_name]) + setting.value = self._raw_config_settings[settings_def_name] self._settings[settings_def_name] = setting @property def name(self): - # name as defined in the env config + """ + The name of this plugin instance + """ return self._name @property def tasks(self): + """ + Tasks associated with this publish plugin. + """ return self._tasks + def add_task(self, task): + """ + Adds a task to this publish plugin. + + :param task: Task instance to add. + """ + self._tasks.append(task) + @property - def title(self): + def plugin_name(self): + """ + The name of the publish plugin. + Always a string. + """ + value = None try: - return self._plugin.title + value = self._plugin.name except AttributeError: - return "Untitled Integration." + pass + + return value or "Untitled Integration." @property def description(self): + """ + The decscription of the publish plugin. + Always a string. + """ + value = None try: - return self._plugin.description_html + value = self._plugin.description except AttributeError: - return "No detailed description provided." + pass + + return value or "No detailed description provided." @property - def subscriptions(self): + def item_filters(self): + """ + The item filters defined by this plugin + or [] if none have been defined. + """ try: - return self._plugin.subscriptions + return self._plugin.item_filters except AttributeError: return [] @property - def icon_pixmap(self): - try: - icon_path = self._plugin.icon - try: - pixmap = QtGui.QPixmap(icon_path) - return pixmap - except Exception, e: - logger.warning( - "%r: Could not load icon '%s': %s" % (self, icon_path, e) - ) - except AttributeError: - return None + def icon(self): + """ + The associated icon, as a pixmap. + """ + return self._icon_pixmap @property def settings(self): @@ -134,10 +186,13 @@ def settings(self): """ return self._settings - def add_task(self, task): - self._tasks.append(task) - def run_accept(self, item): + """ + Executes the hook accept method for the given item + + :param item: Item to analyze + :returns: dictionary with boolean keys 'accepted' and 'required' + """ try: return self._plugin.accept(self._logger, self.settings, item) except Exception, e: @@ -148,6 +203,13 @@ def run_accept(self, item): QtCore.QCoreApplication.processEvents() def run_validate(self, settings, item): + """ + Executes the validation logic for this plugin instance. + + :param settings: Dictionary of settings + :param item: Item to analyze + :return: True if validation passed, False otherwise. + """ try: return self._plugin.validate(self._logger, settings, item) except Exception, e: @@ -157,8 +219,13 @@ def run_validate(self, settings, item): # give qt a chance to do stuff QtCore.QCoreApplication.processEvents() - def run_publish(self, settings, item): + """ + Executes the publish logic for this plugin instance. + + :param settings: Dictionary of settings + :param item: Item to analyze + """ try: self._plugin.publish(self._logger, settings, item) except Exception, e: @@ -168,8 +235,13 @@ def run_publish(self, settings, item): # give qt a chance to do stuff QtCore.QCoreApplication.processEvents() - def run_finalize(self, settings, item): + """ + Executes the finalize logic for this plugin instance. + + :param settings: Dictionary of settings + :param item: Item to analyze + """ try: self._plugin.finalize(self._logger, settings, item) except Exception, e: diff --git a/python/tk_multi_publish2/processing/plugin_manager.py b/python/tk_multi_publish2/processing/plugin_manager.py index 46be2712..b2309642 100644 --- a/python/tk_multi_publish2/processing/plugin_manager.py +++ b/python/tk_multi_publish2/processing/plugin_manager.py @@ -20,16 +20,15 @@ class PluginManager(object): """ - Handles hook execution + Manager which handles hook initialization and execution. """ def __init__(self, publish_logger): """ - Constructor - - :param parent: The parent QWidget for this control + :param publish_logger: A logger object that the + various hooks can send logging information to. """ - logger.debug("plugin manager waking up") + logger.debug("Plugin manager waking up") self._bundle = sgtk.platform.current_bundle() @@ -40,6 +39,7 @@ def __init__(self, publish_logger): plugin_defs = self._bundle.get_setting("publish_plugins") + # create plugin objects for plugin_def in plugin_defs: logger.debug("Find config chunk %s" % plugin_def) @@ -47,65 +47,59 @@ def __init__(self, publish_logger): hook_path = plugin_def["hook"] settings = plugin_def["settings"] - # maintain a ordered list plugin = Plugin(plugin_instance_name, hook_path, settings, self._logger) - logger.debug("Created %s" % plugin) self._plugins.append(plugin) + logger.debug("Created %s" % plugin) - self._root_item = Item("_root", "_root", "_root", parent=None) + # create an item root + self._root_item = Item.get_invisible_root_item() + + # initalize tasks self._tasks = [] - # do the current scene + # process the current scene self._collect(collect_current_scene=True) - def add_external_files(self, paths): - logger.debug("Adding external files '%s'" % paths) - # and update the data model - self._collect(collect_current_scene=False, paths=paths) - @property def top_level_items(self): + """ + Returns a list of the items which reside on the top level + of the tree, e.g. all the children of the invisible root item. + + :returns: List if :class:`Item` instances + """ return self._root_item.children @property def plugins(self): - return self._plugins - - def _get_matching_items(self, subscriptions, all_items): - """ - Given a list of subscriptions from a plugin, - yield a series of matching items. Items are - randomly ordered. """ - for subscription in subscriptions: - logger.debug("Checking matches for subscription %s" % subscription) - # "maya.*" - for item in all_items: - if fnmatch.fnmatch(item.type, subscription): - yield item + Returns a list of the plugin instances loaded from the configuration + :returns: List of :class:`Plugin` instances. + """ + return self._plugins - def _create_task(self, plugin, item): - task = Task(plugin, item) - plugin.add_task(task) - item.add_task(task) - logger.debug("Created %s" % task) - return task + def add_external_files(self, paths): + """ + Runs the collector for the given set of paths - def _get_items(self, parent): - items = [] - for child in parent.children: - items.append(child) - items.extend(self._get_items(child)) - return items + :param str paths: List of full file path + """ + logger.debug("Adding external files '%s'" % paths) + self._collect(collect_current_scene=False, paths=paths) def _collect(self, collect_current_scene, paths=None): """ Runs the collector and generates fresh items. - """ + :param bool collect_current_scene: Boolean to indicate if collection should + be performed for the current scene, e.g. if the collector hook's + process_current_scene() method should be executed. + :param paths: List of paths for which the collector hook's method + process_file() should be executed for + """ # get existing items - all_items_before = self._get_items(self._root_item) + all_items_before = self._get_item_tree_as_list() # pass 1 - collect stuff from the scene and other places logger.debug("Executing collector") @@ -127,27 +121,58 @@ def _collect(self, collect_current_scene, paths=None): ) # get all items after scan - all_items_after = self._get_items(self._root_item) + all_items_after = self._get_item_tree_as_list() # get list of new things all_new_items = list(set(all_items_after) - set(all_items_before)) - # now we have a series of items from the scene, pass it back to the plugins to see which are interesting + # now we have a series of items from the scene, visit our plugins + # to see if there is interest logger.debug("Visiting all plugins and offering items") for plugin in self._plugins: - for item in self._get_matching_items(plugin.subscriptions, all_new_items): + for item in self._get_matching_items(plugin.item_filters, all_new_items): logger.debug("seeing if %s is interested in %s" % (plugin, item)) accept_data = plugin.run_accept(item) if accept_data.get("accepted"): # this item was accepted by the plugin! # create a task - task = self._create_task(plugin, item) is_required = accept_data.get("required") is True is_enabled = accept_data.get("enabled") is True - task.set_plugin_defaults(is_required, is_enabled) + task = Task.create_task(plugin, item, is_required, is_enabled) self._tasks.append(task) - # TODO: need to do a cull to remove any items in the tree which do not have tasks + # TODO: need to do a cull to remove any items in the tree which do not have tasks? + + def _get_matching_items(self, item_filters, all_items): + """ + Given a list of item filters from a plugin, + yield a series of matching items. Items are + randomly ordered. + + :param item_filters: List of item filters to glob against. + :param all_items: Items to match against. + """ + for item_filter in item_filters: + logger.debug("Checking matches for item filter %s" % item_filter) + # "maya.*" + for item in all_items: + if fnmatch.fnmatch(item.type, item_filter): + yield item + + def _get_item_tree_as_list(self): + """ + Returns the item tree as a flat list. + :returns: List if item objects + """ + def _get_subtree_as_list_r(parent): + items = [] + for child in parent.children: + items.append(child) + items.extend(_get_subtree_as_list_r(child)) + return items + + parent = self._root_item + return _get_subtree_as_list_r(parent) diff --git a/python/tk_multi_publish2/processing/setting.py b/python/tk_multi_publish2/processing/setting.py index 64dd9609..f5492836 100644 --- a/python/tk_multi_publish2/processing/setting.py +++ b/python/tk_multi_publish2/processing/setting.py @@ -18,39 +18,64 @@ class Setting(object): A setting for a plugin or item """ - def __init__(self, setting_name, data_type, default_value, description=None): + """ + :param setting_name: The name of the setting + :param data_type: The data type of the setting + :param default_value: The setting's default value + :param description: Description of the setting + """ self._name = setting_name self._type = data_type self._default_value = default_value self._value = default_value self._description = description or "" - def set_value(self, value): - self._value = value - @property def name(self): + """ + The setting name + """ return self._name - @property - def value(self): + def _get_value(self): + """ + The current value of the setting + """ return self._value + def _set_value(self, value): + # setter for value + self._value = value + + value = property(_get_value, _set_value) + @property def string_value(self): + """ + The setting value, as a string + """ return str(self._value) @property def description(self): + """ + The description of the setting + """ return self._description @property def default_value(self): + """ + The default value of the setting. + """ return self._default_value @property def type(self): + """ + The data type of the setting. + """ return self._type diff --git a/python/tk_multi_publish2/processing/task.py b/python/tk_multi_publish2/processing/task.py index 4eabba76..fde4db74 100644 --- a/python/tk_multi_publish2/processing/task.py +++ b/python/tk_multi_publish2/processing/task.py @@ -12,64 +12,104 @@ logger = sgtk.platform.get_logger(__name__) -from .setting import Setting class Task(object): """ - A plugin instance or the particular action that - should be carried out by a plugin on an item. + A task is a particular unit of work which can to be carried + by the publishing process. A task can be thought of as a + 'plugin instance', e.g a particular publishing plugin operating + on a particular collector item. """ - def __init__(self, plugin, item): + def __init__(self, plugin, item, required, enabled): + """ + :param plugin: The plugin instance associated with this task + :param item: The collector item associated with this task + :param bool required: Indicates that the task is required + :param bool enabled: Indicates that the task is enabled + """ self._plugin = plugin self._item = item self._settings = [] - self._enabled = False - self._required = False - + self._enabled = enabled + self._required = required def __repr__(self): + """ + String representation + """ return "" % (self._plugin, self._item) - def set_plugin_defaults(self, required, enabled): - self._required = required - self._enabled = enabled + @classmethod + def create_task(cls, plugin, item, is_required, is_enabled): + """ + Factory method for new tasks. + + :param plugin: Plugin instance + :param item: Item object + :param is_required: bool to indicate if this options is required + :param is_enabled: bool to indicate if node is enabled + :return: Task instance + """ + task = Task(plugin, item, is_required, is_enabled) + plugin.add_task(task) + item.add_task(task) + logger.debug("Created %s" % task) + return task @property def item(self): + """ + The item associated with this Task + """ return self._item @property def plugin(self): + """ + The plugin associated with this Task + """ return self._plugin - @property - def settings(self): - return self._settings - @property def required(self): + """ + Returns if this Task is required by the publishing + """ return self._required @property def enabled(self): + """ + Returns if this + @return: + """ return self._enabled @property def settings(self): - - # TODO - writable per task settings pleeeease! + """ + Dictionary of settings associated with this Task + """ + # TODO - make settings configurable per task return self.plugin.settings def validate(self): + """ + Validate this Task + :returns: True if validation succeeded, False otherwise. + """ return self.plugin.run_validate(self.settings, self.item) - def publish(self): - + """ + Publish this Task + """ self.plugin.run_publish(self.settings, self.item) def finalize(self): - + """ + Finalize this Task + """ self.plugin.run_finalize(self.settings, self.item) diff --git a/python/tk_multi_publish2/publish_logging.py b/python/tk_multi_publish2/publish_logging.py index 5b715068..4ac3015f 100644 --- a/python/tk_multi_publish2/publish_logging.py +++ b/python/tk_multi_publish2/publish_logging.py @@ -19,28 +19,33 @@ class PublishLogHandler(logging.Handler): """ - Publish Log handler + Publish Log handler that links up a handler to a + qt tree for display. """ def __init__(self, tree_widget): """ - :param engine: Engine to which log messages should be forwarded. - :type engine: :class:`Engine` + :param tree_widget: QTreeWidget to use for logging """ # avoiding super in order to be py25-compatible logging.Handler.__init__(self) + self._tree_widget = tree_widget - self._logging_parent_item = None # none means root + self._logging_parent_item = None # none means root self._debug_brush = QtGui.QBrush(QtGui.QColor("#508937")) # green self._warning_brush = QtGui.QBrush(QtGui.QColor("#FFD786")) # orange self._error_brush = QtGui.QBrush(QtGui.QColor("#FF383F")) # red - - def push(self, text, icon): + """ + Push a child node to the tree. New log records will + be added as children to this child node. + :param text: Caption for the entry + :param icon: QIcon for the entry + """ item = QtGui.QTreeWidgetItem() item.setText(0, text) if self._logging_parent_item is None: @@ -55,7 +60,11 @@ def push(self, text, icon): self._logging_parent_item = item def pop(self): - + """ + Pops any active child section. + If no child sections exist, this operation will not + have any effect. + """ # top level items return None if self._logging_parent_item: self._logging_parent_item = self._logging_parent_item.parent() @@ -95,9 +104,15 @@ def emit(self, record): class PublishLogWrapper(object): + """ + Convenience object that wraps around a logger and a handler + that can be used for publishing. + """ def __init__(self, tree_widget): - + """ + :param tree_widget: QTreeWidget to use for logging + """ # set up a logger full_log_path = "%s.publish" % logger.name @@ -119,12 +134,27 @@ def __init__(self, tree_widget): @property def logger(self): + """ + The associated logger + """ return self._logger def push(self, text, icon=None): + """ + Push a child node to the tree. New log records will + be added as children to this child node. + + :param text: Caption for the entry + :param icon: QIcon for the entry + """ self._handler.push(text, icon) def pop(self): + """ + Pops any active child section. + If no child sections exist, this operation will not + have any effect. + """ self._handler.pop() diff --git a/python/tk_multi_publish2/settings_widget.py b/python/tk_multi_publish2/settings_widget.py index 9d786d9a..afa14ba5 100644 --- a/python/tk_multi_publish2/settings_widget.py +++ b/python/tk_multi_publish2/settings_widget.py @@ -1,4 +1,4 @@ -# Copyright (c) 2015 Shotgun Software Inc. +# Copyright (c) 2017 Shotgun Software Inc # # CONFIDENTIAL AND PROPRIETARY # @@ -18,6 +18,7 @@ from .ui.settings_widget import Ui_SettingsWidget + class FieldNameLabel(QtGui.QLabel): """ Wrapper class so that we can style based on class @@ -34,7 +35,7 @@ class FieldValueLabel(QtGui.QLabel): class SettingsWidget(QtGui.QWidget): """ - Widget that shows shotgun data in a name-value pair, top down fasion: + Widget that shows shotgun data in a name-value pair, top down fashion: Status: In Progress Description: Foo Bar @@ -84,13 +85,11 @@ def clear(self): # make the window visible again and trigger a redraw self.setVisible(True) - - def set_data(self, settings): """ Clear any existing data in the widget and populate it with new data - :param sg_data: Shotgun data dictionary + :param settings: Shotgun data dictionary """ # first clear existing stuff @@ -140,11 +139,8 @@ def set_static_data(self, settings): """ Clear any existing data in the widget and populate it with new data - - - + :param settings: Shotgun data dictionary """ - # first clear existing stuff self.clear() diff --git a/python/tk_multi_publish2/thumbnail.py b/python/tk_multi_publish2/thumbnail.py index 05c478e4..9c2c32eb 100644 --- a/python/tk_multi_publish2/thumbnail.py +++ b/python/tk_multi_publish2/thumbnail.py @@ -8,13 +8,13 @@ # agreement to the Shotgun Pipeline Toolkit Source Code License. All rights # not expressly granted therein are reserved by Shotgun Software Inc. - import sgtk from sgtk.platform.qt import QtCore, QtGui +screen_grab = sgtk.platform.import_framework("tk-framework-qtwidgets", "screen_grab") + logger = sgtk.platform.get_logger(__name__) -screen_grab = sgtk.platform.import_framework("tk-framework-qtwidgets", "screen_grab") class Thumbnail(QtGui.QLabel): """ @@ -23,13 +23,13 @@ class Thumbnail(QtGui.QLabel): using screen capture and other methods. """ + # emitted when screen is captured + # passes the QPixmap as a parameter screen_grabbed = QtCore.Signal(object) def __init__(self, parent=None): """ - Constructor - - :param parent: The parent QWidget for this control + :param parent: The parent QWidget for this control """ QtGui.QLabel.__init__(self, parent) self._thumbnail = None @@ -41,7 +41,9 @@ def __init__(self, parent=None): def set_thumbnail(self, pixmap): """ - Set up the widget as a thumb + Set pixmap to be displayed + + :param pixmap: QPixmap to show or None in order to show default one. """ if pixmap is None: self._set_screenshot_pixmap(self._no_thumb_pixmap) @@ -54,7 +56,6 @@ def mousePressEvent(self, event): """ QtGui.QLabel.mousePressEvent(self, event) - # no pixmap exists - screengrab mode self._bundle.log_debug("Prompting for screenshot...") self.window().hide() @@ -63,17 +64,12 @@ def mousePressEvent(self, event): finally: self.window().show() - # It's possible that there's custom screencapture logic - # happening and we won't get a pixmap back right away. - # A good example of this is something like RV, which - # will handle screenshots itself and provide back the - # image asynchronously. if pixmap: self._bundle.log_debug( "Got screenshot %sx%s" % (pixmap.width(), pixmap.height()) ) - self.screen_grabbed.emit(pixmap) self._set_screenshot_pixmap(pixmap) + self.screen_grabbed.emit(pixmap) def _set_screenshot_pixmap(self, pixmap): """ @@ -92,7 +88,7 @@ def __format_thumbnail(self, pixmap_obj): Format a given pixmap to 16:9 ratio :param pixmap_obj: input screenshot - :returns: 160x90 pixmap + :returns: 320x180 pixmap (16:9 ratio) """ CANVAS_WIDTH = 320 CANVAS_HEIGHT = 180 diff --git a/python/tk_multi_publish2/tree_item.py b/python/tk_multi_publish2/tree_item.py index 6a98df5a..5ada779f 100644 --- a/python/tk_multi_publish2/tree_item.py +++ b/python/tk_multi_publish2/tree_item.py @@ -11,19 +11,35 @@ import sgtk from sgtk.platform.qt import QtCore, QtGui - from .item import Item logger = sgtk.platform.get_logger(__name__) + class PublishTreeWidget(QtGui.QTreeWidgetItem): """ - Base class for all tree widgets + Base class for all tree widgets. + + Each of these QTreeWidgetItem objects encapsulate an + Item widget which is used to display the actual UI + for the tree node. """ + def __init__(self, parent): + """ + :param parent: The parent QWidget for this control + """ super(PublishTreeWidget, self).__init__(parent) + # create an item widget and associate it with this QTreeWidgetItem + tree_widget = self.treeWidget() + self._item_widget = Item(tree_widget) + tree_widget.setItemWidget(self, 0, self._item_widget) + def begin_process(self): + """ + Reset progress state + """ if self.enabled: # enabled nodes get a dotted ring self._item_widget.set_status(self._item_widget.PROCESSING) @@ -40,66 +56,86 @@ def _set_status_upwards(self, status): self.parent()._set_status_upwards(status) def validate(self): + """ + Perform validation + """ self._item_widget.set_status(self._item_widget.VALIDATION_COMPLETE) return True def publish(self): + """ + Perform publish + """ self._item_widget.set_status(self._item_widget.PUBLISH_COMPLETE) return True def finalize(self): + """ + Perform finalize + """ self._item_widget.set_status(self._item_widget.FINALIZE_COMPLETE) return True @property def checkbox(self): + """ + The checkbox associated with this item + """ return self._item_widget.checkbox @property def icon(self): - raise NotImplementedError + """ + The icon pixmap associated with this item + """ + return self._item_widget.icon @property def enabled(self): + """ + Returns true if the checkbox is enabled + """ return self._item_widget.checkbox.isChecked() class PublishTreeWidgetTask(PublishTreeWidget): """ - Tree item for a task + Tree item for a publish task """ def __init__(self, task, parent): """ + :param task: Task instance + :param parent: The parent QWidget for this control """ super(PublishTreeWidgetTask, self).__init__(parent) + self._task = task - tree_widget = self.treeWidget() - self._item_widget = Item(tree_widget) self._item_widget.set_header(self._task.plugin.name) - self._item_widget.set_icon(self._task.plugin.icon_pixmap) + self._item_widget.set_icon(self._task.plugin.icon) self._item_widget.checkbox.setChecked(self._task.enabled) + if self._task.required: self._item_widget.checkbox.setEnabled(False) else: self._item_widget.checkbox.setEnabled(True) - tree_widget.setItemWidget(self, 0, self._item_widget) def __str__(self): return self._task.plugin.name @property def task(self): + """ + Associated task instance + """ return self._task - @property - def icon(self): - # qicon for the node - return QtGui.QIcon(self._task.plugin.icon_pixmap) - def validate(self): + """ + Perform validation + """ try: status = self._task.validate() except Exception, e: @@ -113,6 +149,9 @@ def validate(self): return status def publish(self): + """ + Perform publish + """ try: self._task.publish() except Exception, e: @@ -123,6 +162,9 @@ def publish(self): return True def finalize(self): + """ + Perform finalize + """ try: self._task.finalize() except Exception, e: @@ -133,42 +175,32 @@ def finalize(self): return True - class PublishTreeWidgetPlugin(PublishTreeWidget): """ Tree item for a plugin """ - def __init__(self, plugin, parent): """ + :param parent: The parent QWidget for this control """ super(PublishTreeWidgetPlugin, self).__init__(parent) self._plugin = plugin - tree_widget = self.treeWidget() - - self._item_widget = Item(tree_widget) self._item_widget.set_header(self._plugin.name) - self._item_widget.set_icon(self._plugin.icon_pixmap) + self._item_widget.set_icon(self._plugin.icon) - tree_widget = self.treeWidget() - tree_widget.setItemWidget(self, 0, self._item_widget) def __str__(self): return self._plugin.name @property def plugin(self): + """ + associated plugin instance + """ return self._plugin - @property - def icon(self): - # qicon for the node - return QtGui.QIcon(self._plugin.icon_pixmap) - - - class PublishTreeWidgetItem(PublishTreeWidget): """ @@ -176,26 +208,22 @@ class PublishTreeWidgetItem(PublishTreeWidget): """ def __init__(self, item, parent): + """ + :param item: + :param parent: The parent QWidget for this control + """ super(PublishTreeWidgetItem, self).__init__(parent) self._item = item - - tree_widget = self.treeWidget() - - self._item_widget = Item(tree_widget) self._item_widget.set_header("%s
%s" % (self._item.name, self._item.display_type)) - self._item_widget.set_icon(self._item.icon_pixmap) - - tree_widget.setItemWidget(self, 0, self._item_widget) - + self._item_widget.set_icon(self._item.icon) def __str__(self): return "%s %s" % (self._item.display_type, self._item.name) @property def item(self): + """ + Associated item instance + """ return self._item - @property - def icon(self): - # qicon for the node - return QtGui.QIcon(self._item.icon_pixmap) diff --git a/resources/publish_menu_icon.png b/resources/publish_menu_icon.png deleted file mode 100644 index a5d5ccc4..00000000 Binary files a/resources/publish_menu_icon.png and /dev/null differ diff --git a/resources/resources.graffle b/resources/resources.graffle index a6fce750..ab5f503f 100644 Binary files a/resources/resources.graffle and b/resources/resources.graffle differ diff --git a/style.qss b/style.qss index 26c9bda8..2ce15d11 100644 --- a/style.qss +++ b/style.qss @@ -1,5 +1,5 @@ /* -Copyright (c) 2015 Shotgun Software Inc. +Copyright (c) 2017 Shotgun Software Inc CONFIDENTIAL AND PROPRIETARY @@ -10,9 +10,35 @@ agreement to the Shotgun Pipeline Toolkit Source Code License. All rights not expressly granted therein are reserved by Shotgun Software Inc. */ +/* --------------------------------------------------------------------- */ +/* Global styling */ +/* --------------------------------------------------------------------- */ +/* Get rid of borders for main Listing Views */ +QListView, QTableView, QScrollArea, QFrame { + border: none; +} + +/* Use open sans font across the app if core supports it */ +QWidget { + font-family: Open Sans; + font-style: Regular; +} +/* need this to kick QT into 'style mode' for labels otherwise things are not formatted correctly */ +QLabel { + margin-top: 1px; +} + +/* general styling of QTreeView */ +QTreeView::branch { + background: none; +} + +/* --------------------------------------------------------------------- */ +/* Right hand side tab control */ +/* --------------------------------------------------------------------- */ QTabWidget::pane { border-top: 1px solid rgb(255, 255, 255, 20); @@ -24,8 +50,8 @@ QTabWidget::pane { QTabBar::tab { font-size: 12px; font-weight: 100; - padding-left: 12px; - padding-right: 12px; + padding-left: 18px; + padding-right: 18px; padding-bottom: 8px; padding-top: 0px; border-bottom: 2px solid rgb(255, 255, 255, 5); @@ -42,74 +68,82 @@ QTabBar::tab:selected { -/* ----------------------- GLOBAL STYLE SETTINGS ----------------------- */ +/* --------------------------------------------------------------------- */ +/* Right hand side details area */ +/* --------------------------------------------------------------------- */ -/* Get rid of borders for main Listing Views */ -QListView, QTableView, QScrollArea, QFrame { - border: none; -} - - -/* Use open sans font across the app if core supports it */ -QWidget { - font-family: Open Sans; - font-style: Regular; -} +/* fonts for various headers */ #plugin_name, #task_name, #item_name, #summary_header { font-size: 22px; font-weight: 100; } -/* the two labels above the tree on the left hand side */ -#items_tree_label, #reversed_items_tree_label { - font-size: 14px; - font-weight: 100; - padding-bottom: 10px; -} - - -/* subheading under the item name in the details */ -#item_type { +#item_settings_label, #task_settings_label, #plugin_settings_label { font-size: 16px; font-weight: 100; + padding-top: 18px; } -#please_select_an_item { +/* subheading under the item name in the details */ +#item_type, #please_select_an_item { font-size: 16px; font-weight: 100; } -/* need this to kick QT into 'style mode' for labels otherwise things are not formatted correctly */ -QLabel { - margin-top: 1px; +/* divider lines in the details UI */ +#summary_divider, #task_divider, #item_divider, #plugin_divider { + border-top: 1px solid rgba(255, 255, 255, 20); } -DropAreaFrame[dragging="true"] { - border: 1px solid {{SG_HIGHLIGHT_COLOR}}; -} +/* --------------------------------------------------------------------- */ +/* Settings widgets, used in details section */ +/* --------------------------------------------------------------------- */ + +FieldNameLabel { + padding: 8px; + /* there seems to be an odd bug where border-bottom isn't evaluated + unless there is a border top, so add a transparent line... */ + border-top: 1px dotted rgba(0,0,0,0%); + color: rgba(200, 200, 200, 40%); + border-bottom: 1px dotted rgba(200, 200, 200, 18%); +} -Item { padding-left: 4px; padding-top: 2px; padding-bottom: 4px; } +FieldValueLabel { + padding: 8px; + /* there seems to be an odd bug where border-bottom isn't evaluated + unless there is a border top, so add a transparent line... */ + border-top: 1px dotted rgba(0,0,0,0%); + border-bottom: 1px dotted rgba(200, 200, 200, 18%); +} -QTreeView::branch { background: none; } -#summary_divider, #task_divider, #item_divider, #plugin_divider { - border-top: 1px solid rgba(255, 255, 255, 20); +/* --------------------------------------------------------------------- */ +/* Left hand side tree view */ +/* --------------------------------------------------------------------- */ +/* left hand side frame when someone drag something in */ +DropAreaFrame[dragging="true"] { + border: 1px solid {{SG_HIGHLIGHT_COLOR}}; } - -#log_tree { - font-size: 12px; +/* the two labels above the tree on the left hand side */ +#items_tree_label, #reversed_items_tree_label { + font-size: 14px; font-weight: 100; + padding-bottom: 10px; } -#log_tree::item { - margin: 4px; + +/* Tree view items */ +Item { + padding-left: 4px; + padding-top: 2px; + padding-bottom: 4px; } #items_tree, #reversed_items_tree { @@ -118,33 +152,18 @@ QTreeView::branch { background: none; } -#item_settings_label, #task_settings_label, #plugin_settings_label { - font-size: 16px; +/* --------------------------------------------------------------------- */ +/* Right hand side logging tree */ +/* --------------------------------------------------------------------- */ + +#log_tree { + font-size: 12px; font-weight: 100; - padding-top: 18px; } +#log_tree::item { + margin: 4px; +} -/****************************************************************/ -/* Info tab showing all shotgun fields */ - - -FieldNameLabel { - - padding: 8px; - /* there seems to be an odd bug where border-bottom isn't evaluated - unless there is a border top, so add a transparent line... */ - border-top: 1px dotted rgba(0,0,0,0%); - color: rgba(200, 200, 200, 40%); - border-bottom: 1px dotted rgba(200, 200, 200, 18%); -} - -FieldValueLabel { - padding: 8px; - /* there seems to be an odd bug where border-bottom isn't evaluated - unless there is a border top, so add a transparent line... */ - border-top: 1px dotted rgba(0,0,0,0%); - border-bottom: 1px dotted rgba(200, 200, 200, 18%); -}