diff --git a/app.py b/app.py index 81c9e0a..b4ee8d0 100644 --- a/app.py +++ b/app.py @@ -16,21 +16,22 @@ def init_app(self): """ Called as the application is being initialized """ - + tk_multi_workfiles = self.import_module("tk_multi_workfiles") # register commands: self._work_files_handler = tk_multi_workfiles.WorkFiles(self) self.engine.register_command("Tank File Manager...", self._work_files_handler.show_dlg) + cmd = lambda app=self: tk_multi_workfiles.SaveAs.show_save_as_dlg(app) + self.engine.register_command("Tank Save As...", cmd) + # other commands are only valid if we have valid work and publish templates: if self.get_template("template_work") and self.get_template("template_publish"): - cmd = lambda app=self: tk_multi_workfiles.SaveAs.show_save_as_dlg(app) - self.engine.register_command("Tank Save As...", cmd) - + cmd = lambda app=self: tk_multi_workfiles.Versioning.show_change_version_dlg(app) self.engine.register_command("Version up Current Scene...", cmd) - + def destroy_app(self): self.log_debug("Destroying tk-multi-workfiles") - self._work_files_handler = None \ No newline at end of file + self._work_files_handler = None diff --git a/python/tk_multi_workfiles/save_as.py b/python/tk_multi_workfiles/save_as.py index f092edf..0fd4a6e 100644 --- a/python/tk_multi_workfiles/save_as.py +++ b/python/tk_multi_workfiles/save_as.py @@ -6,40 +6,80 @@ 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 from .async_worker import AsyncWorker +from .work_files import WorkFiles + 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") - + self._workfiles = WorkFiles(app) + + @property + def context(self): + """ + Returns the context from our internal + work files instance. + """ + return self._workfiles.context + + @property + def work_template(self): + """ + Returns the work_template from our + internal work files instance. + """ + return self._workfiles.work_template + + @property + def publish_template(self): + """ + Returns the publish_template from our + internal work files instance. + """ + return self._workfiles.publish_template + + def change_work_area(self): + """ + Calls change work area from our internal + work files instance. This will in turn + change the work area for save as due to + sharing state with work files. + """ + return self._workfiles.change_work_area() + + def restart_engine(self, new_ctx): + """ + Uses workfiles to restart the engine with the passed + context, new_ctx. + """ + self._workfiles.restart_engine(new_ctx) + 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,102 +89,115 @@ 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) + is_publish = False fields = {} title = "Tank Save As" name = "" - if is_publish: - fields = self._publish_template.get_fields(current_path) + # We might be in a context without a publish_template. We need to check + # to make sure publish_template is not None. + if self.publish_template and self.publish_template.validate(current_path): + is_publish = True + fields = self.publish_template.get_fields(current_path) title = "Copy to Work Area" name = fields.get("name") else: default_name = "scene" + name = default_name fields = {} - if self._work_template.validate(current_path): - fields = self._work_template.get_fields(current_path) - title = "Tank Save As" - name = fields.get("name") or default_name - 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 - # limit it: - counter_limit = 10 - for counter in range(0, counter_limit): - 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) - - + + # We might be in a context that doesn't have a template so we need + # to check work_template is not None. + if self.work_template: + if self.work_template.validate(current_path): + fields = self.work_template.get_fields(current_path) + title = "Tank Save As" + name = fields.get("name") or default_name + else: + name = default_name + fields = self.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 + # limit it: + counter_limit = 10 + for counter in range(0, counter_limit): + 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) - + (res, form) = self._app.engine.show_modal(title, self._app, SaveAsForm, self, 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) + + # If the context has changed during the save as event, + # we need to restart the engine. + if self.context != self._app.context: + # restart the engine with the new context + self.restart_engine(self.context) except Exception, e: self._app.log_exception("Something went wrong while saving!") - + break else: break - + 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")) - + ctx_entity = self.context.task if self.context.task else self.context.entity + self._app.tank.create_filesystem_structure(ctx_entity.get("type"), ctx_entity.get("id"), engine=self._app.engine.name) + # 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) - + self._app.execute_hook("hook_scene_operation", operation="save_as", file_path=path, context = self.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 @@ -154,12 +207,16 @@ def generate_new_work_file_path(self, current_path, current_is_publish, new_name msg = None can_reset_version = False + if not self.work_template: + msg = "You must select a work area!" + return {"message":msg, "disable_controls":True} + # validate name: if not new_name: msg = "You must enter a name!" return {"message":msg} - - if not self._work_template.keys["name"].validate(new_name): + + if not self.work_template.keys["name"].validate(new_name): msg = "Your filename contains illegal characters!" return {"message":msg} @@ -167,17 +224,18 @@ def generate_new_work_file_path(self, current_path, current_is_publish, new_name fields = {} # start with fields from context: - fields = self._app.context.as_template_fields(self._work_template) - + + fields = self.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 + base_template = self.publish_template if current_is_publish else self.work_template if base_template.validate(current_path): template_fields = base_template.get_fields(current_path) fields = dict(chain(template_fields.iteritems(), fields.iteritems())) else: # just make sure there is a version fields["version"] = 1 - + current_version = fields.get("version") current_name = fields.get("name") @@ -186,56 +244,59 @@ def generate_new_work_file_path(self, current_path, current_is_publish, new_name # find the current max work file and publish versions: from .versioning import Versioning - versioning = Versioning(self._app) + + # We explicitly pass the templates to Versioning because we may + # be in a different context than what the current engine/app is in. + versioning = Versioning(self._app, work_template=self.work_template, + publish_template=self.publish_template, context=self.context) 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) - + 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 + return self._app.execute_hook("hook_scene_operation", operation="current_path", file_path="", context = self.context) + + + + diff --git a/python/tk_multi_workfiles/save_as_form.py b/python/tk_multi_workfiles/save_as_form.py index a837233..39e8160 100644 --- a/python/tk_multi_workfiles/save_as_form.py +++ b/python/tk_multi_workfiles/save_as_form.py @@ -11,52 +11,59 @@ class SaveAsForm(QtGui.QWidget): """ UI for saving the current tank work file """ - + @property def exit_code(self): return self._exit_code - - def __init__(self, preview_updater, is_publish, name, parent = None): + + def __init__(self, handler, preview_updater, is_publish, name, parent = None): """ Construction """ QtGui.QWidget.__init__(self, parent) + self._handler = handler + self._preview_updater = preview_updater if self._preview_updater: self._preview_updater.work_done.connect(self._preview_info_updated) - + self._reset_version = False self._launched_from_publish = is_publish - + # set up the UI from .ui.save_as_form import Ui_SaveAsForm self._ui = Ui_SaveAsForm() self._ui.setupUi(self) - + self._ui.cancel_btn.clicked.connect(self._on_cancel) self._ui.continue_btn.clicked.connect(self._on_continue) self._ui.name_edit.textEdited.connect(self._on_name_edited) self._ui.name_edit.returnPressed.connect(self._on_name_return_pressed) self._ui.reset_version_cb.stateChanged.connect(self._on_reset_version_changed) + if is_publish: + self._ui.change_work_area_btn.setVisible(False) + else: + self._ui.change_work_area_btn.clicked.connect(self._change_work_area) + self._ui.name_edit.setText(name) if not self._launched_from_publish: # make sure text in name edit is selected ready to edit: self._ui.name_edit.setFocus() self._ui.name_edit.selectAll() - + # initialize the preview info: self._preview_info_updated({}, {}) - - # initialize line to be plain and the same colour as the text: + + # initialize line to be plain and the same colour as the text: self._ui.break_line.setFrameShadow(QtGui.QFrame.Plain) clr = QtGui.QApplication.palette().text().color() self._ui.break_line.setStyleSheet("#break_line{color: rgb(%d,%d,%d);}" % (clr.red() * 0.75, clr.green() * 0.75, clr.blue() * 0.75)) - # finally, start preview info update in background + # finally, start preview info update in background self._update_preview_info() - + def showEvent(self, e): """ On first show, make sure that the name edit is focused: @@ -64,75 +71,105 @@ def showEvent(self, e): self._ui.name_edit.setFocus() self._ui.name_edit.selectAll() QtGui.QWidget.showEvent(self, e) - + @property def name(self): """ Get and set the name """ return self._get_new_name() - + @property def reset_version(self): """ Get and set if the version number should be reset """ return self._should_reset_version() - + + def _enable_controls(self, val=True): + """ + Enables or disables all the input controls. + """ + self._ui.name_edit.setEnabled(val) + self._ui.continue_btn.setEnabled(val) + self._ui.cancel_btn.setEnabled(val) + self._ui.reset_version_cb.setEnabled(val) + self._ui.filename_preview_label.setEnabled(val) + self._ui.path_preview_edit.setEnabled(val) + def _on_cancel(self): """ Called when the cancel button is clicked """ self._exit_code = QtGui.QDialog.Rejected self.close() - + def _on_continue(self): """ Called when the continue button is clicked """ self._exit_code = QtGui.QDialog.Accepted self.close() - + + def _change_work_area(self): + """ + Changes the work area the file will be saved into. + """ + # disable controls while the change work area window + # is open. + self._enable_controls(False) + + # call the change work area method on + # SaveAs handler. + self._handler.change_work_area() + + # not sure if this is the best way but we want to wait + # a moment while the change work area window closes. + QtCore.QTimer.singleShot(200, self._update_preview_info) + def _on_name_edited(self, txt): self._update_preview_info() - + def _on_name_return_pressed(self): self._on_continue() - + def _on_reset_version_changed(self, state): if self._ui.reset_version_cb.isEnabled(): self._reset_version = self._ui.reset_version_cb.isChecked() self._update_preview_info() - + def _get_new_name(self): return str(self._ui.name_edit.text()).strip() - + def _should_reset_version(self): return self._reset_version - + def _update_preview_info(self): """ - + """ if self._preview_updater: self._preview_updater.do({"name":self._get_new_name(), "reset_version":self._should_reset_version()}) - + def _preview_info_updated(self, details, result): """ - + """ path = result.get("path") msg = result.get("message") can_reset_version = result.get("can_reset_version") - + + # we flip this result to be True if False and False if True. + enable_controls = not result.get("disable_controls", False) + # update name and work area previews: - path_preview = "" - name_preview = "" + path_preview = "

