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