diff --git a/hooks/scene_operation_tk-3dsmax.py b/hooks/scene_operation_tk-3dsmax.py index a3de0b8..4f6803b 100644 --- a/hooks/scene_operation_tk-3dsmax.py +++ b/hooks/scene_operation_tk-3dsmax.py @@ -12,62 +12,72 @@ class SceneOperation(Hook): """ - Hook called to perform an operation with the + Hook called to perform an operation with the current scene """ - + def execute(self, operation, file_path, context, **kwargs): """ Main hook entry point - + :operation: String Scene operation to perform - + :file_path: String File path to use if the operation requires it (e.g. open) - + :context: Context The context the file operation is being performed in. - + :returns: Depends on operation: 'current_path' - Return the current scene file path as a String - 'reset' - True if scene was reset to an empty + 'reset' - True if scene was reset to an empty state, otherwise False all others - None """ - + if operation == "current_path": - # return the current scene path + """ + Returns the current scene path + """ if not mxs.maxFileName: return "" return os.path.join(mxs.maxFilePath, mxs.maxFileName) + elif operation == "open": # open the specified scene mxs.loadMaxFile(file_path) + return True + elif operation == "save": # save the current scene: file_path = os.path.join(mxs.maxFilePath, mxs.maxFileName) mxs.saveMaxFile(file_path) + return True + elif operation == "save_as": # save the scene as file_path: mxs.saveMaxFile(file_path) + return True + + elif operation == "scene_modified": + """ + Returns True if the current scene is dirty + """ + return mxs.getSaveRequired() + elif operation == "reset": """ - Reset the scene to an empty state + Reset the scene to an empty state. + Does not check if saving is needed. """ - # use the standard Max mechanism to check - # for and save the file if required: - if not mxs.checkForSave(): - return False - # now reset the scene: mxs.resetMAXFile(mxs.pyhelper.namify("noPrompt")) - + return True + else: raise TankError("Don't know how to perform scene operation '%s'" % operation) - - diff --git a/hooks/scene_operation_tk-maya.py b/hooks/scene_operation_tk-maya.py index 616c767..4f73321 100644 --- a/hooks/scene_operation_tk-maya.py +++ b/hooks/scene_operation_tk-maya.py @@ -13,47 +13,52 @@ class SceneOperation(Hook): """ - Hook called to perform an operation with the + Hook called to perform an operation with the current scene """ - + def execute(self, operation, file_path, context, **kwargs): """ Main hook entry point - + :operation: String Scene operation to perform - + :file_path: String File path to use if the operation requires it (e.g. open) - + :context: Context The context the file operation is being performed in. - + :returns: Depends on operation: 'current_path' - Return the current scene file path as a String - 'reset' - True if scene was reset to an empty + 'reset' - True if scene was reset to an empty state, otherwise False all others - None """ if operation == "current_path": # return the current scene path return cmds.file(query=True, sceneName=True) + elif operation == "open": - # do new scene as Maya doesn't like opening - # the scene it currently has open! - cmds.file(new=True, force=True) + # do new scene as Maya doesn't like opening + # the scene it currently has open! + cmds.file(new=True, force=True) cmds.file(file_path, open=True) + return True + elif operation == "save": # save the current scene: cmds.file(save=True) + return True + elif operation == "save_as": # first rename the scene as file_path: cmds.file(rename=file_path) - + # Maya can choose the wrong file type so # we should set it here explicitely based # on the extension @@ -62,38 +67,29 @@ def execute(self, operation, file_path, context, **kwargs): maya_file_type = "mayaAscii" elif file_path.lower().endswith(".mb"): maya_file_type = "mayaBinary" - + # save the scene: if maya_file_type: cmds.file(save=True, force=True, type=maya_file_type) else: cmds.file(save=True, force=True) - + return True + + elif operation == "scene_modified": + """ + Returns True if the current scene is dirty + """ + return cmds.file(query=True, modified=True) + elif operation == "reset": """ - Reset the scene to an empty state + Reset the scene to an empty state. + Does not check if saving is needed. """ - while cmds.file(query=True, modified=True): - # changes have been made to the scene - res = QtGui.QMessageBox.question(None, - "Save your scene?", - "Your scene has unsaved changes. Save before proceeding?", - QtGui.QMessageBox.Yes|QtGui.QMessageBox.No|QtGui.QMessageBox.Cancel) - - if res == QtGui.QMessageBox.Cancel: - return False - elif res == QtGui.QMessageBox.No: - break - else: - scene_name = cmds.file(query=True, sn=True) - if not scene_name: - cmds.SaveSceneAs() - else: - cmds.file(save=True) - - # do new file: + # do new file: cmds.file(newFile=True, force=True) return True + else: raise TankError("Don't know how to perform scene operation '%s'" % operation) diff --git a/hooks/scene_operation_tk-nuke.py b/hooks/scene_operation_tk-nuke.py index bc5f893..b5eab6c 100644 --- a/hooks/scene_operation_tk-nuke.py +++ b/hooks/scene_operation_tk-nuke.py @@ -13,95 +13,94 @@ class SceneOperation(Hook): """ - Hook called to perform an operation with the + Hook called to perform an operation with the current scene """ - + def execute(self, operation, file_path, context, **kwargs): """ Main hook entry point - + :operation: String Scene operation to perform - + :file_path: String File path to use if the operation requires it (e.g. open) - + :context: Context The context the file operation is being performed in. - + :returns: Depends on operation: 'current_path' - Return the current scene file path as a String - 'reset' - True if scene was reset to an empty + 'reset' - True if scene was reset to an empty state, otherwise False all others - None """ - + if file_path: file_path = file_path.replace("/", os.path.sep) - + if operation == "current_path": # return the current script path - return nuke.root().name().replace("/", os.path.sep) - + scene_name = nuke.root().name() + if scene_name == "Root": + return "" + return scene_name.replace("/", os.path.sep) + elif operation == "open": # open the specified script nuke.scriptOpen(file_path) - + # reset any write node render paths: if self._reset_write_node_render_paths(): # something changed so make sure to save the script again: nuke.scriptSave() - + return True + elif operation == "save": # save the current script: nuke.scriptSave() - + elif operation == "save_as": old_path = nuke.root()["name"].value() try: # rename script: nuke.root()["name"].setValue(file_path) - + # reset all write nodes: self._reset_write_node_render_paths() - + # save script: - nuke.scriptSaveAs(file_path, -1) + nuke.scriptSaveAs(file_path, -1) except Exception, e: # something went wrong so reset to old path: nuke.root()["name"].setValue(old_path) raise TankError("Failed to save scene %s", e) - - elif operation == "reset": + return True + + elif operation == "scene_modified": """ - Reset the scene to an empty state + Returns True if the current scene is dirty """ - while nuke.root().modified(): - # changes have been made to the scene - res = QtGui.QMessageBox.question(None, - "Save your script?", - "Your script has unsaved changes. Save before proceeding?", - QtGui.QMessageBox.Yes|QtGui.QMessageBox.No|QtGui.QMessageBox.Cancel) - - if res == QtGui.QMessageBox.Cancel: - return False - elif res == QtGui.QMessageBox.No: - break - else: - nuke.scriptSave() + return nuke.root().modified() + elif operation == "reset": + """ + Reset the scene to an empty state. + Does not check if saving is needed. + """ # now clear the script: nuke.scriptClear() - + nuke.createNode("Viewer") return True + else: raise TankError("Don't know how to perform scene operation '%s'" % operation) - - + + def _reset_write_node_render_paths(self): """ Use the tk-nuke-writenode app interface to find and reset @@ -110,11 +109,11 @@ def _reset_write_node_render_paths(self): write_node_app = self.parent.engine.apps.get("tk-nuke-writenode") if not write_node_app: return - + write_nodes = write_node_app.get_write_nodes() for write_node in write_nodes: write_node_app.reset_node_render_path(write_node) - + return len(write_nodes) > 0 - - \ No newline at end of file + + diff --git a/hooks/scene_operation_tk-softimage.py b/hooks/scene_operation_tk-softimage.py index 63e1a29..1999c76 100644 --- a/hooks/scene_operation_tk-softimage.py +++ b/hooks/scene_operation_tk-softimage.py @@ -18,29 +18,29 @@ class SceneOperation(Hook): """ - Hook called to perform an operation with the + Hook called to perform an operation with the current scene """ - + def execute(self, operation, file_path, **kwargs): """ Main hook entry point - + :operation: String Scene operation to perform - + :file_path: String File path to use if the operation requires it (e.g. open) - + :returns: Depends on operation: 'current_path' - Return the current scene file path as a String - 'reset' - True if scene was reset to an empty + 'reset' - True if scene was reset to an empty state, otherwise False all others - None """ - + if operation == "current_path": # return the current scene path scene_name = Application.ActiveProject.ActiveScene.Name @@ -54,25 +54,39 @@ def execute(self, operation, file_path, **kwargs): elif operation == "open": # open the specified scene Application.OpenScene(file_path, 0, 0) + return True elif operation == "save": # save the current scene: Application.SaveScene() + return True elif operation == "save_as": # save the scene as file_path: Application.SaveSceneAs(file_path, 0) + return True + + elif operation == "scene_modified": + """ + Returns True if the current scene is dirty + """ + dirty_count = Application.GetValue("Project.dirtycount") + if dirty_count: + return True + for model in Application.ActiveSceneRoot.Models: + dirty_count = model.GetPropertyFromName("dirty_count") + if dirty_count: + return True + return False elif operation == "reset": + """ + Reset the scene to an empty state. + Does not check if saving is needed. + """ # reset the scene to an empty state - try: - # use the standard Softimage mechanism to check - # for and save the file if required: - Application.NewScene("", 1) - except com_error: - # exception here means the user hit 'Cancel' - return False - else: - return True + Application.NewScene("", 0) + return True + else: raise TankError("Don't know how to perform scene operation '%s'" % operation) diff --git a/python/tk_multi_workfiles/save_as.py b/python/tk_multi_workfiles/save_as.py index f092edf..a50a06f 100644 --- a/python/tk_multi_workfiles/save_as.py +++ b/python/tk_multi_workfiles/save_as.py @@ -6,7 +6,7 @@ from itertools import chain import tank -from tank.platform.qt import QtCore, QtGui +from tank.platform.qt import QtCore, QtGui from tank import TankError from pprint import pprint @@ -15,31 +15,31 @@ class SaveAs(object): """ - + """ - + @staticmethod def show_save_as_dlg(app): """ - + """ handler = SaveAs(app) handler._show_save_as_dlg() - + def __init__(self, app): """ Construction """ self._app = app - + self._work_template = self._app.get_template("template_work") self._publish_template = self._app.get_template("template_publish") - + def _show_save_as_dlg(self): """ Show the save as dialog """ - + # get the current file path: try: current_path = self._get_current_file_path() @@ -49,7 +49,7 @@ def _show_save_as_dlg(self): "Unable to continue!" % e) QtGui.QMessageBox.critical(None, "Save As Error!", msg) return - + # determine if this is a publish path or not: is_publish = self._publish_template.validate(current_path) fields = {} @@ -70,7 +70,7 @@ def _show_save_as_dlg(self): else: name = default_name fields = self._app.context.as_template_fields(self._work_template) - + try: # make sure the work file name doesn't already exist: # note, this could potentially be slow so for now lets @@ -80,71 +80,72 @@ def _show_save_as_dlg(self): test_name = name if counter > 0: test_name = "%s%d" % (name, counter) - + test_fields = fields.copy() test_fields["name"] = test_name - + existing_files = self._app.tank.paths_from_template(self._work_template, test_fields, ["version"]) if not existing_files: name = test_name break - + except TankError, e: # this shouldn't be fatal so just log a debug message: self._app.log_debug("Warning - failed to find a default name for Tank Save-As: %s" % e) - - + + worker_cb = lambda details, wp=current_path, ip=is_publish: self.generate_new_work_file_path(wp, ip, details.get("name"), details.get("reset_version")) with AsyncWorker(worker_cb) as preview_updater: while True: # show modal dialog: from .save_as_form import SaveAsForm (res, form) = self._app.engine.show_modal(title, self._app, SaveAsForm, preview_updater, is_publish, name) - + if res == QtGui.QDialog.Accepted: # get details from UI: name = form.name reset_version = form.reset_version - + details = self.generate_new_work_file_path(current_path, is_publish, name, reset_version) new_path = details.get("path") msg = details.get("message") - + if not new_path: # something went wrong! QtGui.QMessageBox.information(None, "Unable to Save", "Unable to Save!\n\n%s" % msg) continue - + # ok, so do save-as: try: self.save_as(new_path) except Exception, e: self._app.log_exception("Something went wrong while saving!") - - break + return 0 + + return res else: - break - + return res + def save_as(self, new_path): """ Do actual save-as of the current scene as the new path - assumes all validity checking has already been done """ - + # always try to create folders: ctx_entity = self._app.context.task if self._app.context.task else self._app.context.entity self._app.tank.create_filesystem_structure(ctx_entity.get("type"), ctx_entity.get("id")) - + # and save the current file as the new path: self._save_current_file_as(new_path) - + def _save_current_file_as(self, path): """ Use hook to get the current work/scene file path """ self._app.execute_hook("hook_scene_operation", operation="save_as", file_path=path, context = self._app.context) - + def generate_new_work_file_path(self, current_path, current_is_publish, new_name, reset_version): """ Generate a new work file path from the current path taking into @@ -158,7 +159,7 @@ def generate_new_work_file_path(self, current_path, current_is_publish, new_name if not new_name: msg = "You must enter a name!" return {"message":msg} - + if not self._work_template.keys["name"].validate(new_name): msg = "Your filename contains illegal characters!" return {"message":msg} @@ -168,7 +169,7 @@ def generate_new_work_file_path(self, current_path, current_is_publish, new_name # start with fields from context: fields = self._app.context.as_template_fields(self._work_template) - + # add in any additional fields from current path: base_template = self._publish_template if current_is_publish else self._work_template if base_template.validate(current_path): @@ -177,7 +178,7 @@ def generate_new_work_file_path(self, current_path, current_is_publish, new_name else: # just make sure there is a version fields["version"] = 1 - + current_version = fields.get("version") current_name = fields.get("name") @@ -190,52 +191,52 @@ def generate_new_work_file_path(self, current_path, current_is_publish, new_name max_work_version = versioning.get_max_workfile_version(fields) max_publish_version = versioning.get_max_publish_version(new_name) max_version = max(max_work_version, max_publish_version) - - # now depending on what the source was + + # now depending on what the source was # and if the name has been changed: new_version = None if current_is_publish and new_name == current_name: # we're ok to just copy publish across and version up can_reset_version = False new_version = max_version + 1 - + if new_version != current_version+1: #(AD) - do we need a warning here? pass - + msg = None else: if max_version: # already have a publish and/or work file can_reset_version = False new_version = max_version + 1 - + if max_version == max_work_version: msg = "A work file with this name already exists. If you proceed, your file will use the next available version number." else: msg = "A publish file with this name already exists. If you proceed, your file will use the next available version number." - + else: # don't have an existing version can_reset_version = True msg = "" if reset_version: new_version = 1 - + # now create new path if new_version: fields["version"] = new_version new_work_path = self._work_template.apply_fields(fields) - + return {"path":new_work_path, "message":msg, "can_reset_version":can_reset_version} - - + + def _get_current_file_path(self): """ Use hook to get the current work/scene file path """ return self._app.execute_hook("hook_scene_operation", operation="current_path", file_path="", context = self._app.context) - - - - \ No newline at end of file + + + + diff --git a/python/tk_multi_workfiles/work_files.py b/python/tk_multi_workfiles/work_files.py index ed5e46a..96e1f46 100644 --- a/python/tk_multi_workfiles/work_files.py +++ b/python/tk_multi_workfiles/work_files.py @@ -8,23 +8,23 @@ from datetime import datetime import tank -from tank.platform.qt import QtCore, QtGui +from tank.platform.qt import QtCore, QtGui from tank import TankError from tank_vendor.shotgun_api3 import sg_timezone from .work_file import WorkFile class WorkFiles(object): - + def __init__(self, app): """ Construction """ self._app = app self._workfiles_ui = None - + self._user_details_cache = {} - + # set up the work area from the app: self._context = None self._configuration_is_valid = False @@ -32,19 +32,19 @@ def __init__(self, app): self._work_area_template = None self._publish_template = None self._publish_area_template = None - + initial_ctx = self._app.context if not initial_ctx or not initial_ctx: # TODO: load from setting pass - + self._update_current_work_area(initial_ctx) - + def show_dlg(self): """ - Show the main tank file manager dialog + Show the main tank file manager dialog """ - + from .work_files_form import WorkFilesForm self._workfiles_ui = self._app.engine.show_dialog("Tank File Manager", self._app, WorkFilesForm, self._app, self) @@ -53,20 +53,20 @@ def show_dlg(self): self._workfiles_ui.new_file.connect(self._on_new_file) self._workfiles_ui.show_in_fs.connect(self._on_show_in_file_system) self._workfiles_ui.show_in_shotgun.connect(self._on_show_in_shotgun) - + def find_files(self, user): """ Find files using the current context, work and publish templates - + If user is specified then HumanUser should be overriden to be this user when resolving paths. - + Will return a WorkFile instance for every file found in both work and publish areas """ if not self._work_template or not self._publish_template: return [] - + current_user = tank.util.get_current_user(self._app.tank) if current_user and user and user["id"] == current_user["id"]: # user is current user. Set to none not to override. @@ -80,7 +80,7 @@ def find_files(self, user): if user: work_fields["HumanUser"] = user["login"] work_file_paths = self._app.tank.paths_from_template(self._work_template, work_fields, ["version"]) - + # build an index of the published file tasks to use if we don't have a task in the context: publish_task_map = {} task_id_to_task_map = {} @@ -90,28 +90,28 @@ def find_files(self, user): if not task: continue - # the key for the path-task map is the 'version zero' work file that + # the key for the path-task map is the 'version zero' work file that # matches this publish path. This is constructed from the publish # fields together with any additional fields from the context etc. publish_fields = self._publish_template.get_fields(publish_path) publish_fields["version"] = 0 work_path_key = self._work_template.apply_fields(dict(chain(work_fields.iteritems(), publish_fields.iteritems()))) - + task_id_to_task_map[task["id"]] = task publish_task_map.setdefault(work_path_key, set()).add(task["id"]) - + # add entries for work files: file_details = [] handled_publish_files = set() - + for work_path in work_file_paths: # resolve the publish path: fields = self._work_template.get_fields(work_path) publish_path = self._publish_template.apply_fields(fields) - + handled_publish_files.add(publish_path) publish_details = publish_file_details.get(publish_path) - + # create file entry: details = {} if "version" in fields: @@ -121,7 +121,7 @@ def find_files(self, user): # entity is always the context entity: details["entity"] = self._context.entity - + if publish_details: # add other info from publish: details["task"] = publish_details.get("task") @@ -149,37 +149,37 @@ def find_files(self, user): task_ids = publish_task_map.get(key) if task_ids and len(task_ids) == 1: task = task_id_to_task_map[list(task_ids)[0]] - + details["task"] = task # get the local file modified time - ensure it has a time-zone set: details["modified_time"] = datetime.fromtimestamp(os.path.getmtime(work_path), tz=sg_timezone.local) - + # get the last modified by: last_user = self._get_file_last_modified_user(work_path) details["modified_by"] = last_user file_details.append(WorkFile(work_path, publish_path, True, publish_details != None, details)) - + # add entries for any publish files that don't have a work file for publish_path, publish_details in publish_file_details.iteritems(): if publish_path in handled_publish_files: continue - + # resolve the work path using work template fields + publish fields: publish_fields = self._publish_template.get_fields(publish_path) work_path = self._work_template.apply_fields(dict(chain(work_fields.iteritems(), publish_fields.iteritems()))) # create file entry: is_work_file = (work_path in work_file_paths) - details = {} + details = {} if "version" in publish_fields: details["version"] = publish_fields["version"] if "name" in publish_fields: details["name"] = publish_fields["name"] details["entity"] = self._context.entity - + # add additional details from publish record: details["task"] = publish_details.get("task") details["thumbnail"] = publish_details.get("image") @@ -187,11 +187,11 @@ def find_files(self, user): details["modified_by"] = publish_details.get("created_by", {}) details["publish_description"] = publish_details.get("description") details["published_file_id"] = publish_details.get("published_file_id") - - file_details.append(WorkFile(work_path, publish_path, is_work_file, True, details)) + + file_details.append(WorkFile(work_path, publish_path, is_work_file, True, details)) return file_details - + def _on_show_in_file_system(self, work_area, user): """ Show the work area/publish area path in the file system @@ -201,31 +201,31 @@ def _on_show_in_file_system(self, work_area, user): template = self._work_area_template if work_area else self._publish_area_template if not self._context or not template: return - + # now build fields to construct path with: fields = self._context.as_template_fields(template) if user: fields["HumanUser"] = user["login"] - + # try to build a path from the template with these fields: while template and template.missing_keys(fields): template = template.parent if not template: # failed to find a template with no missing keys! return - + # build the path: path = template.apply_fields(fields) except TankError, e: return - + # now find the deepest path that actually exists: while path and not os.path.exists(path): path = os.path.dirname(path) if not path: return path = path.replace("/", os.path.sep) - + # build the command: system = sys.platform if system == "linux2": @@ -236,93 +236,135 @@ def _on_show_in_file_system(self, work_area, user): cmd = "cmd.exe /C start \"Folder\" \"%s\"" % path else: raise TankError("Platform '%s' is not supported." % system) - + # run the command: exit_code = os.system(cmd) if exit_code != 0: - self._app.log_error("Failed to launch '%s'!" % cmd) - + self._app.log_error("Failed to launch '%s'!" % cmd) + def _on_show_in_shotgun(self, file): """ Show the specified published file in shotgun """ if not file.is_published or file.published_file_id is None: return - + # construct and open the url: published_file_entity_type = tank.util.get_published_file_entity_type(self._app.tank) url = "%s/detail/%s/%d" % (self._app.tank.shotgun.base_url, published_file_entity_type, file.published_file_id) QtGui.QDesktopServices.openUrl(QtCore.QUrl(url)) - + def have_valid_configuration_for_work_area(self): return self._configuration_is_valid - + def can_do_new_file(self): """ Do some validation to see if it's possible to start a new file with the selected context. """ if (not self._context - or not self._context.entity + or not self._context.entity or not self._work_area_template): return False - + # ensure that context contains everything required by the work area template: ctx_fields = self._context.as_template_fields(self._work_area_template) if self._work_area_template.missing_keys(ctx_fields): return False - + return True - + + def _execute_scene_operation_hook(self, operation, file_path=None): + return self._app.execute_hook("hook_scene_operation", operation=operation, file_path=file_path, context = self._context) + def _reset_current_scene(self): """ Use hook to clear the current scene """ - res = self._app.execute_hook("hook_scene_operation", operation="reset", file_path=None, context = self._context) + while self._execute_scene_operation_hook("scene_modified"): + # changes have been made to the scene + res = QtGui.QMessageBox.question(QtGui.QApplication.focusWidget(), + "Save your scene?", + "Your scene has unsaved changes. Save before proceeding?", + QtGui.QMessageBox.Yes|QtGui.QMessageBox.No|QtGui.QMessageBox.Cancel) + + if res == QtGui.QMessageBox.Cancel: + return False + elif res == QtGui.QMessageBox.No: + break + else: + scene_name = self._execute_scene_operation_hook("current_path") + try: + if scene_name: + snapshot = self._app.engine.commands.get("Snapshot...") + res = snapshot.get("callback")() + if not res: + return False + else: + tank_save = self._app.engine.commands.get("Tank Save As...") + res = tank_save.get("callback")() + if not res: + return False + except: + if scene_name: + res = self._execute_scene_operation_hook("save", file_path=scene_name) + else: + res = self._execute_scene_operation_hook("save_as") + + # check for any weirdness... + if res == None or not isinstance(res, bool): + raise TankError("Unexpected type returned from 'hook_scene_operation' - expected 'bool' but returned '%s'" % type(res).__name__) + + # if res we are done. + if res: + break + + # Now do the scene reset... + res = self._execute_scene_operation_hook("reset") if res == None or not isinstance(res, bool): raise TankError("Unexpected type returned from 'hook_scene_operation' - expected 'bool' but returned '%s'" % type(res).__name__) return res - + def _open_file(self, path): """ Use hook to open the specified file. """ # do open: - self._app.execute_hook("hook_scene_operation", operation="open", file_path=path, context = self._context) - + self._execute_scene_operation_hook("open", file_path=path) + def _copy_file(self, source_path, target_path): """ Use hook to copy a file from source to target path """ - self._app.execute_hook("hook_copy_file", - source_path=source_path, + self._app.execute_hook("hook_copy_file", + source_path=source_path, target_path=target_path) - + def _save_file(self): """ Use hook to save the current file """ - self._app.execute_hook("hook_scene_operation", operation="save", file_path=None, context = self._context) - + self._execute_scene_operation_hook("save") + def _restart_engine(self, ctx): """ Set context to the new context. This will clear the current scene and restart the current engine with the specified context """ - # restart engine: + # restart engine: try: current_engine_name = self._app.engine.name - - # stop current engine: - if tank.platform.current_engine(): + + # stop current engine: + if tank.platform.current_engine(): tank.platform.current_engine().destroy() - + # start engine with new context: tank.platform.start_engine(current_engine_name, ctx.tank, ctx) except Exception, e: raise TankError("Failed to change work area and start a new engine - %s" % e) - + def _create_folders(self, ctx): """ Create folders for specified context @@ -330,7 +372,7 @@ def _create_folders(self, ctx): # create folders: ctx_entity = ctx.task if ctx.task else ctx.entity self._app.tank.create_filesystem_structure(ctx_entity.get("type"), ctx_entity.get("id"), engine=self._app.engine.name) - + def _on_open_file(self, file, is_previous_version): """ Main function used to open a file when requested by the UI @@ -340,10 +382,10 @@ def _on_open_file(self, file, is_previous_version): # get the path of the file to open. Handle # other user sandboxes and publishes if need to - + src_path = None work_path = None - + if is_previous_version: # if the file is a previous version then we just open it # rather than attempting to copy it @@ -353,94 +395,94 @@ def _on_open_file(self, file, is_previous_version): work_path = file.publish_path if not os.path.exists(work_path): QtGui.QMessageBox.critical(self._workfiles_ui, "File doesn't exist!", "The published file\n\n%s\n\nCould not be found to open!" % work_path) - return + return else: # what we do depends on the current location of the file - + if file.is_local: # trying to open a work file... work_path = file.path - - try: + + try: fields = self._work_template.get_fields(work_path) except TankError, e: - QtGui.QMessageBox.critical(self._workfiles_ui, "Failed to resolve file path", + QtGui.QMessageBox.critical(self._workfiles_ui, "Failed to resolve file path", "Failed to resolve file path:\n\n%s\n\nagainst work template:\n\n%s\n\nUnable to open file!" % (work_path, e)) return except Exception, e: self._app.log_exception("Failed to resolve file path %s against work template" % work_path) return - + # check if file is in this users sandbox or another users: user = fields.get("HumanUser") if user: current_user = tank.util.get_current_user(self._app.tank) if current_user and current_user["login"] != user: - + fields["HumanUser"] = current_user["login"] # TODO: do we need to version up as well?? local_path = self._work_template.apply_fields(fields) - + if local_path != work_path: - + # get the actual user: sg_user = self._get_user_details(user) if sg_user: user = sg_user.get("name", user) - + # more than just an open so prompt user to confirm: #TODO: replace with tank dialog answer = QtGui.QMessageBox.question(self._workfiles_ui, "Open file from other user?", ("The work file you are opening:\n\n%s\n\n" "is in a user sandbox belonging to %s. Would " - "you like to copy the file to your sandbox and open it?" % (work_path, user)), + "you like to copy the file to your sandbox and open it?" % (work_path, user)), QtGui.QMessageBox.Yes | QtGui.QMessageBox.Cancel) if answer == QtGui.QMessageBox.Cancel: return - + src_path = work_path work_path = local_path - + else: # trying to open a publish: src_path = file.publish_path - + if not os.path.exists(src_path): QtGui.QMessageBox.critical(self._workfiles_ui, "File doesn't exist!", "The published file\n\n%s\n\nCould not be found to open!" % src_path) - return - + return + new_version = None - + # get the work path for the publish: - try: + try: fields = self._publish_template.get_fields(src_path) - + # add additional fields: current_user = tank.util.get_current_user(self._app.tank) if current_user: # populate if current user is defined. fields["HumanUser"] = current_user.get("login") - - # get next version: + + # get next version: new_version = self._get_next_available_version(fields) fields["version"] = new_version - + # construct work path: work_path = self._work_template.apply_fields(fields) except TankError, e: - QtGui.QMessageBox.critical(self._workfiles_ui, "Failed to get work file path", + QtGui.QMessageBox.critical(self._workfiles_ui, "Failed to get work file path", "Failed to resolve work file path from publish path:\n\n%s\n\n%s\n\nUnable to open file!" % (src_path, e)) return except Exception, e: self._app.log_exception("Failed to resolve work file path from publish path: %s" % src_path) return - + # prompt user to confirm: answer = QtGui.QMessageBox.question(self._workfiles_ui, "Open file from publish area?", ("The published file:\n\n%s\n\n" "will be copied to your work area, versioned " "up to v%03d and then opened.\n\n" - "Would you like to continue?" % (src_path, new_version)), + "Would you like to continue?" % (src_path, new_version)), QtGui.QMessageBox.Yes | QtGui.QMessageBox.Cancel) if answer == QtGui.QMessageBox.Cancel: return @@ -452,7 +494,7 @@ def _on_open_file(self, file, is_previous_version): if not work_path or not new_ctx: # can't do anything! return - + if new_ctx != self._app.context: # ensure folders exist. This serves the # dual purpose of populating the path @@ -461,47 +503,47 @@ def _on_open_file(self, file, is_previous_version): try: self._create_folders(new_ctx) except TankError, e: - QtGui.QMessageBox.critical(self._workfiles_ui, "Failed to create folders!", + QtGui.QMessageBox.critical(self._workfiles_ui, "Failed to create folders!", "Failed to create folders:\n\n%s!" % e) return except Exception, e: self._app.log_exception("Failed to create folders") return - + # if need to, copy file if src_path: # check that local path doesn't already exist: if os.path.exists(work_path): #TODO: replace with tank dialog answer = QtGui.QMessageBox.question(self._workfiles_ui, "Overwrite file?", - "The file\n\n%s\n\nalready exists. Would you like to overwrite it?" % (work_path), + "The file\n\n%s\n\nalready exists. Would you like to overwrite it?" % (work_path), QtGui.QMessageBox.Yes | QtGui.QMessageBox.Cancel) if answer == QtGui.QMessageBox.Cancel: return - + try: # copy file: self._copy_file(src_path, work_path) except TankError, e: - QtGui.QMessageBox.critical(self._workfiles_ui, "Copy file failed!", + QtGui.QMessageBox.critical(self._workfiles_ui, "Copy file failed!", "Copy of file failed!\n\n%s!" % e) return except Exception, e: self._app.log_exception("Copy file failed") - return - + return + # switch context (including do new file): try: # reset the current scene: if not self._reset_current_scene(): self._app.log_debug("Unable to perform New Scene operation after failing to reset scene!") return - + if new_ctx != self._app.context: # restart the engine with the new context self._restart_engine(new_ctx) except TankError, e: - QtGui.QMessageBox.critical(self._workfiles_ui, "Failed to change work area", + QtGui.QMessageBox.critical(self._workfiles_ui, "Failed to change work area", "Failed to change the work area to '%s':\n\n%s\n\nUnable to continue!" % (new_ctx, e)) return except Exception, e: @@ -512,13 +554,13 @@ def _on_open_file(self, file, is_previous_version): try: self._open_file(work_path) except TankError, e: - QtGui.QMessageBox.critical(self._workfiles_ui, "Failed to open file", + QtGui.QMessageBox.critical(self._workfiles_ui, "Failed to open file", "Failed to open file\n\n%s\n\n%s" % (work_path, e)) return except Exception, e: self._app.log_exception("Failed to open file %s!" % work_path) return - + # close work files UI as it will no longer # be valid anyway as the context has changed self._workfiles_ui.close() @@ -548,11 +590,11 @@ def _on_new_file(self): self._app.log_debug("Unable to perform New Scene operation after failing to reset scene!") return - if self._context != self._app.context: + if self._context != self._app.context: # restart the engine with the new context self._restart_engine(self._context) except TankError, e: - QtGui.QMessageBox.information(self._workfiles_ui, "Something went wrong!", + QtGui.QMessageBox.information(self._workfiles_ui, "Something went wrong!", "Something went wrong:\n\n%s!" % e) return except Exception, e: @@ -561,53 +603,53 @@ def _on_new_file(self): # close work files UI: self._workfiles_ui.close() - + def get_current_work_area(self): """ Get the current work area/context """ return self._context - + def change_work_area(self): """ Show a ui for the user to select a new work area/context """ from .select_work_area_form import SelectWorkAreaForm (res, widget) = self._app.engine.show_modal("Pick a Work Area", self._app, SelectWorkAreaForm, self._app, self) - - # make sure to explicitly call close so + + # make sure to explicitly call close so # that browser threads are cleaned up # correctly widget.close() - + if res == QtGui.QDialog.Accepted: - + # update the current work area: self._update_current_work_area(widget.context) - + # and return it: return self._context return None - + def create_new_task(self): """ - Called when user clicks the new task button + Called when user clicks the new task button on the select work area form """ - raise NotImplementedError - + raise NotImplementedError + def _update_current_work_area(self, ctx): """ Update the current work area being used """ if self._context != ctx: - + # update templates for the new context: templates = {} try: - templates = self._get_templates_for_context(ctx, ["template_work", - "template_work_area", + templates = self._get_templates_for_context(ctx, ["template_work", + "template_work_area", "template_publish", "template_publish_area"]) except TankError, e: @@ -616,17 +658,17 @@ def _update_current_work_area(self, ctx): self._configuration_is_valid = False else: self._configuration_is_valid = True - + #if templates is not None: self._work_template = templates.get("template_work") self._work_area_template = templates.get("template_work_area") self._publish_template = templates.get("template_publish") self._publish_area_template = templates.get("template_publish_area") self._context = ctx - + # TODO: validate templates? - - + + def _get_user_details(self, login_name): """ Get the shotgun HumanUser entry: @@ -641,11 +683,11 @@ def _get_user_details(self, login_name): pass self._user_details_cache[login_name] = sg_user return sg_user - + def _get_file_last_modified_user(self, path): """ Get the user details of the last person - to modify the specified file + to modify the specified file """ login_name = None if sys.platform == "win32": @@ -653,69 +695,69 @@ def _get_file_last_modified_user(self, path): pass else: try: - from pwd import getpwuid + from pwd import getpwuid login_name = getpwuid(os.stat(path).st_uid).pw_name except: pass - + if login_name: return self._get_user_details(login_name) - + return None - + def _get_published_file_details(self): """ Get the details of all published files that match the current publish template. """ - + # get list of published files for entity: filters = [["entity", "is", self._context.entity]] if self._context.task: filters.append(["task", "is", self._context.task]) - + published_file_entity_type = tank.util.get_published_file_entity_type(self._app.tank) sg_publish_fields = ["description", "version_number", "image", "created_at", "created_by", "name", "path", "task", "description"] sg_published_files = self._app.shotgun.find(published_file_entity_type, filters, sg_publish_fields) - + publish_files = {} for sg_file in sg_published_files: path = sg_file.get("path").get("local_path") - # make sure path matches publish template: + # make sure path matches publish template: if not self._publish_template.validate(path): continue - + details = sg_file.copy() details["path"] = path details["published_file_id"] = sg_file.get("id") - + publish_files[path] = details - + return publish_files - + def get_usersandbox_users(self): """ - Find all available user sandbox users for the + Find all available user sandbox users for the current work area. """ if not self._work_area_template: return - + # use the fields for the current context to get a list of work area paths: fields = self._context.as_template_fields(self._work_area_template) work_area_paths = self._app.tank.paths_from_template(self._work_area_template, fields, ["HumanUser"]) - + # from paths, find a unique list of user's: users = set() for path in work_area_paths: - + fields = self._work_area_template.get_fields(path) user = fields.get("HumanUser") - if user: + if user: users.add(user) - + # first look for details in cache: user_details = [] users_to_fetch = [] @@ -726,7 +768,7 @@ def get_usersandbox_users(self): else: if details: user_details.append(details) - + if users_to_fetch: # get remaining details from shotgun: filter = ["login", "in"] + list(users_to_fetch) @@ -738,18 +780,18 @@ def get_usersandbox_users(self): login = sg_user.get("login") if login not in users_to_fetch: continue - + self._user_details_cache[login] = sg_user user_details.append(sg_user) users_found.add(login) - + # and fill in any blanks so we don't bother searching again: for user in users_to_fetch: if user not in users_found: self._user_details_cache[user] = {} - + return user_details - + def _get_templates_for_context(self, context, keys): """ Find templates for the given context. @@ -759,37 +801,37 @@ def _get_templates_for_context(self, context, keys): raise TankError("Failed to find Work Files settings for context '%s'.\n\nPlease ensure that" " the Work Files app is installed for the environment that will be used for" " this context" % context) - + templates = {} for key in keys: template = self._app.get_template_from(settings, key) templates[key] = template - + return templates - - + + def _get_app_settings_for_context(self, context): """ Find settings for the app in the specified context """ if not context: return - - # find settings for all instances of app in + + # find settings for all instances of app in # the environment picked for the given context: other_settings = tank.platform.find_app_settings(self._app.engine.name, self._app.name, self._app.tank, context) - + if len(other_settings) == 1: return other_settings[0].get("settings") - + settings_by_engine = {} for settings in other_settings: settings_by_engine.setdefault(settings.get("engine_instance"), list()).append(settings) - - # can't handle more than one engine! + + # can't handle more than one engine! if len(settings_by_engine) != 1: return - + # ok, so have a single engine but multiple apps # lets try to find an app with the same instance # name: @@ -800,14 +842,14 @@ def _get_app_settings_for_context(self, context): break if not app_instance_name: return - + for engine_name, engine_settings in settings_by_engine.iteritems(): for settings in engine_settings: if settings.get("app_instance") == app_instance_name: return settings.get("settings") - - - - - + + + + +