Unable to generate Path

" + name_preview = "

Unable to generate Path

" if path: path_preview, name_preview = os.path.split(path) self._ui.filename_preview_label.setText(name_preview) self._ui.path_preview_edit.setText(path_preview) - + # update header: header_txt = "" if msg: @@ -145,7 +182,7 @@ def _preview_info_updated(self, details, result): else: header_txt = ("Type in a name below and Tank will save the current scene") self._ui.header_label.setText(header_txt) - + # update reset version check box: if can_reset_version: self._ui.reset_version_cb.setChecked(self._reset_version) @@ -153,6 +190,10 @@ def _preview_info_updated(self, details, result): else: self._ui.reset_version_cb.setEnabled(False) self._ui.reset_version_cb.setChecked(False) - - - \ No newline at end of file + + # make sure the controls are enabled in case they + # were disabled (which change_work_area does). + self._enable_controls(enable_controls) + + + diff --git a/python/tk_multi_workfiles/ui/save_as_form.py b/python/tk_multi_workfiles/ui/save_as_form.py index d45759e..55961f0 100644 --- a/python/tk_multi_workfiles/ui/save_as_form.py +++ b/python/tk_multi_workfiles/ui/save_as_form.py @@ -122,6 +122,9 @@ def setupUi(self, SaveAsForm): self.horizontalLayout_3 = QtGui.QHBoxLayout() self.horizontalLayout_3.setContentsMargins(12, 8, 12, 12) self.horizontalLayout_3.setObjectName("horizontalLayout_3") + self.change_work_area_btn = QtGui.QPushButton(SaveAsForm) + self.change_work_area_btn.setObjectName("change_work_area_btn") + self.horizontalLayout_3.addWidget(self.change_work_area_btn) spacerItem1 = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) self.horizontalLayout_3.addItem(spacerItem1) self.cancel_btn = QtGui.QPushButton(SaveAsForm) @@ -156,6 +159,7 @@ def retranslateUi(self, SaveAsForm): "

