diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..2d36d00 Binary files /dev/null and b/.DS_Store differ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cde4237 --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# macOS +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes + +# Python +__pycache__/ +*.py[cod] +*$py.class +*.egg-info/ +dist/ +build/ +*.egg + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# Environment +.env +*.log diff --git a/Python/.DS_Store b/Python/.DS_Store new file mode 100644 index 0000000..a2bc0a3 Binary files /dev/null and b/Python/.DS_Store differ diff --git a/Python/Startup/.DS_Store b/Python/Startup/.DS_Store new file mode 100644 index 0000000..c3818de Binary files /dev/null and b/Python/Startup/.DS_Store differ diff --git a/Python/Startup/nt_loader/__init__.py b/Python/Startup/nt_loader/__init__.py index e69de29..376a7c3 100644 --- a/Python/Startup/nt_loader/__init__.py +++ b/Python/Startup/nt_loader/__init__.py @@ -0,0 +1,187 @@ +""" +NukeTimelineLoader - nt_loader package init. + +Ensures required third-party dependencies (e.g. requests) are available +before the rest of the package is imported. This handles environments like +Hiero/Nuke 17+ where the bundled Python may not include these packages. + +Also pre-imports tk-core (tank) using PySide6 natively so its QtImporter +picks the correct code path, and patches Qt resource stubs removed in Qt6. +""" + +import sys +import os + +# --------------------------------------------------------------------------- +# Qt5 resource function stubs (must run before any tk-core / tank import) +# --------------------------------------------------------------------------- +def _patch_qt_resource_functions(): + """Ensure qRegisterResourceData / qUnregisterResourceData exist. + + tk-core's compiled Qt resource files call these functions which existed + in PySide2 (Qt5) but were removed in PySide6 (Qt6). We add no-op stubs + directly on PySide6.QtCore. This is safe to call multiple times. + """ + _stub = lambda *args, **kwargs: True + + try: + from PySide6 import QtCore as _qtcore + if not hasattr(_qtcore, "qRegisterResourceData"): + _qtcore.qRegisterResourceData = _stub + if not hasattr(_qtcore, "qUnregisterResourceData"): + _qtcore.qUnregisterResourceData = _stub + except ImportError: + pass + + # Also patch any PySide2.QtCore shim already in sys.modules + mod = sys.modules.get("PySide2.QtCore") + if mod is not None: + if not hasattr(mod, "qRegisterResourceData"): + mod.qRegisterResourceData = _stub + if not hasattr(mod, "qUnregisterResourceData"): + mod.qUnregisterResourceData = _stub + + +_patch_qt_resource_functions() + + +# --------------------------------------------------------------------------- +# Pre-import tank using PySide6 natively +# --------------------------------------------------------------------------- +def _import_tank_with_pyside6(): + """Pre-import tank while PySide2 shim is hidden from sys.modules. + + tk-core's QtImporter tries PySide2 first, then PySide6. The PySide2 + shim from menu.py makes the PySide2 path succeed initially, but then + pyside2_patcher fails because the underlying modules are PySide6 + (e.g. QTextCodec was removed in Qt6). + + By temporarily hiding the PySide2 shim, QtImporter skips the PySide2 + path and uses its native PySide6 support (pyside6_patcher) instead. + Once tank is imported, we restore the PySide2 shim for any other code + that may depend on it. + """ + # Save and temporarily remove PySide2/shiboken2 shim entries + saved = {} + for key in list(sys.modules): + if key == "PySide2" or key.startswith("PySide2.") or key == "shiboken2": + saved[key] = sys.modules.pop(key) + + try: + import tank # noqa: F401 - triggers QtImporter + print("[NukeTimelineLoader] tank imported via PySide6 path") + + # QtImporter may fail silently on BOTH PySide2 and PySide6 paths + # (e.g. missing pyside6_patcher), leaving QtCore = None. + # Fix: inject PySide6 modules directly into qt_abstraction. + try: + import tank.authentication.ui.qt_abstraction as _qa + if _qa.QtCore is None: + from PySide6 import QtCore, QtGui, QtWidgets, QtNetwork + # tk-core expects a PySide1-style API where QtGui contains + # everything from QtWidgets + some classes from QtCore. + # The pyside2/6_patcher normally does this, but since both + # patchers failed, we merge the modules ourselves. + import types + _merged_gui = types.ModuleType("PySide6.QtGui._merged") + # Start with all QtGui attributes + for attr in dir(QtGui): + if not attr.startswith("_"): + setattr(_merged_gui, attr, getattr(QtGui, attr)) + # Add all QtWidgets classes (PySide1 had these in QtGui) + for attr in dir(QtWidgets): + if not attr.startswith("_"): + setattr(_merged_gui, attr, getattr(QtWidgets, attr)) + # Add QtCore classes that PySide1 exposed via QtGui + for attr in ("QSortFilterProxyModel", "QItemSelectionModel", + "QStringListModel", "QAbstractProxyModel", + "QItemSelection", "QItemSelectionRange"): + if hasattr(QtCore, attr): + setattr(_merged_gui, attr, getattr(QtCore, attr)) + + _qa.QtCore = QtCore + _qa.QtGui = _merged_gui + _qa.QtNetwork = QtNetwork + print("[NukeTimelineLoader] Patched qt_abstraction with PySide6 modules") + except ImportError: + pass + + except Exception as exc: + print(f"[NukeTimelineLoader] WARNING: tank pre-import failed: {exc}") + finally: + # Restore PySide2 shim for other code that depends on it + sys.modules.update(saved) + + +# Only run on PySide6 environments (Hiero 17+) where the PySide2 shim exists +if "PySide2" in sys.modules: + try: + import PySide6 # noqa: F401 - check if we're in a PySide6 environment + _import_tank_with_pyside6() + except ImportError: + pass + +# --------------------------------------------------------------------------- +# Auto-install missing dependencies +# --------------------------------------------------------------------------- +# Format: { "import_name": "pip_name_or_url" } +_REQUIRED_PACKAGES = { + "requests": "requests", + "PIL": "pillow", + "fileseq": "fileseq", + "qtpy": "qtpy", + "cv2": "opencv-python", + "tank_vendor": "git+https://github.com/shotgunsoftware/tk-core.git@v0.21.7", +} + + +def _ensure_dependencies(): + """Check for required packages and pip-install any that are missing.""" + missing = [] + for import_name, pip_name in _REQUIRED_PACKAGES.items(): + try: + __import__(import_name) + except ImportError: + missing.append(pip_name) + + if not missing: + return + + # Determine a target directory for installed packages + target = os.environ.get("NTL_SITE_PACKAGES") + if not target: + # Use user site-packages as a safe default + import site + target = site.getusersitepackages() + + os.makedirs(target, exist_ok=True) + + # Ensure target is on sys.path so subsequent imports find the packages + if target not in sys.path: + sys.path.insert(0, target) + + print(f"[NukeTimelineLoader] Installing missing dependencies: {missing}") + try: + # Use pip._internal instead of subprocess because in Nuke/Hiero 17+ + # sys.executable points to the Nuke binary (not a Python interpreter), + # which causes SIGSEGV when invoked with "-m pip". + from pip._internal.cli.main import main as pip_main + pip_args = ["install", "--target", target, "--upgrade"] + missing + exit_code = pip_main(pip_args) + if exit_code == 0: + print("[NukeTimelineLoader] Dependencies installed successfully.") + # Refresh sys.path so newly installed packages are importable + import importlib + importlib.invalidate_caches() + else: + raise RuntimeError(f"pip exited with code {exit_code}") + except Exception as exc: + print( + f"[NukeTimelineLoader] WARNING: Could not auto-install dependencies: {exc}\n" + f" Please install manually by running in a terminal:\n" + f" pip3 install --target \"{target}\" {' '.join(missing)}\n" + f" Or use ntl_pip_dependency_installer.py" + ) + + +_ensure_dependencies() diff --git a/Python/Startup/nt_loader/__pycache__/__init__.cpython-311.pyc b/Python/Startup/nt_loader/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..5781498 Binary files /dev/null and b/Python/Startup/nt_loader/__pycache__/__init__.cpython-311.pyc differ diff --git a/Python/Startup/nt_loader/__pycache__/fn_crud.cpython-311.pyc b/Python/Startup/nt_loader/__pycache__/fn_crud.cpython-311.pyc new file mode 100644 index 0000000..c52aa6e Binary files /dev/null and b/Python/Startup/nt_loader/__pycache__/fn_crud.cpython-311.pyc differ diff --git a/Python/Startup/nt_loader/__pycache__/fn_globals.cpython-311.pyc b/Python/Startup/nt_loader/__pycache__/fn_globals.cpython-311.pyc new file mode 100644 index 0000000..8c50e8a Binary files /dev/null and b/Python/Startup/nt_loader/__pycache__/fn_globals.cpython-311.pyc differ diff --git a/Python/Startup/nt_loader/__pycache__/fn_helpers.cpython-311.pyc b/Python/Startup/nt_loader/__pycache__/fn_helpers.cpython-311.pyc new file mode 100644 index 0000000..bd59609 Binary files /dev/null and b/Python/Startup/nt_loader/__pycache__/fn_helpers.cpython-311.pyc differ diff --git a/Python/Startup/nt_loader/__pycache__/fn_hiero_func.cpython-311.pyc b/Python/Startup/nt_loader/__pycache__/fn_hiero_func.cpython-311.pyc new file mode 100644 index 0000000..fda64b8 Binary files /dev/null and b/Python/Startup/nt_loader/__pycache__/fn_hiero_func.cpython-311.pyc differ diff --git a/Python/Startup/nt_loader/__pycache__/fn_manifest_func.cpython-311.pyc b/Python/Startup/nt_loader/__pycache__/fn_manifest_func.cpython-311.pyc new file mode 100644 index 0000000..6b83874 Binary files /dev/null and b/Python/Startup/nt_loader/__pycache__/fn_manifest_func.cpython-311.pyc differ diff --git a/Python/Startup/nt_loader/__pycache__/fn_model.cpython-311.pyc b/Python/Startup/nt_loader/__pycache__/fn_model.cpython-311.pyc new file mode 100644 index 0000000..3316a89 Binary files /dev/null and b/Python/Startup/nt_loader/__pycache__/fn_model.cpython-311.pyc differ diff --git a/Python/Startup/nt_loader/__pycache__/fn_sg_func.cpython-311.pyc b/Python/Startup/nt_loader/__pycache__/fn_sg_func.cpython-311.pyc new file mode 100644 index 0000000..25a4df8 Binary files /dev/null and b/Python/Startup/nt_loader/__pycache__/fn_sg_func.cpython-311.pyc differ diff --git a/Python/Startup/nt_loader/__pycache__/fn_ui.cpython-311.pyc b/Python/Startup/nt_loader/__pycache__/fn_ui.cpython-311.pyc new file mode 100644 index 0000000..7fa9af3 Binary files /dev/null and b/Python/Startup/nt_loader/__pycache__/fn_ui.cpython-311.pyc differ diff --git a/Python/Startup/nt_loader/__pycache__/fn_workers.cpython-311.pyc b/Python/Startup/nt_loader/__pycache__/fn_workers.cpython-311.pyc new file mode 100644 index 0000000..3af3e00 Binary files /dev/null and b/Python/Startup/nt_loader/__pycache__/fn_workers.cpython-311.pyc differ diff --git a/Python/Startup/nt_loader/fn_globals.py b/Python/Startup/nt_loader/fn_globals.py index 6f0febb..a6b28b7 100644 --- a/Python/Startup/nt_loader/fn_globals.py +++ b/Python/Startup/nt_loader/fn_globals.py @@ -10,7 +10,7 @@ # __INTEGRATE__ Globals for Connection to Shotgrid/Flow # --- # setup global for studio SG/Flow site -SHOTGUN_URL = "https://your.studio.shotgrid" +SHOTGUN_URL = "https://blackshipvfx.shotgrid.autodesk.com" # retrieve the current status icons from shotgrid STATUS_PNG_URL = f"{SHOTGUN_URL}/images/sg_icon_image_map.png" # Below can vary depending on studio structure. If the default is blocked follow below instructions @@ -121,7 +121,7 @@ # Optional - Add API key string below if using API key connection. Note: enabling this approach will override above # authentication globals. WARNING you will need to customize this codebase as it is built with the tk-core web login # with authenticating users permission restrictions in mind. -SHOTGUN_API_KEY = None +SHOTGUN_API_KEY = "Ifzcp3xlaxgnansv!ugjqhqlx" # --- # __CUSTOMIZE__ Mixed OS path mapping @@ -136,14 +136,19 @@ # "Linux": ["/mnt/media/v", "/mnt/media/z"], # "Darwin": ["/media/v", "/media/z"] # OSX # } -SG_MEDIA_PATH_MAP = {} +SG_MEDIA_PATH_MAP = { + "Windows":["v:"], + "Linux": ["/mnt"], + "Darwin": ["/Volumes"] # OSX +} # --- # __CUSTOMIZE__ Globals to set correct localization directory # --- # To avoid repeated "choose localization directory" dialogs un comment below with desired path -# os.environ["SG_LOCALIZE_DIR"] = "" # Desired path can be driven by environment variable +if not os.environ.get("SG_LOCALIZE_DIR"): + os.environ["SG_LOCALIZE_DIR"] = os.path.expanduser("~/Documents/NukeTimelineLoader") DEFAULT_LOCALIZE_DIR = os.environ.get("SG_LOCALIZE_DIR", None) # Mostly used internally to assess if the project requires Shotgrid/Flow tags to be created SG_TAGS_CREATED = os.environ.get("SG_TAGS_CREATED", False) diff --git a/Python/Startup/nt_loader/fn_helpers.py b/Python/Startup/nt_loader/fn_helpers.py index 04d4be0..d4941fc 100644 --- a/Python/Startup/nt_loader/fn_helpers.py +++ b/Python/Startup/nt_loader/fn_helpers.py @@ -201,9 +201,9 @@ def convert_media_path_to_map(path): converted = pattern.sub(replacement, path) # Final normalization of the complete path return os.path.normpath(converted) - raise Exception( - "Error: fn_globals.py:SG_MEDIA_PATH_MAP set to unsupported OS for path substitution" - ) + # No mapping match found — return original path unchanged + # This is expected for localized/downloaded files that are already local paths + return path else: return path diff --git a/Python/Startup/nt_loader/fn_sg_func.py b/Python/Startup/nt_loader/fn_sg_func.py index ffa1930..45a77ef 100644 --- a/Python/Startup/nt_loader/fn_sg_func.py +++ b/Python/Startup/nt_loader/fn_sg_func.py @@ -112,13 +112,13 @@ def instance_handler(): """Handles authentication and SG class instantiation prompting for Auth if session timed out Returns: - session, sg (object, object): wrapped shotgrid api object for use in direct Qt calls + sg (object): wrapped shotgrid api object for use in direct Qt calls """ # Handle API key authentication if defined if SHOTGUN_API_KEY: sg = SGWrapper( SHOTGUN_URL, - script_name="api_user", + script_name="nuke_timeline_loader", api_key=SHOTGUN_API_KEY, sg_session_id=None, session_token=None, @@ -129,6 +129,8 @@ def instance_handler(): user = session_cache.get_current_user(SHOTGUN_URL) try: session_data = session_cache.get_session_data(SHOTGUN_URL, user) + if not session_data or "session_token" not in session_data: + raise Exception("No valid session data found") sg = SGWrapper( SHOTGUN_URL, sg_session_id=session_data["session_token"], @@ -141,8 +143,13 @@ def instance_handler(): print("Session Expired initializing SG login dialog") login_window = WebLoginDialog(True, hostname=SHOTGUN_URL) + login_window.exec_() # Result of successful login session = login_window.result() + if not session: + raise Exception( + "Authentication failed: login dialog was cancelled or returned no session." + ) user = session[1] session_cache.cache_session_data(SHOTGUN_URL, user, session[2]) session_cache.set_current_user(SHOTGUN_URL, user) @@ -164,7 +171,7 @@ def session_handler(): if SHOTGUN_API_KEY: sg = SGWrapper( SHOTGUN_URL, - script_name="api_user", + script_name="nuke_timeline_loader", api_key=SHOTGUN_API_KEY, sg_session_id=None, session_token=None, @@ -176,6 +183,8 @@ def session_handler(): user = session_cache.get_current_user(SHOTGUN_URL) try: session_data = session_cache.get_session_data(SHOTGUN_URL, user) + if not session_data or "session_token" not in session_data: + raise Exception("No valid session data found") sg = SGWrapper( SHOTGUN_URL, sg_session_id=session_data["session_token"], @@ -189,8 +198,13 @@ def session_handler(): print("Session Expired initializing SG login dialog") login_window = WebLoginDialog(True, hostname=SHOTGUN_URL) + login_window.exec_() # Result of successful login session = login_window.result() + if not session: + raise Exception( + "Authentication failed: login dialog was cancelled or returned no session." + ) user = session[1] session_cache.cache_session_data(SHOTGUN_URL, user, session[2]) session_cache.set_current_user(SHOTGUN_URL, user) @@ -260,6 +274,28 @@ def access_token(session_token): raise Exception("Failed to retrieve Auth token") +def get_sg_data(entity, sg, id=None, fields=True): + """SG query using API token + + Args: + entity (str): Name of SG entity to query + id (int, optional): specific sg id to query. Defaults to None. + fields (bool, optional): when True return all applicable fields. + Defaults to True. + + Raises: + Exception: Fails to find applicable data will raise + + Returns: + dict: dict of json response data relating to query + """ + if not fields: + params = [] + else: + params = list(sg.schema_field_read(entity)) + ["url"] + + return sg.find(entity, [], params) + def get_rest_data(access_token, entity, sg, id=None, fields=True): """Rest based SG query @@ -321,6 +357,38 @@ def fetch_css(url): raise Exception(f"Failed to fetch CSS. Status code: {response.status_code}") +def _get_status_code(stat): + """Helper to extract status code from either REST or SG API format. + + Args: + stat (dict): Status entity in REST format (attributes/relationships) + or SG Python API format (flat dict) + + Returns: + str: The status code (e.g. 'ip', 'fin', 'wtg') + """ + if "attributes" in stat: + return stat["attributes"]["code"] + return stat.get("code", "") + + +def _get_status_display_name(stat): + """Helper to extract display name from either REST or SG API format.""" + if "attributes" in stat: + return stat["attributes"].get("cached_display_name", "") + return stat.get("cached_display_name", "") + + +def _get_status_icon_name(stat): + """Helper to extract icon name from either REST or SG API format. + Falls back to status code when relationships data is not available. + """ + try: + return stat["relationships"]["icon"]["data"]["name"] + except (KeyError, TypeError): + return stat.get("code", "") + + def extract_css_info(css, png_url, sg_statuses): """Parse collected CSS file for icon information. SG uses a single png to drive all icons this will cut this png into usable icons @@ -340,20 +408,22 @@ def extract_css_info(css, png_url, sg_statuses): for match in matches: if png_file in match.group(0): for stat in sg_statuses: - if stat["relationships"]["icon"]["data"]["name"] in match.group(0): + icon_name = _get_status_icon_name(stat) + status_code = _get_status_code(stat) + if icon_name and icon_name in match.group(0): icons_info.append( { - "icon_name": stat["attributes"]["code"], + "icon_name": status_code, "width": int(match.group(2)), "height": int(match.group(3)), "x_offset": int(match.group(4)), "y_offset": int(match.group(5)), } ) - if stat["attributes"]["code"] == match.group(1): + if status_code == match.group(1): icons_info.append( { - "icon_name": stat["attributes"]["code"], + "icon_name": status_code, "width": int(match.group(2)), "height": int(match.group(3)), "x_offset": int(match.group(4)), @@ -456,14 +526,17 @@ def setup_sg_tags(sg, session_token, localize_path): Returns: (list): of dict pertaining to the currently setup tags for later use in Foundry manifest Base entity """ - token = access_token(session_token) - sg_statuses = get_rest_data(token, "Status", sg, fields=True) + if SHOTGUN_API_KEY: + sg_statuses = get_sg_data("Status", sg, fields=True) + else: + token = access_token(session_token) + sg_statuses = get_rest_data(token, "Status", sg, fields=True) tag_data = create_icons(STATUS_PNG_URL, STATUS_CSS_URL, localize_path, sg_statuses) tag_data = {tuple(sorted(d.items())): d for d in tag_data} for icon in list(tag_data.values()): for stat in sg_statuses: - if icon["name"] == stat["attributes"]["code"]: - icon["lname"] = stat["attributes"]["cached_display_name"] + if icon["name"] == _get_status_code(stat): + icon["lname"] = _get_status_display_name(stat) return list(tag_data.values()) diff --git a/Python/Startup/nt_loader/fn_ui.py b/Python/Startup/nt_loader/fn_ui.py index 87293b2..5a6ccc5 100644 --- a/Python/Startup/nt_loader/fn_ui.py +++ b/Python/Startup/nt_loader/fn_ui.py @@ -666,8 +666,14 @@ def init_ui(self): container_layout.setContentsMargins(0, 0, 0, 0) self.options_button = QPushButton() + try: + # Qt6 / PySide6 scoped enum + _sp_icon = self.style().StandardPixmap.SP_FileDialogContentsView + except AttributeError: + # Qt5 / PySide2 flat enum + _sp_icon = self.style().SP_FileDialogContentsView self.options_button.setIcon( - self.style().standardIcon(self.style().SP_FileDialogContentsView) + self.style().standardIcon(_sp_icon) ) self.options_button.setStyleSheet("border:none") self.options_button.setFixedSize(QSize(30, 30)) @@ -711,8 +717,14 @@ def init_ui(self): self.search_button = QPushButton() self.search_button.setStyleSheet("border:none") + try: + # Qt6 / PySide6 scoped enum + _sp_arrow = self.style().StandardPixmap.SP_ArrowRight + except AttributeError: + # Qt5 / PySide2 flat enum + _sp_arrow = self.style().SP_ArrowRight self.search_button.setIcon( - self.style().standardIcon(self.style().SP_ArrowRight) + self.style().standardIcon(_sp_arrow) ) self.search_button.setFixedSize(QSize(30, 30)) self.search_button.hide() diff --git a/Python/Startup/nt_loader/fn_workers.py b/Python/Startup/nt_loader/fn_workers.py index 4f14162..32c6751 100644 --- a/Python/Startup/nt_loader/fn_workers.py +++ b/Python/Startup/nt_loader/fn_workers.py @@ -124,27 +124,52 @@ def __init__(self, sg_instance_pool, download_file_path, sg_url, signals=None): self.url = sg_url self.download_file_path = download_file_path + def _download_via_requests(self): + """Fallback download using requests when SG API download_attachment fails.""" + response = requests.get(self.url, stream=True, verify=False) + response.raise_for_status() + os.makedirs(os.path.dirname(self.download_file_path), exist_ok=True) + with open(self.download_file_path, "wb") as f: + for chunk in response.iter_content(chunk_size=8192): + if chunk: + f.write(chunk) + def run(self): sg_instance = self.sg_instance_pool.get_sg_instance() + download_success = False try: attachment = {"url": self.url} result = sg_instance.download_attachment( attachment, self.download_file_path ) - if not result: - raise Exception("unable to download {}".format(self.url)) - except: - traceback_info = sys.exc_info() - exctype, value, tb = traceback_info - while tb.tb_next: - tb = tb.tb_next - func_name = tb.tb_frame.f_code.co_name - line_no = tb.tb_lineno + # Verify the file was actually written (not 0 bytes) + if not result or not os.path.exists(self.download_file_path) or os.path.getsize(self.download_file_path) == 0: + raise Exception("SG API download returned empty file") + download_success = True + except Exception as sg_error: + # SG API download failed — try direct requests fallback UPDATE_SIGNALS.details_text.emit( - True, - f"SGDownloadWorker Error in function {func_name} at line {line_no}: {str(value)}", + False, + f"SG API download failed, trying direct download...", ) + try: + self._download_via_requests() + if os.path.exists(self.download_file_path) and os.path.getsize(self.download_file_path) > 0: + download_success = True + else: + raise Exception("Direct download produced empty file") + except: + traceback_info = sys.exc_info() + exctype, value, tb = traceback_info + while tb.tb_next: + tb = tb.tb_next + func_name = tb.tb_frame.f_code.co_name + line_no = tb.tb_lineno + UPDATE_SIGNALS.details_text.emit( + True, + f"SGDownloadWorker Error in function {func_name} at line {line_no}: {str(value)}", + ) finally: self.sg_instance_pool.release_sg_instance(sg_instance) self.signals.finished.emit(self.download_file_path) diff --git a/Python/StartupUI/__pycache__/ntl_main.cpython-311.pyc b/Python/StartupUI/__pycache__/ntl_main.cpython-311.pyc new file mode 100644 index 0000000..1b1954f Binary files /dev/null and b/Python/StartupUI/__pycache__/ntl_main.cpython-311.pyc differ diff --git a/Python/StartupUI/ntl_main.py b/Python/StartupUI/ntl_main.py index 699d4f1..4b5a2a4 100644 --- a/Python/StartupUI/ntl_main.py +++ b/Python/StartupUI/ntl_main.py @@ -46,22 +46,39 @@ def after_project_load(event): """This sets up NT loader for the new project by hooking into the callback "kAfterNewProjectCreated" + BLACKSHIP OVERRIDE: + Into the callback "kAfterProjectLoad" instead. + + With ayon, there is a Project called 'Tag Presets' + which is loaded first, then when we open/create a project, + 'Tag Presets' is updated as a Startup Project. + Because of that the kAfterProjectLoad event is called multiple times. + That's why we added a condition for that callback happens after the + Tag Presets setup. + Args: event (object): Hiero callback event object . Unused in this function """ + # BLACKSHIP OVERRIDE + projects = hiero.core.projects(hiero.core.Project.kStartupProjects) + if not projects: + return + loading_dialog = LoadingDialog("Initializing\nNuke Timeline Loader") loading_dialog.show() def on_load(): - session_token, sg = nt_loader.fn_sg_func.session_handler() - widget = ShotgridLoaderWidget(sg, session_token, SCHEMA_MAP) - # widget.show() - wm = hiero.ui.windowManager() - wm.addWindow(widget) - loading_dialog.close() + try: + session_token, sg = nt_loader.fn_sg_func.session_handler() + widget = ShotgridLoaderWidget(sg, session_token, SCHEMA_MAP) + # widget.show() + wm = hiero.ui.windowManager() + wm.addWindow(widget) + finally: + loading_dialog.close() QTimer.singleShot(3000, on_load) # Register the NTL after_project_load function to be triggered on hiero callback -hiero.core.events.registerInterest("kAfterNewProjectCreated", after_project_load) +hiero.core.events.registerInterest("kAfterProjectLoad", after_project_load) diff --git a/ntl_pip_dependency_installer.py b/ntl_pip_dependency_installer.py index 9a5ddfc..886d90e 100644 --- a/ntl_pip_dependency_installer.py +++ b/ntl_pip_dependency_installer.py @@ -12,6 +12,10 @@ # __INTEGRATE__ Set the alternate site-packages location +# For Nuke/Hiero 15.x: +# alternate_location = "C:/Program Files/Nuke15.1v1/pythonextensions/site-packages" +# For Nuke/Hiero 17.x (uses Python 3.11+): +# alternate_location = "C:/Program Files/Nuke17.0v1/pythonextensions/site-packages" alternate_location = "C:/Program Files/Nuke15.1v1/pythonextensions/site-packages" # Ensure the alternate location exists