line 3

", None, QtGui.QApplication.UnicodeUTF8)) self.label_4.setText(QtGui.QApplication.translate("SaveAsForm", "

Preview:

", None, QtGui.QApplication.UnicodeUTF8)) self.label_6.setText(QtGui.QApplication.translate("SaveAsForm", "

Work Area:

", None, QtGui.QApplication.UnicodeUTF8)) + self.change_work_area_btn.setText(QtGui.QApplication.translate("SaveAsForm", "Change Work Area...", None, QtGui.QApplication.UnicodeUTF8)) self.cancel_btn.setText(QtGui.QApplication.translate("SaveAsForm", "Cancel", None, QtGui.QApplication.UnicodeUTF8)) self.continue_btn.setText(QtGui.QApplication.translate("SaveAsForm", "Save", None, QtGui.QApplication.UnicodeUTF8)) diff --git a/python/tk_multi_workfiles/work_files.py b/python/tk_multi_workfiles/work_files.py index ed5e46a..3cf1f6d 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,54 @@ 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) - + + @property + def context(self): + """ + Read-only accessor for the current internal context. + """ + return self._context + + @property + def work_template(self): + """ + Read-only accessor for the current work_template. + """ + return self._work_template + + @property + def work_area_template(self): + """ + Read-only accessor for the current work_area_template. + """ + return self._work_area_template + + @property + def publish_template(self): + """ + Read-only accessor for the current publish_template. + """ + return self._publish_template + + @property + def publish_area_template(self): + """ + Read-only accessor for the current publish_area_template. + """ + return self._publish_area_template + 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 +88,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 +115,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 +125,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 +156,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 +184,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 +222,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 +236,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,44 +271,44 @@ 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 _reset_current_scene(self): """ Use hook to clear the current scene @@ -282,47 +317,47 @@ def _reset_current_scene(self): 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) - + 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) - - def _restart_engine(self, ctx): + + 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 +365,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 +375,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 +388,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 +487,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 +496,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) + 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,18 +547,17 @@ 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() - def _get_next_available_version(self, fields): """ Get the next available version @@ -548,11 +582,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) + 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 +595,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 +650,16 @@ 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 +674,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 +686,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 +759,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 +771,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 +792,36 @@ 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 +832,8 @@ 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") - - - - - -